root/branches/release-39/lib/MT/AtomServer.pm @ 2434

Revision 2434, 43.8 kB (checked in by fumiakiy, 18 months ago)

Applied patch from Hiroshi Sakai which enables "limit" parameter in AtomServer. BugId:79858

  • 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->exist({
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    my $enc = $app->config->PublishCharset;
433
434    # TODO: libxml support? XPath should always be available...
435    require XML::XPath;
436    require XML::XPath::Node::Element;
437    require XML::XPath::Node::Namespace;
438    require XML::XPath::Node::Text;
439
440    my $doc = XML::XPath::Node::Element->new('service');
441    my $app_ns = XML::XPath::Node::Namespace->new('#default' => NS_APP());
442    $doc->appendNamespace($app_ns);
443    my $atom_ns = XML::XPath::Node::Namespace->new('atom' => 'http://www.w3.org/2005/Atom');
444    $doc->appendNamespace($atom_ns);
445
446    while (my $thing = $iter->()) {
447        # TODO: provide media collection if author can upload to this blog.
448        if ($thing->isa('MT::Permission')) {
449            next if !$thing->can_create_post;
450        }
451
452        my $blog = $thing->isa('MT::Blog') ? $thing
453            : MT::Blog->load($thing->blog_id);
454        next unless $blog;
455        my $uri = $base . '/blog_id=' . $blog->id;
456
457        my $workspace = XML::XPath::Node::Element->new('workspace');
458        $doc->appendChild($workspace);
459
460        my $title = XML::XPath::Node::Element->new('atom:title', 'atom');
461        my $blogname = encode_text($blog->name, $enc, 'utf-8');
462        $title->appendChild(XML::XPath::Node::Text->new($blogname));
463        $workspace->appendChild($title);
464
465        my $entries = XML::XPath::Node::Element->new('collection');
466        $entries->appendAttribute(XML::XPath::Node::Attribute->new('href', $uri));
467        $workspace->appendChild($entries);
468
469        my $e_title = XML::XPath::Node::Element->new('atom:title', 'atom');
470        my $feed_title = encode_text(MT->translate('[_1]: Entries', $blog->name), $enc, 'utf-8');
471        $e_title->appendChild(XML::XPath::Node::Text->new($feed_title));
472        $entries->appendChild($e_title);
473
474        my $cats = XML::XPath::Node::Element->new('categories');
475        $cats->appendAttribute(XML::XPath::Node::Attribute->new('href', $uri . '/svc=categories'));
476        $entries->appendChild($cats);
477    }
478    $app->response_code(200);
479    $app->response_content_type('application/atomsvc+xml');
480    '<?xml version="1.0" encoding="utf-8"?>' . "\n" .                                                         
481        $doc->toString;
482}
483
484sub get_categories {
485    my $app = shift;
486    my $blog = $app->{blog};
487
488    # TODO: libxml support? XPath should always be available...
489    require XML::XPath;
490    require XML::XPath::Node::Element;
491    require XML::XPath::Node::Namespace;
492    require XML::XPath::Node::Text;
493
494    my $doc = XML::XPath::Node::Element->new('categories');
495    my $app_ns = XML::XPath::Node::Namespace->new('#default' => NS_APP());
496    $doc->appendNamespace($app_ns);
497    my $atom_ns = XML::XPath::Node::Namespace->new('atom' => 'http://www.w3.org/2005/Atom');
498    $doc->appendNamespace($atom_ns);
499    $doc->appendAttribute(XML::XPath::Node::Attribute->new('fixed', 'yes'));
500
501    my $iter = MT::Category->load_iter({ blog_id => $blog->id });
502    while (my $cat = $iter->()) {
503        my $cat_node = XML::XPath::Node::Element->new('atom:category', 'atom');
504        $cat_node->appendAttribute(XML::XPath::Node::Attribute->new('term', $cat->label));
505        $doc->appendChild($cat_node);
506    }
507
508    $app->response_code(200);
509    $app->response_content_type('application/atomcat+xml');
510    '<?xml version="1.0" encoding="utf-8"?>' . "\n" .                                                         
511        $doc->toString;
512}
513
514sub new_post {
515    my $app = shift;
516    my $atom = $app->atom_body or return $app->error(500, "No body!");
517    my $blog = $app->{blog};
518    my $user = $app->{user};
519    my $perms = $app->{perms};
520    my $enc = $app->config('PublishCharset');
521    ## Check for category in dc:subject. We will save it later if
522    ## it's present, but we want to give an error now if necessary.
523    my($cat);
524    if (my $label = $atom->get(NS_DC, 'subject')) {
525        my $label_enc = encode_text($label,'utf-8',$enc);
526        $cat = MT::Category->load({ blog_id => $blog->id, label => $label_enc })
527            or return $app->error(400, "Invalid category '$label'");
528    }
529
530    my $content = $atom->content;
531    my $type = $content->type; 
532    my $body = encode_text(MT::I18N::utf8_off($content->body),'utf-8',$enc); 
533    my $asset;
534    if ($type && $type !~ m!^application/.*xml$!) {
535        if ($type !~ m!^text/!) {
536            $asset = $app->_upload_to_asset or return;
537        }
538        elsif ($type && $type eq 'text/plain') {
539            ## Check for LifeBlog Note & SMS records.
540            my $format = $atom->get(NS_DC, 'format');
541            if ($format && ($format eq 'Note' || $format eq 'SMS')) {
542                $asset = $app->_upload_to_asset or return;
543            }
544        }
545    }
546    if ( $atom->get(NS_TYPEPAD, 'standalone') && $asset ) {
547        $app->response_code(201);
548        $app->response_content_type('application/atom_xml');
549        my $a = MT::Atom::Entry->new_with_asset($asset);
550        return $a->as_xml; 
551    } 
552
553    my $entry = MT::Entry->new;
554    my $orig_entry = $entry->clone;
555    $entry->blog_id($blog->id);
556    $entry->author_id($user->id);
557    $entry->created_by($user->id);
558    $entry->status($perms->can_publish_post ? MT::Entry::RELEASE() : MT::Entry::HOLD() );
559    $entry->allow_comments($blog->allow_comments_default);
560    $entry->allow_pings($blog->allow_pings_default);
561    $entry->convert_breaks($blog->convert_paras);
562    $entry->title(encode_text($atom->title,'utf-8',$enc));
563    $entry->text(encode_text(MT::I18N::utf8_off($atom->content()->body()),'utf-8',$enc));
564    $entry->excerpt(encode_text($atom->summary,'utf-8',$enc));
565    if (my $iso = $atom->issued) {
566        my $pub_ts = MT::Util::iso2ts($blog, $iso);
567        $entry->authored_on($pub_ts);
568        require MT::DateTime;
569        if ( 0 < MT::DateTime->compare( blog => $blog,
570                a => $pub_ts,
571                b => { value => time(), type => 'epoch' } )
572           )
573        {
574            $entry->status(MT::Entry::FUTURE())
575        }
576    }
577## xxx mt/typepad-specific fields
578    $app->apply_basename($entry, $atom);
579    $entry->discover_tb_from_entry();
580
581    if (my @link = $atom->link) {
582        my $i = 0;
583        my $img_html = '';
584        my $num_links = scalar @link;
585        for my $link (@link) {
586            next unless $link->rel eq 'related';
587            my($asset_id) = $link->href =~ /asset\-(\d+)$/;
588            if ($asset_id) {
589                require MT::Asset;
590                my $a = MT::Asset->load($asset_id);
591                next unless $a;
592                my $pkg = MT::Asset->handler_for_file($a->file_name);
593                my $asset = bless $a, $pkg;
594                $img_html .= $asset->as_html({ include => 1 });
595            }
596        }
597        if ($img_html) {
598            $img_html .= qq{<br style="clear: left;" />\n\n};
599            $entry->text($img_html . $body);
600        }
601    }
602
603    MT->run_callbacks('api_pre_save.entry', $app, $entry, $orig_entry)
604        or return $app->error(500, MT->translate("PreSave failed [_1]", MT->errstr));
605
606    $entry->save or return $app->error(500, $entry->errstr);
607
608    require MT::Log;
609    $app->log({
610        message => $app->translate("User '[_1]' (user #[_2]) added [lc,_4] #[_3]", $user->name, $user->id, $entry->id, $entry->class_label),
611        level => MT::Log::INFO(),
612        class => 'entry',
613        category => 'new',
614        metadata => $entry->id
615    });
616    ## Save category, if present.
617    if ($cat) {
618        my $place = MT::Placement->new;
619        $place->is_primary(1);
620        $place->entry_id($entry->id);
621        $place->blog_id($blog->id);
622        $place->category_id($cat->id);
623        $place->save or return $app->error(500, $place->errstr);
624    }
625
626    MT->run_callbacks('api_post_save.entry', $app, $entry, $orig_entry);
627
628    $app->publish($entry);
629    $app->response_code(201);
630    $app->response_content_type('application/atom+xml');
631    my $edit_uri = $app->base . $app->uri . '/blog_id=' . $entry->blog_id . '/entry_id=' . $entry->id;
632    $app->set_header('Location', $edit_uri);
633    $atom = $app->new_with_entry($entry);
634    $atom->add_link({ rel => $app->edit_link_rel,
635                      href => $edit_uri,
636                      type => 'application/atom+xml',  # even in Legacy
637                      title => $entry->title });
638    $atom->as_xml;
639}
640
641sub edit_post {
642    my $app = shift;
643    my $atom = $app->atom_body or return;
644    my $blog = $app->{blog};
645    my $enc = $app->config('PublishCharset');
646    my $entry_id = $app->{param}{entry_id}
647        or return $app->error(400, "No entry_id");
648    my $entry = MT::Entry->load($entry_id)
649        or return $app->error(400, "Invalid entry_id");
650    return $app->error(403, "Access denied")
651        unless $app->{perms}->can_edit_entry($entry, $app->{user});
652    my $orig_entry = $entry->clone;
653    $entry->title(encode_text($atom->title,'utf-8',$enc));
654    $entry->text(encode_text(MT::I18N::utf8_off($atom->content()->body()),'utf-8',$enc));
655    $entry->excerpt(encode_text($atom->summary,'utf-8',$enc));
656    $entry->modified_by($app->{user}->id);
657    if (my $iso = $atom->issued) {
658        my $pub_ts = MT::Util::iso2ts($blog, $iso);
659        $entry->authored_on($pub_ts);
660        require MT::DateTime;
661        if ( 0 < MT::DateTime->compare( blog => $blog,
662                a => $pub_ts,
663                b => { value => time(), type => 'epoch' } )
664           )
665        {
666            $entry->status(MT::Entry::FUTURE())
667        }
668    }
669## xxx mt/typepad-specific fields
670    $app->apply_basename($entry, $atom);
671    $entry->discover_tb_from_entry();
672
673    MT->run_callbacks('api_pre_save.entry', $app, $entry, $orig_entry)
674        or return $app->error(500, MT->translate("PreSave failed [_1]", MT->errstr));
675
676    $entry->save or return $app->error(500, "Entry not saved");
677
678    require MT::Log;
679    $app->log({
680        message => $app->translate("User '[_1]' (user #[_2]) edited [lc,_4] #[_3]", $app->{user}->name, $app->{user}->id, $entry->id, $entry->class_label),
681        level => MT::Log::INFO(),
682        class => 'entry',
683        category => 'new',
684        metadata => $entry->id
685    });
686
687    MT->run_callbacks('api_post_save.entry', $app, $entry, $orig_entry);
688
689    if ($entry->status == MT::Entry::RELEASE()) {
690        $app->publish($entry) or return $app->error(500, "Entry not published");
691    }
692    $app->response_code(200);
693    $app->response_content_type($app->atom_content_type);
694    $atom = $app->new_with_entry($entry);
695    $atom->as_xml;
696}
697
698sub get_posts {
699    my $app = shift;
700    my $blog = $app->{blog};
701    my %terms = (blog_id => $blog->id);
702    my %arg = (sort => $app->get_posts_order_field, direction => 'descend');
703    $arg{limit}  = $app->{param}{limit}  || 21;
704    $arg{offset} = $app->{param}{offset} || 0;
705    my $iter = MT::Entry->load_iter(\%terms, \%arg);
706    my $feed = $app->new_feed();
707    my $uri = $app->base . $app->uri . '/blog_id=' . $blog->id;
708    my $blogname = encode_text($blog->name, undef, 'utf-8');
709    $feed->add_link({ rel => 'alternate', type => 'text/html',
710                      href => $blog->site_url });
711    $feed->add_link({ rel => 'self', type => $app->atom_x_content_type,
712                      href => $uri });
713    $feed->title($blogname);
714    # FIXME: move the line to the Legacy class
715    if ( !$feed->version || ( $feed->version < 1.0 ) ) {
716        $feed->add_link({ rel => 'service.post', type => $app->atom_x_content_type,
717                          href => $uri, title => $blogname });
718    }
719    require URI;
720    my $site_uri = URI->new($blog->site_url);
721    if ( $site_uri ) {
722        my $blog_created = format_ts('%Y-%m-%d', $blog->created_on, $blog, 'en', 0);
723        my $id = 'tag:'.$site_uri->host.','.$blog_created.':'.$site_uri->path.'/'.$blog->id;
724        $feed->id($id);
725    }
726    my $latest_date = 0;
727    $uri .= '/entry_id=';
728    my @entries;
729    while (my $entry = $iter->()) {
730        my $e = $app->new_with_entry($entry);
731        $e->add_link({ rel => $app->edit_link_rel, type => $app->atom_x_content_type,
732                       href => ($uri . $entry->id), title => encode_text($entry->title, undef,'utf-8') });
733        $e->add_link({ rel => 'replies', type => $app->atom_x_content_type,
734                href => $app->base . $app->app_path . $app->config->AtomScript . '/comments/blog_id=' . $blog->id . '/entry_id=' . $entry->id });
735
736        # feed/updated should be added before entries
737        # so we postpone adding them until later
738        push @entries, $e;
739        my $date = $entry->modified_on || $entry->authored_on;
740        if ( $latest_date < $date ) {
741            $latest_date = $date;
742            $feed->updated( $e->updated );
743        }
744    }
745    $feed->add_entry($_) foreach @entries;
746    ## xxx add next/prev links
747    $app->run_callbacks( 'get_posts', $feed, $blog );
748    $app->response_content_type($app->atom_content_type);
749    $feed->as_xml;
750}
751
752sub get_post {
753    my $app = shift;
754    my $blog = $app->{blog};
755    my $entry_id = $app->{param}{entry_id}
756        or return $app->error(400, "No entry_id");
757    my $entry = MT::Entry->load($entry_id)
758        or return $app->error(400, "Invalid entry_id");
759    return $app->error(403, "Access denied")
760        unless $app->{perms}->can_edit_entry($entry, $app->{user});
761    $app->response_content_type($app->atom_content_type);
762    my $atom = $app->new_with_entry($entry);
763    my $uri = $app->base . $app->uri . '/blog_id=' . $blog->id;
764    $uri .= '/entry_id=';
765    $atom->add_link({ rel => $app->edit_link_rel, type => $app->atom_x_content_type,
766        href => ($uri . $entry->id), title => encode_text($entry->title, undef,'utf-8') });
767    $atom->add_link({ rel => 'replies', type => $app->atom_x_content_type,
768        href => $app->base
769            . $app->app_path
770            . $app->config->AtomScript
771            . '/comments/blog_id=' . $blog->id
772            . '/entry_id=' . $entry->id
773    });
774    $app->run_callbacks( 'get_post', $atom, $entry );
775    $app->response_content_type($app->atom_content_type);
776    $atom->as_xml;
777}
778
779sub delete_post {
780    my $app = shift;
781    my $blog = $app->{blog};
782    my $entry_id = $app->{param}{entry_id}
783        or return $app->error(400, "No entry_id");
784    my $entry = MT::Entry->load($entry_id)
785        or return $app->error(400, "Invalid entry_id");
786    return $app->error(403, "Access denied")
787        unless $app->{perms}->can_edit_entry($entry, $app->{user});
788    $entry->remove
789        or return $app->error(500, $entry->errstr);
790    $app->publish($entry, 1) or return $app->error(500, $app->errstr);
791    '';
792}
793
794sub _upload_to_asset {
795    my $app = shift;
796    my $atom = $app->atom_body or return;
797    my $blog = $app->{blog};
798    my $user = $app->{user};
799    my %MIME2EXT = (
800        'text/plain'         => '.txt',
801        'image/jpeg'         => '.jpg',
802        'video/3gpp'         => '.3gp',
803        'application/x-mpeg' => '.mpg',
804        'video/mp4'          => '.mp4',
805        'video/quicktime'    => '.mov',
806        'audio/mpeg'         => '.mp3',
807        'audio/x-wav'        => '.wav',
808        'audio/ogg'          => '.ogg',
809        'audio/ogg-vorbis'   => '.ogg',
810    );
811
812    return $app->error(403, "Access denied") unless $app->{perms}->can_upload;
813    my $content = $atom->content;
814    my $type = $content->type
815        or return $app->error(400, "content \@type is required");
816    my $fname = $atom->title or return $app->error(400, "title is required");
817    $fname = basename($fname);
818    return $app->error(400, "Invalid or empty filename")
819        if $fname =~ m!/|\.\.|\0|\|!;
820
821    my $local_relative = File::Spec->catfile('%r', $fname);
822    my $local = File::Spec->catfile($blog->site_path, $fname);
823    my $fmgr = $blog->file_mgr;
824    my($base, $path, $ext) = File::Basename::fileparse($local, '\.[^\.]*');
825    $ext = $MIME2EXT{$type} unless $ext;
826    my $base_copy = $base;
827    my $ext_copy = $ext;
828    $ext_copy =~ s/\.//;
829    my $i = 1;
830    while ($fmgr->exists($path . $base . $ext)) {
831        $base = $base_copy . '_' . $i++;
832    }
833    $local = $path . $base . $ext;
834    my $data = $content->body;
835    defined(my $bytes = $fmgr->put_data($data, $local, 'upload'))
836        or return $app->error(500, "Error writing uploaded file");
837
838    eval { require Image::Size; };
839    return $app->error(500, MT->translate("Perl module Image::Size is required to determine width and height of uploaded images.")) if $@;
840    my ( $w, $h, $id ) = Image::Size::imgsize($local);
841
842    require MT::Asset;
843    my $asset_pkg = MT::Asset->handler_for_file($local);
844    my $is_image  = defined($w)
845      && defined($h)
846      && $asset_pkg->isa('MT::Asset::Image');
847    my $asset;
848    if (!($asset = $asset_pkg->load(
849                { file_path => $local, blog_id => $blog->id })))
850    {
851        $asset = $asset_pkg->new();
852        $asset->file_path($local_relative);
853        $asset->file_name($base.$ext);
854        $asset->file_ext($ext_copy);
855        $asset->blog_id($blog->id);
856        $asset->created_by( $user->id );
857    }
858    else {
859        $asset->modified_by( $user->id );
860    }
861    my $original = $asset->clone;
862    my $url = '%r/' . $base . $ext;
863    $asset->url($url);
864    if ($is_image) {
865        $asset->image_width($w);
866        $asset->image_height($h);
867    }
868    $asset->mime_type($type);
869    $asset->save;
870
871    MT->run_callbacks(
872        'api_upload_file.' . $asset->class,
873        File => $local, file => $local,
874        Url => $url, url => $url,
875        Size => $bytes, size => $bytes,
876        Asset => $asset, asset => $asset,
877        Type => $asset->class, type => $asset->class,
878        Blog => $blog, blog => $blog);
879    if ($is_image) {
880        MT->run_callbacks(
881            'api_upload_image',
882            File => $local, file => $local,
883            Url => $url, url => $url,
884            Size => $bytes, size => $bytes,
885            Asset => $asset, asset => $asset,
886            Height => $h, height => $h,
887            Width => $w, width => $w,
888            Type => 'image', type => 'image',
889            ImageType => $id, image_type => $id,
890            Blog => $blog, blog => $blog);
891    }
892
893    $asset;
894}
895
896sub handle_upload {
897    my $app = shift;
898    my $blog = $app->{blog};
899   
900    my $asset = $app->_upload_to_asset or return;
901
902    my $link = XML::Atom::Link->new;
903    $link->type($asset->mime_type);
904    $link->rel('alternate');
905    $link->href($asset->url);
906    my $atom = XML::Atom::Entry->new;
907    $atom->title($asset->file_name);
908    $atom->add_link($link);
909    $app->response_code(201);
910    $app->response_content_type('application/x.atom+xml');
911    $atom->as_xml;
912}
913
914package MT::AtomServer::Weblog::Legacy;
915use strict;
916
917use base qw( MT::AtomServer::Weblog );
918
919use MT::I18N qw( encode_text );
920use XML::Atom;  # for LIBXML
921use XML::Atom::Feed;
922use MT::Blog;
923use MT::Permission;
924
925use constant NS_CATEGORY => 'http://sixapart.com/atom/category#';
926use constant NS_DC => MT::AtomServer::Weblog->NS_DC();
927
928sub script { $_[0]->{cfg}->AtomScript . '/weblog' }
929
930sub atom_content_type   { 'application/xml' }
931sub atom_x_content_type { 'application/x.atom+xml' }
932
933sub edit_link_rel { 'service.edit' }
934sub get_posts_order_field { 'authored_on' }
935
936sub new_feed {
937    my $app = shift;
938    XML::Atom::Feed->new();
939}
940
941sub new_with_entry {
942    my $app = shift;
943    my ($entry) = @_;
944    MT::Atom::Entry->new_with_entry($entry);
945}
946
947sub apply_basename {}
948
949sub get_weblogs {
950    my $app = shift;
951    my $user = $app->{user};
952    my $iter = $user->is_superuser
953        ? MT::Blog->load_iter()
954        : MT::Permission->load_iter({ author_id => $user->id });
955    my $feed = $app->new_feed();
956    my $base = $app->base . $app->uri;
957    require URI;
958    my $uri = URI->new($base);
959    if ( $uri ) {
960        my $created = MT::Util::format_ts('%Y-%m-%d', $user->created_on, undef, 'en', 0);
961        my $id = 'tag:'.$uri->host.','.$created.':'.$uri->path.'/weblogs-'.$user->id;
962        $feed->id($id);
963    }
964    while (my $thing = $iter->()) {
965        if ($thing->isa('MT::Permission')) {
966            next unless $thing->can_create_post;
967        }
968        my $blog = $thing->isa('MT::Blog') ? $thing
969            : MT::Blog->load($thing->blog_id);
970        next unless $blog;
971        my $uri = $base . '/blog_id=' . $blog->id;
972        my $blogname = encode_text($blog->name . ' #' . $blog->id, undef, 'utf-8');
973        $feed->add_link({ rel => 'service.post', title => $blogname,
974                          href => $uri, type => 'application/x.atom+xml' });
975        $feed->add_link({ rel => 'service.feed', title => $blogname,
976                          href => $uri, type => 'application/x.atom+xml' });
977        $feed->add_link({ rel => 'service.upload', title => $blogname,
978                          href => $uri . '/svc=upload',
979                          type => 'application/x.atom+xml' });
980        $feed->add_link({ rel => 'service.categories', title => $blogname,
981                          href => $uri . '/svc=categories',
982                          type => 'application/x.atom+xml' });
983        $feed->add_link({ rel => 'alternate', title => $blogname,
984                          href => $blog->site_url,
985                          type => 'text/html' });
986    }
987    $app->response_code(200);
988    $app->response_content_type('application/x.atom+xml');
989    $feed->as_xml;
990}
991
992sub get_categories {
993    my $app = shift;
994    my $blog = $app->{blog};
995    my $iter = MT::Category->load_iter({ blog_id => $blog->id });
996    my $doc;
997    if (LIBXML) {
998        $doc = XML::LibXML::Document->createDocument('1.0', 'utf-8');
999        my $root = $doc->createElementNS(NS_CATEGORY, 'categories');
1000        $doc->setDocumentElement($root);
1001    } else {
1002        $doc = XML::XPath::Node::Element->new('categories');
1003        my $ns = XML::XPath::Node::Namespace->new('#default' => NS_CATEGORY);
1004        $doc->appendNamespace($ns);
1005    }
1006    while (my $cat = $iter->()) {
1007        my $catlabel = encode_text($cat->label, undef, 'utf-8');
1008        if (LIBXML) {
1009            my $elem = $doc->createElementNS(NS_DC, 'subject');
1010            $doc->getDocumentElement->appendChild($elem);
1011            $elem->appendChild(XML::LibXML::Text->new($catlabel));
1012        } else {
1013            my $elem = XML::XPath::Node::Element->new('subject');
1014            my $ns = XML::XPath::Node::Namespace->new('#default' => NS_DC);
1015            $elem->appendNamespace($ns);
1016            $doc->appendChild($elem);
1017            $elem->appendChild(XML::XPath::Node::Text->new($catlabel));
1018        }
1019    }
1020    $app->response_code(200);
1021    $app->response_content_type('application/x.atom+xml');
1022    if (LIBXML) {
1023        $doc->toString(1);
1024    } else {
1025        return '<?xml version="1.0" encoding="utf-8"?>' . "\n" . $doc->toString;
1026    }
1027}
1028
1029package MT::AtomServer::Comments;
1030use strict;
1031
1032use base qw( MT::AtomServer::Weblog );
1033use MT::I18N qw( encode_text );
1034
1035sub script { $_[0]->{cfg}->AtomScript . '/comments' }
1036
1037sub handle_request {
1038    my $app = shift;
1039    $app->authenticate || return;
1040    if (my $svc = $app->{param}{svc}) {
1041        if ($svc eq 'upload') {
1042            return $app->handle_upload;
1043        } elsif ($svc eq 'categories') {
1044            return $app->get_categories;
1045        }
1046    }
1047    my $method = $app->request_method;
1048    if ($method eq 'POST') {
1049#        return $app->new_comment;
1050    } elsif ($method eq 'PUT') {
1051#        return $app->edit_comment;
1052    } elsif ($method eq 'DELETE') {
1053#        return $app->delete_comment;
1054    } elsif ($method eq 'GET') {
1055        if ($app->{param}{comment_id}) {
1056            return $app->get_comment;
1057        } elsif ($app->{param}{entry_id}) {
1058            return $app->get_comments;
1059        } else {
1060            return $app->get_blog_comments;
1061        }
1062    }
1063}
1064
1065sub new_with_comment {
1066    my $app = shift;
1067    my ($comment) = @_;
1068    my $atom = MT::Atom::Entry->new_with_comment( $comment, Version => 1.0 );
1069
1070    my $mo = MT::Atom::Entry::_create_issued(
1071        $comment->modified_on || $comment->created_on, $comment->blog);
1072    $atom->set(MT::AtomServer::Weblog::NS_APP(), 'edited', $mo);
1073
1074    $atom;
1075}
1076
1077sub get_comment {
1078    my $app = shift;
1079    my $blog = $app->{blog};
1080    my $comment_id = $app->{param}{comment_id}
1081        or return $app->error(400, "No comment_id");
1082    my $comment = MT::Comment->load($comment_id)
1083        or return $app->error(400, "Invalid comment_id");
1084    my $entry = $comment->entry;
1085    my $uri = $app->base . $app->uri . '/blog_id=' . $blog->id;
1086    my $c = $app->new_with_comment($comment);
1087    $c->add_link({ rel => 'self', type => $app->atom_x_content_type,
1088                   href => $uri . '/comment_id=' . $comment->id });
1089    # feed/updated should be added before entries
1090    # so we postpone adding them until later
1091    $c->set('http://purl.org/syndication/thread/1.0', 'in-reply-to',
1092        undef,
1093        { ref => $entry->atom_id,
1094            type => 'text/html',
1095            href => $entry->permalink } );
1096    $app->run_callbacks( 'get_comment', $c, $comment );
1097    $app->response_content_type($app->atom_content_type);
1098    $c->as_xml;
1099}
1100
1101sub get_blog_comments {
1102    my $app = shift;
1103    my $blog = $app->{blog};
1104    my %terms = (blog_id => $blog->id, visible => 1);
1105    my %arg = (sort => $app->get_posts_order_field, direction => 'descend');
1106    $arg{limit}  = $app->{param}{limit}  || 21;
1107    $arg{offset} = $app->{param}{offset} || 0;
1108
1109    my $feed = $app->new_feed();
1110    my $uri = $app->base . $app->uri . '/blog_id=' . $blog->id;
1111    my $blogname = encode_text($blog->name, undef, 'utf-8');
1112    $feed->add_link({ rel => 'alternate', type => 'text/html',
1113                      href => $blog->site_url });
1114    $feed->add_link({ rel => 'self', type => $app->atom_x_content_type,
1115                      href => $uri });
1116    $feed->title($blogname);
1117
1118    require URI;
1119    my $site_uri = URI->new($blog->site_url);
1120    if ( $site_uri ) {
1121        my $blog_created = MT::Util::format_ts('%Y-%m-%d', $blog->created_on, $blog, 'en', 0);
1122        my $id = 'tag:'.$site_uri->host.','.$blog_created.':'.$site_uri->path.'/'.$blog->id;
1123        $feed->id($id);
1124    }
1125    $app->_comments_in_atom($feed, \%terms, \%arg);
1126    $app->run_callbacks( 'get_blog_comments', $feed, $blog );
1127    ## xxx add next/prev links
1128    $app->response_content_type($app->atom_content_type);
1129    $feed->as_xml;
1130}
1131
1132sub get_comments {
1133    my $app = shift;
1134    my $blog = $app->{blog};
1135    my $entry_id = $app->{param}{entry_id}
1136        or return $app->error(400, "No entry_id");
1137    my $entry = MT::Entry->load($entry_id)
1138        or return $app->error(400, "Invalid entry_id");
1139    my %terms = (blog_id => $blog->id, entry_id => $entry->id, visible => 1);
1140    my %arg = (sort => $app->get_posts_order_field, direction => 'descend');
1141    $arg{limit}  = $app->{param}{limit}  || 21;
1142    $arg{offset} = $app->{param}{offset} || 0;
1143
1144    my $feed = $app->new_feed();
1145    my $uri = $app->base . $app->uri . '/blog_id=' . $blog->id;
1146    my $blogname = encode_text($blog->name, undef, 'utf-8');
1147    $feed->add_link({ rel => 'alternate', type => 'text/html',
1148                      href => $entry->permalink });
1149    $feed->add_link({ rel => 'self', type => $app->atom_x_content_type,
1150                      href => $uri . '/entry_id=' . $entry->id });
1151    $feed->title($entry->title);
1152    $feed->id($entry->atom_id . '/comments');
1153    $app->_comments_in_atom($feed, \%terms, \%arg);
1154    $app->run_callbacks( 'get_comments', $feed, $entry );
1155    ## xxx add next/prev links
1156    $app->response_content_type($app->atom_content_type);
1157    $feed->as_xml;
1158}
1159
1160sub _comments_in_atom {
1161    my $app = shift;
1162    my ( $feed, $terms, $args ) = @_;
1163    require MT::Comment;
1164    my $iter = MT::Comment->load_iter($terms, $args);
1165    my $latest_date = 0;
1166    my @comments;
1167    while (my $comment = $iter->()) {
1168        my $c = $app->new_with_comment($comment);
1169        # feed/updated should be added before entries
1170        # so we postpone adding them until later
1171        my $entry = $comment->entry;
1172        $c->set('http://purl.org/syndication/thread/1.0', 'in-reply-to',
1173            undef,
1174            { ref => $entry->atom_id,
1175              type => 'text/html',
1176              href => $entry->permalink } );
1177        push @comments, $c;
1178        my $date = $comment->modified_on || $comment->created_on;
1179        if ( $latest_date < $date ) {
1180            $latest_date = $date;
1181            $feed->updated( $c->updated );
1182        }
1183    }
1184    $feed->add_entry($_) foreach @comments;
1185    $feed;
1186}
1187
1188
11891;
1190__END__
1191
1192=head1 NAME
1193
1194MT::AtomServer
1195
1196=head1 SYNOPSIS
1197
1198An Atom Publishing API interface for communicating with Movable Type.
1199
1200=head1 METHODS
1201
1202=head2 $app->xml_body()
1203
1204Takes the content posted to the server and parses it into an XML document.
1205Uses either XML::LibXML or XML::XPath depending on which is available.
1206
1207=head2 $app->iso2epoch($iso_ts)
1208
1209Converts C<$iso_ts> in the format of an ISO timestamp into a unix timestamp
1210(seconds since the epoch).
1211
1212=head2 $app->init
1213
1214Initializes the application.
1215
1216=head2 $app->get_auth_info
1217
1218Processes the request for WSSE authentication and returns a hash containing:
1219
1220=over 4
1221
1222=item * Username
1223
1224=item * PasswordDigest
1225
1226=item * Nonce
1227
1228=item * Created
1229
1230=back
1231
1232=head2 $app->handle_request
1233
1234The implementation of this in I<MT::AtomServer::Weblog> passes the request
1235to the proper method.
1236
1237=head2 $app->handle
1238
1239Wrapper method that determines the proper AtomServer package to pass the
1240request to.
1241
1242=head2 $app->iso2ts($iso_ts, $target_zone)
1243
1244Converts C<$iso_ts> in the format of an ISO timestamp into a MT-compatible
1245timestamp (YYYYMMDDHHMMSS) for the specified timezone C<$target_zone>.
1246
1247=head2 $app->atom_body
1248
1249Processes the request as Atom content and returns an XML::Atom object.
1250
1251=head2 $app->error($code, $message)
1252
1253Sends the HTTP headers necessary to relay an error.
1254
1255=head2 $app->authenticate()
1256
1257Checks the WSSE authentication with the local MT user database and
1258confirms the user is authorized to access the resources required by
1259the request.
1260
1261=head2 $app->show_error($message)
1262
1263Returns an XML wrapper for the error response.
1264
1265=head2 $app->auth_failure($code, $message)
1266
1267Handles the response in the event of an authentication failure.
1268
1269=head1 CALLBACKS
1270
1271=over 4
1272
1273=item api_pre_save.entry
1274
1275    callback($eh, $app, $entry, $original_entry)
1276
1277Called before saving a new or existing entry. If saving a new entry, the
1278$original_entry will have an unassigned 'id'. This callback is executed
1279as a filter, so your handler must return 1 to allow the entry to be saved.
1280
1281=item api_post_save.entry
1282
1283    callback($eh, $app, $entry, $original_entry)
1284
1285Called after saving a new or existing entry. If saving a new entry, the
1286$original_entry will have an unassigned 'id'.
1287
1288=item get_posts
1289
1290    callback($eh, $app, $feed, $blog)
1291
1292Called right before get_posts method returns atom feed response.
1293I<$feed> is a reference to XML::Atom::Feed object.
1294I<$blog> is a reference to the requested MT::Blog object.
1295
1296=item get_post
1297
1298    callback($eh, $app, $atom_entry, $entry)
1299
1300Called right before get_post method returns atom entry response.
1301I<$atom_entry> is a reference to XML::Atom::Entry object.
1302I<$entry> is a reference to the requested MT::Entry object.
1303
1304=item get_blog_comments
1305
1306    callback($eh, $app, $feed, $blog)
1307
1308Called right before get_blog_comments method returns atom feed response.
1309I<$feed> is a reference to XML::Atom::Feed object.
1310I<$blog> is a reference to the requested MT::Blog object.
1311
1312=item get_comments
1313
1314    callback($eh, $app, $feed, $entry)
1315
1316Called right before get_comments method returns atom feed response.
1317I<$feed> is a reference to XML::Atom::Feed object.
1318I<$entry> is a reference to the requested MT::Entry object.
1319
1320=item get_comment
1321
1322    callback($eh, $app, $atom_entry, $comment)
1323
1324Called right before get_comment method returns atom entry response.
1325I<$atom_entry> is a reference to XML::Atom::Entry object.
1326I<$comment> is a reference to the requested MT::Comment object.
1327
1328=back
1329
1330=cut
Note: See TracBrowser for help on using the browser.