root/branches/release-34/lib/MT/AtomServer.pm @ 1825

Revision 1825, 36.0 kB (checked in by fumiakiy, 20 months ago)

Return appropriate error contents in error condition from AtomServer. BugId:68405

URL to the category document in the service document was wrong. BugId:67832

Thanks Witha System for these bug reports!

  • Property svn:keywords set to Author Date Id Revision
Line 
1# Movable Type (r) Open Source (C) 2001-2008 Six Apart, Ltd.
2# This program is distributed under the terms of the
3# GNU General Public License, version 2.
4#
5# $Id$
6
7package MT::AtomServer;
8use strict;
9
10use MT::I18N qw( encode_text );
11use XML::Atom;
12use XML::Atom::Util qw( first textValue );
13use base qw( MT::App );
14use MIME::Base64 ();
15use Digest::SHA1 ();
16use MT::Atom;
17use MT::Util qw( encode_xml );
18use MT::Author;
19
20use constant NS_SOAP => 'http://schemas.xmlsoap.org/soap/envelope/';
21use constant NS_WSSE => 'http://schemas.xmlsoap.org/ws/2002/07/secext';
22use constant NS_WSU => 'http://schemas.xmlsoap.org/ws/2002/07/utility';
23
24sub init {
25    my $app = shift;
26    $app->{no_read_body} = 1
27        if $app->request_method eq 'POST' || $app->request_method eq 'PUT';
28    $app->SUPER::init(@_) or return $app->error("Initialization failed");
29    $app->request_content
30        if $app->request_method eq 'POST' || $app->request_method eq 'PUT';
31    $app->add_methods(
32        handle => \&handle,
33    );
34    $app->{default_mode} = 'handle';
35    $app->{is_admin} = 0;
36    $app->{warning_trace} = 0;
37    $app;
38}
39
40sub handle {
41    my $app = shift;
42
43    my $out = eval {
44        (my $pi = $app->path_info) =~ s!^/!!;
45        my($subapp, @args) = split /\//, $pi;
46        $app->{param} = {};
47        for my $arg (@args) {
48            my($k, $v) = split /=/, $arg, 2;
49            $app->{param}{$k} = $v;
50        }
51        if (my $action = $app->get_header('SOAPAction')) {
52            $app->{is_soap} = 1;
53            $action =~ s/"//g; # "
54            my($method) = $action =~ m!/([^/]+)$!;
55            $app->request_method($method);
56        }
57        my $apps = $app->config->AtomApp;
58        if (my $class = $apps->{$subapp}) {
59            bless $app, $class;
60        }
61        my $out = $app->handle_request;
62        return unless defined $out;
63        if ($app->{is_soap}) {
64            $out =~ s!^(<\?xml.*?\?>)!!;
65            $out = <<SOAP;
66$1
67<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
68    <soap:Body>$out</soap:Body>
69</soap:Envelope>
70SOAP
71        }
72        return $out;
73    };
74    if (my $e = $@) {
75        $app->error(500, $e);
76        $app->show_error("Internal Error");
77    }
78    return $out;
79}
80
81sub handle_request {
82    1;
83}
84
85sub error {
86    my $app = shift;
87    my($code, $msg) = @_;
88    return unless ref($app);
89    if ($code && $msg) {
90        chomp($msg = encode_xml($msg)); 
91        $app->response_code($code);
92        $app->response_message($msg);
93        $app->response_content_type('text/xml'); 
94        $app->response_content("<error>$msg</error>"); 
95    }
96    elsif ($code) {
97        return $app->SUPER::error($code);
98    }
99    return undef;
100}
101
102sub show_error {
103    my $app = shift;
104    my($err) = @_;
105    chomp($err = encode_xml($err));
106    if ($app->{is_soap}) {
107        my $code = $app->response_code;
108        if ($code >= 400) {
109            $app->response_code(500);
110            $app->response_message($err);
111        }
112        return <<FAULT;
113<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
114  <soap:Body>
115    <soap:Fault>
116      <faultcode>$code</faultcode>
117      <faultstring>$err</faultstring>
118    </soap:Fault>
119  </soap:Body>
120</soap:Envelope>
121FAULT
122    } else {
123        return <<ERR;
124<error>$err</error>
125ERR
126    }
127}
128
129sub get_auth_info {
130    my $app = shift;
131    my %param;
132    if ($app->{is_soap}) {
133        my $xml = $app->xml_body;
134        my $auth = first($xml, NS_WSSE, 'UsernameToken');
135        $param{Username} = textValue($auth, NS_WSSE, 'Username');
136        $param{PasswordDigest} = textValue($auth, NS_WSSE, 'Password');
137        $param{Nonce} = textValue($auth, NS_WSSE, 'Nonce');
138        $param{Created} = textValue($auth, NS_WSU, 'Created');
139    } else {
140        my $req = $app->get_header('X-WSSE')
141            or return $app->auth_failure(401, 'X-WSSE authentication required');
142        $req =~ s/^WSSE //;
143        my ($profile);
144        ($profile, $req) = $req =~ /(\S+),?\s+(.*)/;
145        return $app->error(400, "Unsupported WSSE authentication profile") 
146            if $profile !~ /\bUsernameToken\b/i;
147        for my $i (split /,\s*/, $req) {
148            my($k, $v) = split /=/, $i, 2;
149            $v =~ s/^"//;
150            $v =~ s/"$//;
151            $param{$k} = $v;
152        }
153    }
154    \%param;
155}
156
157sub authenticate {
158    my $app = shift;
159    my $auth = $app->get_auth_info
160        or return $app->auth_failure(401, "Unauthorized");
161    for my $f (qw( Username PasswordDigest Nonce Created )) {
162        return $app->auth_failure(400, "X-WSSE requires $f")
163            unless $auth->{$f};
164    }
165    require MT::Session;
166    my $nonce_record = MT::Session->load($auth->{Nonce});
167   
168    if ($nonce_record && $nonce_record->id eq $auth->{Nonce}) {
169        return $app->auth_failure(403, "Nonce already used");
170    }
171    $nonce_record = new MT::Session();
172    $nonce_record->set_values({
173        id => $auth->{Nonce},
174        start => time,
175        kind => 'AN'
176    });
177    $nonce_record->save();
178# xxx Expire sessions on shorter timeout?
179    my $enc = $app->config('PublishCharset');
180    my $username = encode_text($auth->{Username},undef,$enc);
181    my $user = MT::Author->load({ name => $username, type => 1 })
182        or return $app->auth_failure(403, 'Invalid login');
183    return $app->auth_failure(403, 'Invalid login')
184        unless $user->api_password;
185    return $app->auth_failure(403, 'Invalid login')
186        unless $user->is_active;
187    my $created_on_epoch = $app->iso2epoch($auth->{Created});
188    if (abs(time - $created_on_epoch) > $app->config('WSSETimeout')) {
189        return $app->auth_failure(403, 'X-WSSE UsernameToken timed out');
190    }
191    $auth->{Nonce} = MIME::Base64::decode_base64($auth->{Nonce});
192    my $expected = Digest::SHA1::sha1_base64(
193         $auth->{Nonce} . $auth->{Created} . $user->api_password);
194    # Some base64 implementors do it wrong and don't put the =
195    # padding on the end. This should protect us against that without
196    # creating any holes.
197    $expected =~ s/=*$//;
198    $auth->{PasswordDigest} =~ s/=*$//;
199    #print STDERR "expected $expected and got " . $auth->{PasswordDigest} . "\n";
200    return $app->auth_failure(403, 'X-WSSE PasswordDigest is incorrect')
201        unless $expected eq $auth->{PasswordDigest};
202    $app->{user} = $user;
203
204    ## update session so the user will be counted as active
205    require MT::Session;
206    my $sess_active = MT::Session->load( { kind => 'UA', name => $user->id } );
207    if (!$sess_active) {
208        $sess_active = MT::Session->new;
209        $sess_active->id($app->make_magic_token());
210        $sess_active->kind('UA'); # UA == User Activation
211        $sess_active->name($user->id);
212    }
213    $sess_active->start(time);
214    $sess_active->save;
215    return 1;
216}
217
218sub auth_failure {
219    my $app = shift;
220    $app->set_header('WWW-Authenticate', 'WSSE profile="UsernameToken"');
221    return $app->error(@_);
222}
223
224sub xml_body {
225    my $app = shift;
226    unless (exists $app->{xml_body}) {
227        if (LIBXML) {
228            my $parser = XML::LibXML->new;
229            $app->{xml_body} = $parser->parse_string($app->request_content);
230        } else {
231            my $xp = XML::XPath->new(xml => $app->request_content);
232            $app->{xml_body} = ($xp->find('/')->get_nodelist)[0];
233        }
234    }
235    $app->{xml_body};
236}
237
238sub atom_body {
239    my $app = shift;
240    my $atom;
241    if ($app->{is_soap}) {
242        my $xml = $app->xml_body;
243        $atom = MT::Atom::Entry->new(Elem => first($xml, NS_SOAP, 'Body'))
244            or return $app->error(500, MT::Atom::Entry->errstr);
245    } else {
246        $atom = MT::Atom::Entry->new(Stream => \$app->request_content)
247            or return $app->error(500, MT::Atom::Entry->errstr);
248    }
249    $atom;
250}
251
252# $target_zone is expected to be a number of hours from GMT
253sub iso2ts {
254    my $app = shift;
255    my($ts, $target_zone) = @_;
256    return unless $ts =~ /^(\d{4})(?:-?(\d{2})(?:-?(\d\d?)(?:T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|([+-]\d{2}:\d{2}))?)?)?)?/;
257    my($y, $mo, $d, $h, $m, $s, $zone) =
258        ($1, $2 || 1, $3 || 1, $4 || 0, $5 || 0, $6 || 0, $7);
259    if ($zone) {
260        my ($zh, $zm) = $zone =~ /([+-]\d\d):(\d\d)/;
261        use Time::Local qw( timegm );
262        my $ts = timegm( $s, $m, $h, $d, $mo - 1, $y - 1900 );
263        if ($zone ne 'Z') {
264            require MT::DateTime;
265            my $tz_secs = MT::DateTime->tz_offset_as_seconds($zone);
266            $ts -= $tz_secs;
267        }
268        if ($target_zone) {
269            my $tz_secs = (3600 * int($target_zone) + 
270                           60 * abs($target_zone - int($target_zone)));
271            $ts += $tz_secs;
272        }
273        ($s, $m, $h, $d, $mo, $y) = gmtime( $ts );
274        $y += 1900; $mo++;
275    }
276    sprintf("%04d%02d%02d%02d%02d%02d", $y, $mo, $d, $h, $m, $s);
277}
278
279sub iso2epoch {
280    my $app = shift;
281    my($ts) = @_;
282    return unless $ts =~ /^(\d{4})(?:-?(\d{2})(?:-?(\d\d?)(?:T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|([+-]\d{2}:\d{2}))?)?)?)?/;
283    my($y, $mo, $d, $h, $m, $s, $zone) =
284        ($1, $2 || 1, $3 || 1, $4 || 0, $5 || 0, $6 || 0, $7);
285
286    use Time::Local;
287    my $dt = timegm($s, $m, $h, $d, $mo-1, $y);
288    if ($zone && $zone ne 'Z') {
289        require MT::DateTime;
290        my $tz_secs = MT::DateTime->tz_offset_as_seconds($zone);
291        $dt -= $tz_secs;
292    }
293    $dt;
294}
295
296package MT::AtomServer::Weblog;
297use strict;
298
299use MT::I18N qw( encode_text );
300use XML::Atom;
301use XML::Atom::Feed;
302use base qw( MT::AtomServer );
303use MT::Blog;
304use MT::Entry;
305use MT::Util qw( encode_xml format_ts );
306use MT::Permission;
307use File::Spec;
308use File::Basename;
309
310use constant NS_APP => 'http://www.w3.org/2007/app';
311use constant NS_DC => 'http://purl.org/dc/elements/1.1/';
312use constant NS_TYPEPAD => 'http://sixapart.com/atom/typepad#';
313
314sub script { $_[0]->{cfg}->AtomScript . '/1.0' }
315
316sub atom_content_type   { 'application/atom+xml' }
317sub atom_x_content_type { 'application/atom+xml' }
318
319sub edit_link_rel { 'edit' }
320sub get_posts_order_field { 'modified_on' }
321
322sub new_feed {
323    my $app = shift;
324    XML::Atom::Feed->new( Version => 1.0 );
325}
326
327sub new_with_entry {
328    my $app = shift;
329    my ($entry) = @_;
330    my $atom = MT::Atom::Entry->new_with_entry( $entry, Version => 1.0 );
331
332    my $mo = MT::Atom::Entry::_create_issued($entry->modified_on, $entry->blog);
333    $atom->set(NS_APP(), 'edited', $mo);
334
335    $atom;
336}
337
338sub apply_basename {
339    my $app = shift;
340    my ($entry, $atom) = @_;
341
342    if (my $basename = $app->get_header('Slug')) {
343        my $entry_class = ref $entry;
344        my $basename_uses = $entry_class->count({
345            blog_id  => $entry->blog_id,
346            basename => $basename,
347            ($entry->id ? ( id => { op => '!=', value => $entry->id } ) : ()),
348        });
349        if ($basename_uses) {
350            $basename = MT::Util::make_unique_basename($entry);
351        }
352
353        $entry->basename($basename);
354    }
355
356    $entry;
357}
358
359sub handle_request {
360    my $app = shift;
361    $app->authenticate || return;
362    if (my $svc = $app->{param}{svc}) {
363        if ($svc eq 'upload') {
364            return $app->handle_upload;
365        } elsif ($svc eq 'categories') {
366            return $app->get_categories;
367        }
368    }
369    my $method = $app->request_method;
370    if ($method eq 'POST') {
371        return $app->new_post;
372    } elsif ($method eq 'PUT') {
373        return $app->edit_post;
374    } elsif ($method eq 'DELETE') {
375        return $app->delete_post;
376    } elsif ($method eq 'GET') {
377        if ($app->{param}{entry_id}) {
378            return $app->get_post;
379        } elsif ($app->{param}{blog_id}) {
380            return $app->get_posts;
381        } else {
382            return $app->get_weblogs;
383        }
384    }
385}
386
387sub authenticate {
388    my $app = shift;
389
390    $app->SUPER::authenticate or return;
391    if (my $blog_id = $app->{param}{blog_id}) {
392        $app->{blog} = MT::Blog->load($blog_id)
393            or return $app->error(400, "Invalid blog ID '$blog_id'");
394        $app->{user} 
395            or return $app->error(403, "Authenticate");
396        if ($app->{user}->is_superuser()) {
397            $app->{perms} = new MT::Permission;
398            $app->{perms}->blog_id($blog_id);
399            $app->{perms}->author_id($app->{user}->id);
400            $app->{perms}->can_administer_blog(1);
401            return 1;
402        }
403        my $perms = $app->{perms} = MT::Permission->load({
404                    author_id => $app->{user}->id,
405                    blog_id => $app->{blog}->id });
406        return $app->error(403, "Permission denied.") unless $perms && $perms->can_create_post;
407    }
408    1;
409}
410
411sub publish {
412    my $app = shift;
413    my($entry, $no_ping) = @_;
414    my $blog = MT::Blog->load($entry->blog_id)
415        or return;
416    $app->rebuild_entry( Entry => $entry, Blog => $blog,
417                         BuildDependencies => 1 ) or return;
418    unless ($no_ping) {
419        $app->ping_and_save( Entry => $entry, Blog => $blog )
420            or return;
421    }
422    1;
423}
424
425sub get_weblogs {
426    my $app = shift;
427    my $user = $app->{user};
428    my $iter = $user->is_superuser
429        ? MT::Blog->load_iter()
430        : MT::Permission->load_iter({ author_id => $user->id });
431    my $base = $app->base . $app->uri;
432
433    # TODO: libxml support? XPath should always be available...
434    require XML::XPath;
435    require XML::XPath::Node::Element;
436    require XML::XPath::Node::Namespace;
437    require XML::XPath::Node::Text;
438
439    my $doc = XML::XPath::Node::Element->new('service');
440    my $app_ns = XML::XPath::Node::Namespace->new('#default' => NS_APP());
441    $doc->appendNamespace($app_ns);
442    my $atom_ns = XML::XPath::Node::Namespace->new('atom' => 'http://www.w3.org/2005/Atom');
443    $doc->appendNamespace($atom_ns);
444
445    while (my $thing = $iter->()) {
446        # TODO: provide media collection if author can upload to this blog.
447        if ($thing->isa('MT::Permission')) {
448            next if !$thing->can_create_post;
449        }
450
451        my $blog = $thing->isa('MT::Blog') ? $thing
452            : MT::Blog->load($thing->blog_id);
453        next unless $blog;
454        my $uri = $base . '/blog_id=' . $blog->id;
455
456        my $workspace = XML::XPath::Node::Element->new('workspace');
457        $doc->appendChild($workspace);
458
459        my $title = XML::XPath::Node::Element->new('atom:title', 'atom');
460        $title->appendChild(XML::XPath::Node::Text->new($blog->name));
461        $workspace->appendChild($title);
462
463        my $entries = XML::XPath::Node::Element->new('collection');
464        $entries->appendAttribute(XML::XPath::Node::Attribute->new('href', $uri));
465        $workspace->appendChild($entries);
466
467        my $e_title = XML::XPath::Node::Element->new('atom:title', 'atom');
468        $e_title->appendChild(XML::XPath::Node::Text->new(MT->translate('Entries')));
469        $entries->appendChild($e_title);
470
471        my $cats = XML::XPath::Node::Element->new('categories');
472        $cats->appendAttribute(XML::XPath::Node::Attribute->new('href', $uri . '/svc=categories'));
473        $entries->appendChild($cats);
474    }
475    $app->response_code(200);
476    $app->response_content_type('application/atomsvc+xml');
477    '<?xml version="1.0" encoding="utf-8"?>' . "\n" .                                                         
478        $doc->toString;
479}
480
481sub get_categories {
482    my $app = shift;
483    my $blog = $app->{blog};
484
485    # TODO: libxml support? XPath should always be available...
486    require XML::XPath;
487    require XML::XPath::Node::Element;
488    require XML::XPath::Node::Namespace;
489    require XML::XPath::Node::Text;
490
491    my $doc = XML::XPath::Node::Element->new('categories');
492    my $app_ns = XML::XPath::Node::Namespace->new('#default' => NS_APP());
493    $doc->appendNamespace($app_ns);
494    my $atom_ns = XML::XPath::Node::Namespace->new('atom' => 'http://www.w3.org/2005/Atom');
495    $doc->appendNamespace($atom_ns);
496    $doc->appendAttribute(XML::XPath::Node::Attribute->new('fixed', 'yes'));
497
498    my $iter = MT::Category->load_iter({ blog_id => $blog->id });
499    while (my $cat = $iter->()) {
500        my $cat_node = XML::XPath::Node::Element->new('atom:category', 'atom');
501        $cat_node->appendAttribute(XML::XPath::Node::Attribute->new('term', $cat->label));
502        $doc->appendChild($cat_node);
503    }
504
505    $app->response_code(200);
506    $app->response_content_type('application/atomcat+xml');
507    '<?xml version="1.0" encoding="utf-8"?>' . "\n" .                                                         
508        $doc->toString;
509}
510
511sub new_post {
512    my $app = shift;
513    my $atom = $app->atom_body or return $app->error(500, "No body!");
514    my $blog = $app->{blog};
515    my $user = $app->{user};
516    my $perms = $app->{perms};
517    my $enc = $app->config('PublishCharset');
518    ## Check for category in dc:subject. We will save it later if
519    ## it's present, but we want to give an error now if necessary.
520    my($cat);
521    if (my $label = $atom->get(NS_DC, 'subject')) {
522        my $label_enc = encode_text($label,'utf-8',$enc);
523        $cat = MT::Category->load({ blog_id => $blog->id, label => $label_enc })
524            or return $app->error(400, "Invalid category '$label'");
525    }
526
527    my $content = $atom->content;
528    my $type = $content->type; 
529    my $body = encode_text(MT::I18N::utf8_off($content->body),'utf-8',$enc); 
530    my $asset;
531    if ($type && $type !~ m!^application/.*xml$!) {
532        if ($type !~ m!^text/!) {
533            $asset = $app->_upload_to_asset or return;
534        }
535        elsif ($type && $type eq 'text/plain') {
536            ## Check for LifeBlog Note & SMS records.
537            my $format = $atom->get(NS_DC, 'format');
538            if ($format && ($format eq 'Note' || $format eq 'SMS')) {
539                $asset = $app->_upload_to_asset or return;
540            }
541        }
542    }
543    if ( $atom->get(NS_TYPEPAD, 'standalone') && $asset ) {
544        $app->response_code(201);
545        $app->response_content_type('application/atom_xml');
546        my $a = MT::Atom::Entry->new_with_asset($asset);
547        return $a->as_xml; 
548    } 
549
550    my $entry = MT::Entry->new;
551    my $orig_entry = $entry->clone;
552    $entry->blog_id($blog->id);
553    $entry->author_id($user->id);
554    $entry->created_by($user->id);
555    $entry->status($perms->can_publish_post ? MT::Entry::RELEASE() : MT::Entry::HOLD() );
556    $entry->allow_comments($blog->allow_comments_default);
557    $entry->allow_pings($blog->allow_pings_default);
558    $entry->convert_breaks($blog->convert_paras);
559    $entry->title(encode_text($atom->title,'utf-8',$enc));
560    $entry->text(encode_text(MT::I18N::utf8_off($atom->content()->body()),'utf-8',$enc));
561    $entry->excerpt(encode_text($atom->summary,'utf-8',$enc));
562    if (my $iso = $atom->issued) {
563        my $pub_ts = MT::Util::iso2ts($blog, $iso);
564        $entry->authored_on($pub_ts);
565        if ( 0 < MT::DateTime->compare( blog => $blog,
566                a => $pub_ts,
567                b => { value => time(), type => 'epoch' } )
568           )
569        {
570            $entry->status(MT::Entry::FUTURE())
571        }
572    }
573## xxx mt/typepad-specific fields
574    $app->apply_basename($entry, $atom);
575    $entry->discover_tb_from_entry();
576
577    if (my @link = $atom->link) {
578        my $i = 0;
579        my $img_html = '';
580        my $num_links = scalar @link;
581        for my $link (@link) {
582            next unless $link->rel eq 'related';
583            my($asset_id) = $link->href =~ /asset\-(\d+)$/;
584            if ($asset_id) {
585                require MT::Asset;
586                my $a = MT::Asset->load($asset_id);
587                next unless $a;
588                my $pkg = MT::Asset->handler_for_file($a->file_name);
589                my $asset = bless $a, $pkg;
590                $img_html .= $asset->as_html({ include => 1 });
591            }
592        }
593        if ($img_html) {
594            $img_html .= qq{<br style="clear: left;" />\n\n};
595            $entry->text($img_html . $body);
596        }
597    }
598
599    MT->run_callbacks('api_pre_save.entry', $app, $entry, $orig_entry)
600        or return $app->error(500, MT->translate("PreSave failed [_1]", MT->errstr));
601
602    $entry->save or return $app->error(500, $entry->errstr);
603
604    require MT::Log;
605    $app->log({
606        message => $app->translate("User '[_1]' (user #[_2]) added [lc,_4] #[_3]", $user->name, $user->id, $entry->id, $entry->class_label),
607        level => MT::Log::INFO(),
608        class => 'entry',
609        category => 'new',
610        metadata => $entry->id
611    });
612    ## Save category, if present.
613    if ($cat) {
614        my $place = MT::Placement->new;
615        $place->is_primary(1);
616        $place->entry_id($entry->id);
617        $place->blog_id($blog->id);
618        $place->category_id($cat->id);
619        $place->save or return $app->error(500, $place->errstr);
620    }
621
622    MT->run_callbacks('api_post_save.entry', $app, $entry, $orig_entry);
623
624    $app->publish($entry);
625    $app->response_code(201);
626    $app->response_content_type('application/atom+xml');
627    my $edit_uri = $app->base . $app->uri . '/blog_id=' . $entry->blog_id . '/entry_id=' . $entry->id;
628    $app->set_header('Location', $edit_uri);
629    $atom = $app->new_with_entry($entry);
630    $atom->add_link({ rel => $app->edit_link_rel,
631                      href => $edit_uri,
632                      type => 'application/atom+xml',  # even in Legacy
633                      title => $entry->title });
634    $atom->as_xml;
635}
636
637sub edit_post {
638    my $app = shift;
639    my $atom = $app->atom_body or return;
640    my $blog = $app->{blog};
641    my $enc = $app->config('PublishCharset');
642    my $entry_id = $app->{param}{entry_id}
643        or return $app->error(400, "No entry_id");
644    my $entry = MT::Entry->load($entry_id)
645        or return $app->error(400, "Invalid entry_id");
646    return $app->error(403, "Access denied")
647        unless $app->{perms}->can_edit_entry($entry, $app->{user});
648    my $orig_entry = $entry->clone;
649    $entry->title(encode_text($atom->title,'utf-8',$enc));
650    $entry->text(encode_text(MT::I18N::utf8_off($atom->content()->body()),'utf-8',$enc));
651    $entry->excerpt(encode_text($atom->summary,'utf-8',$enc));
652    $entry->modified_by($app->{user}->id);
653    if (my $iso = $atom->issued) {
654        my $pub_ts = MT::Util::iso2ts($blog, $iso);
655        $entry->authored_on($pub_ts);
656        if ( 0 < MT::DateTime->compare( blog => $blog,
657                a => $pub_ts,
658                b => { value => time(), type => 'epoch' } )
659           )
660        {
661            $entry->status(MT::Entry::FUTURE())
662        }
663    }
664## xxx mt/typepad-specific fields
665    $app->apply_basename($entry, $atom);
666    $entry->discover_tb_from_entry();
667
668    MT->run_callbacks('api_pre_save.entry', $app, $entry, $orig_entry)
669        or return $app->error(500, MT->translate("PreSave failed [_1]", MT->errstr));
670
671    $entry->save or return $app->error(500, "Entry not saved");
672
673    require MT::Log;
674    $app->log({
675        message => $app->translate("User '[_1]' (user #[_2]) edited [lc,_4] #[_3]", $app->{user}->name, $app->{user}->id, $entry->id, $entry->class_label),
676        level => MT::Log::INFO(),
677        class => 'entry',
678        category => 'new',
679        metadata => $entry->id
680    });
681
682    MT->run_callbacks('api_post_save.entry', $app, $entry, $orig_entry);
683
684    if ($entry->status == MT::Entry::RELEASE()) {
685        $app->publish($entry) or return $app->error(500, "Entry not published");
686    }
687    $app->response_code(200);
688    $app->response_content_type($app->atom_content_type);
689    $atom = $app->new_with_entry($entry);
690    $atom->as_xml;
691}
692
693sub get_posts {
694    my $app = shift;
695    my $blog = $app->{blog};
696    my %terms = (blog_id => $blog->id);
697    my %arg = (sort => $app->get_posts_order_field, direction => 'descend');
698    my $Limit = 20;
699    $arg{limit} = $Limit + 1;
700    $arg{offset} = $app->{param}{offset} || 0;
701    my $iter = MT::Entry->load_iter(\%terms, \%arg);
702    my $feed = $app->new_feed();
703    my $uri = $app->base . $app->uri . '/blog_id=' . $blog->id;
704    my $blogname = encode_text($blog->name, undef, 'utf-8');
705    $feed->add_link({ rel => 'alternate', type => 'text/html',
706                      href => $blog->site_url });
707    $feed->title($blogname);
708    $feed->add_link({ rel => 'service.post', type => 'application/x.atom+xml',
709                      href => $uri, title => $blogname });
710    require URI;
711    my $site_uri = URI->new($blog->site_url);
712    if ( $site_uri ) {
713        my $blog_created = format_ts('%Y-%m-%d', $blog->created_on, $blog, 'en', 0);
714        my $id = 'tag:'.$site_uri->host.','.$blog_created.':'.$site_uri->path.'/'.$blog->id;
715        $feed->id($id);
716    }
717    my $latest_date = 0;
718    $uri .= '/entry_id=';
719    my @entries;
720    while (my $entry = $iter->()) {
721        my $e = $app->new_with_entry($entry);
722        $e->add_link({ rel => $app->edit_link_rel, type => $app->atom_x_content_type,
723                       href => ($uri . $entry->id), title => encode_text($entry->title, undef,'utf-8') });
724        # feed/updated should be added before entries
725        # so we postpone adding them until later
726        push @entries, $e;
727        my $date = $entry->modified_on || $entry->authored_on;
728        if ( $latest_date < $date ) {
729            $latest_date = $date;
730            $feed->updated( $e->updated );
731        }
732    }
733    $feed->add_entry($_) foreach @entries;
734    ## xxx add next/prev links
735    $app->response_content_type($app->atom_content_type);
736    $feed->as_xml;
737}
738
739sub get_post {
740    my $app = shift;
741    my $blog = $app->{blog};
742    my $entry_id = $app->{param}{entry_id}
743        or return $app->error(400, "No entry_id");
744    my $entry = MT::Entry->load($entry_id)
745        or return $app->error(400, "Invalid entry_id");
746    return $app->error(403, "Access denied")
747        unless $app->{perms}->can_edit_entry($entry, $app->{user});
748    $app->response_content_type($app->atom_content_type);
749    my $atom = $app->new_with_entry($entry);
750    my $uri = $app->base . $app->uri . '/blog_id=' . $blog->id;
751    $uri .= '/entry_id=';
752    $atom->add_link({ rel => $app->edit_link_rel, type => $app->atom_x_content_type,
753                      href => ($uri . $entry->id), title => encode_text($entry->title, undef,'utf-8') });
754    $atom->as_xml;
755}
756
757sub delete_post {
758    my $app = shift;
759    my $blog = $app->{blog};
760    my $entry_id = $app->{param}{entry_id}
761        or return $app->error(400, "No entry_id");
762    my $entry = MT::Entry->load($entry_id)
763        or return $app->error(400, "Invalid entry_id");
764    return $app->error(403, "Access denied")
765        unless $app->{perms}->can_edit_entry($entry, $app->{user});
766    $entry->remove
767        or return $app->error(500, $entry->errstr);
768    $app->publish($entry, 1) or return $app->error(500, $app->errstr);
769    '';
770}
771
772sub _upload_to_asset {
773    my $app = shift;
774    my $atom = $app->atom_body or return;
775    my $blog = $app->{blog};
776    my $user = $app->{user};
777    my %MIME2EXT = (
778        'text/plain'         => '.txt',
779        'image/jpeg'         => '.jpg',
780        'video/3gpp'         => '.3gp',
781        'application/x-mpeg' => '.mpg',
782        'video/mp4'          => '.mp4',
783        'video/quicktime'    => '.mov',
784        'audio/mpeg'         => '.mp3',
785        'audio/x-wav'        => '.wav',
786        'audio/ogg'          => '.ogg',
787        'audio/ogg-vorbis'   => '.ogg',
788    );
789
790    return $app->error(403, "Access denied") unless $app->{perms}->can_upload;
791    my $content = $atom->content;
792    my $type = $content->type
793        or return $app->error(400, "content \@type is required");
794    my $fname = $atom->title or return $app->error(400, "title is required");
795    $fname = basename($fname);
796    return $app->error(400, "Invalid or empty filename")
797        if $fname =~ m!/|\.\.|\0|\|!;
798
799    my $local_relative = File::Spec->catfile('%r', $fname);
800    my $local = File::Spec->catfile($blog->site_path, $fname);
801    my $fmgr = $blog->file_mgr;
802    my($base, $path, $ext) = File::Basename::fileparse($local, '\.[^\.]*');
803    $ext = $MIME2EXT{$type} unless $ext;
804    my $base_copy = $base;
805    my $ext_copy = $ext;
806    $ext_copy =~ s/\.//;
807    my $i = 1;
808    while ($fmgr->exists($path . $base . $ext)) {
809        $base = $base_copy . '_' . $i++;
810    }
811    $local = $path . $base . $ext;
812    my $data = $content->body;
813    defined(my $bytes = $fmgr->put_data($data, $local, 'upload'))
814        or return $app->error(500, "Error writing uploaded file");
815
816    eval { require Image::Size; };
817    return $app->error(500, MT->translate("Perl module Image::Size is required to determine width and height of uploaded images.")) if $@;
818    my ( $w, $h, $id ) = Image::Size::imgsize($local);
819
820    require MT::Asset;
821    my $asset_pkg = MT::Asset->handler_for_file($local);
822    my $is_image  = defined($w)
823      && defined($h)
824      && $asset_pkg->isa('MT::Asset::Image');
825    my $asset;
826    if (!($asset = $asset_pkg->load(
827                { file_path => $local, blog_id => $blog->id })))
828    {
829        $asset = $asset_pkg->new();
830        $asset->file_path($local_relative);
831        $asset->file_name($base.$ext);
832        $asset->file_ext($ext_copy);
833        $asset->blog_id($blog->id);
834        $asset->created_by( $user->id );
835    }
836    else {
837        $asset->modified_by( $user->id );
838    }
839    my $original = $asset->clone;
840    my $url = '%r/' . $base . $ext;
841    $asset->url($url);
842    if ($is_image) {
843        $asset->image_width($w);
844        $asset->image_height($h);
845    }
846    $asset->mime_type($type);
847    $asset->save;
848
849    MT->run_callbacks(
850        'api_upload_file.' . $asset->class,
851        File => $local, file => $local,
852        Url => $url, url => $url,
853        Size => $bytes, size => $bytes,
854        Asset => $asset, asset => $asset,
855        Type => $asset->class, type => $asset->class,
856        Blog => $blog, blog => $blog);
857    if ($is_image) {
858        MT->run_callbacks(
859            'api_upload_image',
860            File => $local, file => $local,
861            Url => $url, url => $url,
862            Size => $bytes, size => $bytes,
863            Asset => $asset, asset => $asset,
864            Height => $h, height => $h,
865            Width => $w, width => $w,
866            Type => 'image', type => 'image',
867            ImageType => $id, image_type => $id,
868            Blog => $blog, blog => $blog);
869    }
870
871    $asset;
872}
873
874sub handle_upload {
875    my $app = shift;
876    my $blog = $app->{blog};
877   
878    my $asset = $app->_upload_to_asset or return;
879
880    my $link = XML::Atom::Link->new;
881    $link->type($asset->mime_type);
882    $link->rel('alternate');
883    $link->href($asset->url);
884    my $atom = XML::Atom::Entry->new;
885    $atom->title($asset->file_name);
886    $atom->add_link($link);
887    $app->response_code(201);
888    $app->response_content_type('application/x.atom+xml');
889    $atom->as_xml;
890}
891
892package MT::AtomServer::Weblog::Legacy;
893use strict;
894
895use base qw( MT::AtomServer::Weblog );
896
897use MT::I18N qw( encode_text );
898use XML::Atom;  # for LIBXML
899use XML::Atom::Feed;
900use base qw( MT::AtomServer );
901use MT::Blog;
902use MT::Permission;
903
904use constant NS_CATEGORY => 'http://sixapart.com/atom/category#';
905use constant NS_DC => MT::AtomServer::Weblog->NS_DC();
906
907sub script { $_[0]->{cfg}->AtomScript . '/weblog' }
908
909sub atom_content_type   { 'application/xml' }
910sub atom_x_content_type { 'application/x.atom+xml' }
911
912sub edit_link_rel { 'service.edit' }
913sub get_posts_order_field { 'authored_on' }
914
915sub new_feed {
916    my $app = shift;
917    XML::Atom::Feed->new();
918}
919
920sub new_with_entry {
921    my $app = shift;
922    my ($entry) = @_;
923    MT::Atom::Entry->new_with_entry($entry);
924}
925
926sub apply_basename {}
927
928sub get_weblogs {
929    my $app = shift;
930    my $user = $app->{user};
931    my $iter = $user->is_superuser
932        ? MT::Blog->load_iter()
933        : MT::Permission->load_iter({ author_id => $user->id });
934    my $feed = $app->new_feed();
935    my $base = $app->base . $app->uri;
936    require URI;
937    my $uri = URI->new($base);
938    if ( $uri ) {
939        my $created = MT::Util::format_ts('%Y-%m-%d', $user->created_on, undef, 'en', 0);
940        my $id = 'tag:'.$uri->host.','.$created.':'.$uri->path.'/weblogs-'.$user->id;
941        $feed->id($id);
942    }
943    while (my $thing = $iter->()) {
944        if ($thing->isa('MT::Permission')) {
945            next unless $thing->can_create_post;
946        }
947        my $blog = $thing->isa('MT::Blog') ? $thing
948            : MT::Blog->load($thing->blog_id);
949        next unless $blog;
950        my $uri = $base . '/blog_id=' . $blog->id;
951        my $blogname = encode_text($blog->name . ' #' . $blog->id, undef, 'utf-8');
952        $feed->add_link({ rel => 'service.post', title => $blogname,
953                          href => $uri, type => 'application/x.atom+xml' });
954        $feed->add_link({ rel => 'service.feed', title => $blogname,
955                          href => $uri, type => 'application/x.atom+xml' });
956        $feed->add_link({ rel => 'service.upload', title => $blogname,
957                          href => $uri . '/svc=upload',
958                          type => 'application/x.atom+xml' });
959        $feed->add_link({ rel => 'service.categories', title => $blogname,
960                          href => $uri . '/svc=categories',
961                          type => 'application/x.atom+xml' });
962        $feed->add_link({ rel => 'alternate', title => $blogname,
963                          href => $blog->site_url,
964                          type => 'text/html' });
965    }
966    $app->response_code(200);
967    $app->response_content_type('application/x.atom+xml');
968    $feed->as_xml;
969}
970
971sub get_categories {
972    my $app = shift;
973    my $blog = $app->{blog};
974    my $iter = MT::Category->load_iter({ blog_id => $blog->id });
975    my $doc;
976    if (LIBXML) {
977        $doc = XML::LibXML::Document->createDocument('1.0', 'utf-8');
978        my $root = $doc->createElementNS(NS_CATEGORY, 'categories');
979        $doc->setDocumentElement($root);
980    } else {
981        $doc = XML::XPath::Node::Element->new('categories');
982        my $ns = XML::XPath::Node::Namespace->new('#default' => NS_CATEGORY);
983        $doc->appendNamespace($ns);
984    }
985    while (my $cat = $iter->()) {
986        my $catlabel = encode_text($cat->label, undef, 'utf-8');
987        if (LIBXML) {
988            my $elem = $doc->createElementNS(NS_DC, 'subject');
989            $doc->getDocumentElement->appendChild($elem);
990            $elem->appendChild(XML::LibXML::Text->new($catlabel));
991        } else {
992            my $elem = XML::XPath::Node::Element->new('subject');
993            my $ns = XML::XPath::Node::Namespace->new('#default' => NS_DC);
994            $elem->appendNamespace($ns);
995            $doc->appendChild($elem);
996            $elem->appendChild(XML::XPath::Node::Text->new($catlabel));
997        }
998    }
999    $app->response_code(200);
1000    $app->response_content_type('application/x.atom+xml');
1001    if (LIBXML) {
1002        $doc->toString(1);
1003    } else {
1004        return '<?xml version="1.0" encoding="utf-8"?>' . "\n" . $doc->toString;
1005    }
1006}
1007
10081;
1009__END__
1010
1011=head1 NAME
1012
1013MT::AtomServer
1014
1015=head1 SYNOPSIS
1016
1017An Atom Publishing API interface for communicating with Movable Type.
1018
1019=head1 METHODS
1020
1021=head2 $app->xml_body()
1022
1023Takes the content posted to the server and parses it into an XML document.
1024Uses either XML::LibXML or XML::XPath depending on which is available.
1025
1026=head2 $app->iso2epoch($iso_ts)
1027
1028Converts C<$iso_ts> in the format of an ISO timestamp into a unix timestamp
1029(seconds since the epoch).
1030
1031=head2 $app->init
1032
1033Initializes the application.
1034
1035=head2 $app->get_auth_info
1036
1037Processes the request for WSSE authentication and returns a hash containing:
1038
1039=over 4
1040
1041=item * Username
1042
1043=item * PasswordDigest
1044
1045=item * Nonce
1046
1047=item * Created
1048
1049=back
1050
1051=head2 $app->handle_request
1052
1053The implementation of this in I<MT::AtomServer::Weblog> passes the request
1054to the proper method.
1055
1056=head2 $app->handle
1057
1058Wrapper method that determines the proper AtomServer package to pass the
1059request to.
1060
1061=head2 $app->iso2ts($iso_ts, $target_zone)
1062
1063Converts C<$iso_ts> in the format of an ISO timestamp into a MT-compatible
1064timestamp (YYYYMMDDHHMMSS) for the specified timezone C<$target_zone>.
1065
1066=head2 $app->atom_body
1067
1068Processes the request as Atom content and returns an XML::Atom object.
1069
1070=head2 $app->error($code, $message)
1071
1072Sends the HTTP headers necessary to relay an error.
1073
1074=head2 $app->authenticate()
1075
1076Checks the WSSE authentication with the local MT user database and
1077confirms the user is authorized to access the resources required by
1078the request.
1079
1080=head2 $app->show_error($message)
1081
1082Returns an XML wrapper for the error response.
1083
1084=head2 $app->auth_failure($code, $message)
1085
1086Handles the response in the event of an authentication failure.
1087
1088=head1 CALLBACKS
1089
1090=over 4
1091
1092=item api_pre_save.entry
1093
1094    callback($eh, $app, $entry, $original_entry)
1095
1096Called before saving a new or existing entry. If saving a new entry, the
1097$original_entry will have an unassigned 'id'. This callback is executed
1098as a filter, so your handler must return 1 to allow the entry to be saved.
1099
1100=item api_post_save.entry
1101
1102    callback($eh, $app, $entry, $original_entry)
1103
1104Called after saving a new or existing entry. If saving a new entry, the
1105$original_entry will have an unassigned 'id'.
1106
1107=back
1108
1109=cut
Note: See TracBrowser for help on using the browser.