root/branches/release-35/lib/MT/AtomServer.pm @ 1947

Revision 1947, 43.6 kB (checked in by fumiakiy, 20 months ago)

Implemented comments retrieval via Atom PP. BugId:79334

  • 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
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('[_1]: Entries', $blog->name)));
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        require MT::DateTime;
566        if ( 0 < MT::DateTime->compare( blog => $blog,
567                a => $pub_ts,
568                b => { value => time(), type => 'epoch' } )
569           )
570        {
571            $entry->status(MT::Entry::FUTURE())
572        }
573    }
574## xxx mt/typepad-specific fields
575    $app->apply_basename($entry, $atom);
576    $entry->discover_tb_from_entry();
577
578    if (my @link = $atom->link) {
579        my $i = 0;
580        my $img_html = '';
581        my $num_links = scalar @link;
582        for my $link (@link) {
583            next unless $link->rel eq 'related';
584            my($asset_id) = $link->href =~ /asset\-(\d+)$/;
585            if ($asset_id) {
586                require MT::Asset;
587                my $a = MT::Asset->load($asset_id);
588                next unless $a;
589                my $pkg = MT::Asset->handler_for_file($a->file_name);
590                my $asset = bless $a, $pkg;
591                $img_html .= $asset->as_html({ include => 1 });
592            }
593        }
594        if ($img_html) {
595            $img_html .= qq{<br style="clear: left;" />\n\n};
596            $entry->text($img_html . $body);
597        }
598    }
599
600    MT->run_callbacks('api_pre_save.entry', $app, $entry, $orig_entry)
601        or return $app->error(500, MT->translate("PreSave failed [_1]", MT->errstr));
602
603    $entry->save or return $app->error(500, $entry->errstr);
604
605    require MT::Log;
606    $app->log({
607        message => $app->translate("User '[_1]' (user #[_2]) added [lc,_4] #[_3]", $user->name, $user->id, $entry->id, $entry->class_label),
608        level => MT::Log::INFO(),
609        class => 'entry',
610        category => 'new',
611        metadata => $entry->id
612    });
613    ## Save category, if present.
614    if ($cat) {
615        my $place = MT::Placement->new;
616        $place->is_primary(1);
617        $place->entry_id($entry->id);
618        $place->blog_id($blog->id);
619        $place->category_id($cat->id);
620        $place->save or return $app->error(500, $place->errstr);
621    }
622
623    MT->run_callbacks('api_post_save.entry', $app, $entry, $orig_entry);
624
625    $app->publish($entry);
626    $app->response_code(201);
627    $app->response_content_type('application/atom+xml');
628    my $edit_uri = $app->base . $app->uri . '/blog_id=' . $entry->blog_id . '/entry_id=' . $entry->id;
629    $app->set_header('Location', $edit_uri);
630    $atom = $app->new_with_entry($entry);
631    $atom->add_link({ rel => $app->edit_link_rel,
632                      href => $edit_uri,
633                      type => 'application/atom+xml',  # even in Legacy
634                      title => $entry->title });
635    $atom->as_xml;
636}
637
638sub edit_post {
639    my $app = shift;
640    my $atom = $app->atom_body or return;
641    my $blog = $app->{blog};
642    my $enc = $app->config('PublishCharset');
643    my $entry_id = $app->{param}{entry_id}
644        or return $app->error(400, "No entry_id");
645    my $entry = MT::Entry->load($entry_id)
646        or return $app->error(400, "Invalid entry_id");
647    return $app->error(403, "Access denied")
648        unless $app->{perms}->can_edit_entry($entry, $app->{user});
649    my $orig_entry = $entry->clone;
650    $entry->title(encode_text($atom->title,'utf-8',$enc));
651    $entry->text(encode_text(MT::I18N::utf8_off($atom->content()->body()),'utf-8',$enc));
652    $entry->excerpt(encode_text($atom->summary,'utf-8',$enc));
653    $entry->modified_by($app->{user}->id);
654    if (my $iso = $atom->issued) {
655        my $pub_ts = MT::Util::iso2ts($blog, $iso);
656        $entry->authored_on($pub_ts);
657        require MT::DateTime;
658        if ( 0 < MT::DateTime->compare( blog => $blog,
659                a => $pub_ts,
660                b => { value => time(), type => 'epoch' } )
661           )
662        {
663            $entry->status(MT::Entry::FUTURE())
664        }
665    }
666## xxx mt/typepad-specific fields
667    $app->apply_basename($entry, $atom);
668    $entry->discover_tb_from_entry();
669
670    MT->run_callbacks('api_pre_save.entry', $app, $entry, $orig_entry)
671        or return $app->error(500, MT->translate("PreSave failed [_1]", MT->errstr));
672
673    $entry->save or return $app->error(500, "Entry not saved");
674
675    require MT::Log;
676    $app->log({
677        message => $app->translate("User '[_1]' (user #[_2]) edited [lc,_4] #[_3]", $app->{user}->name, $app->{user}->id, $entry->id, $entry->class_label),
678        level => MT::Log::INFO(),
679        class => 'entry',
680        category => 'new',
681        metadata => $entry->id
682    });
683
684    MT->run_callbacks('api_post_save.entry', $app, $entry, $orig_entry);
685
686    if ($entry->status == MT::Entry::RELEASE()) {
687        $app->publish($entry) or return $app->error(500, "Entry not published");
688    }
689    $app->response_code(200);
690    $app->response_content_type($app->atom_content_type);
691    $atom = $app->new_with_entry($entry);
692    $atom->as_xml;
693}
694
695sub get_posts {
696    my $app = shift;
697    my $blog = $app->{blog};
698    my %terms = (blog_id => $blog->id);
699    my %arg = (sort => $app->get_posts_order_field, direction => 'descend');
700    my $Limit = 20;
701    $arg{limit} = $Limit + 1;
702    $arg{offset} = $app->{param}{offset} || 0;
703    my $iter = MT::Entry->load_iter(\%terms, \%arg);
704    my $feed = $app->new_feed();
705    my $uri = $app->base . $app->uri . '/blog_id=' . $blog->id;
706    my $blogname = encode_text($blog->name, undef, 'utf-8');
707    $feed->add_link({ rel => 'alternate', type => 'text/html',
708                      href => $blog->site_url });
709    $feed->add_link({ rel => 'self', type => $app->atom_x_content_type,
710                      href => $uri });
711    $feed->title($blogname);
712    # FIXME: move the line to the Legacy class
713    if ( !$feed->version || ( $feed->version < 1.0 ) ) {
714        $feed->add_link({ rel => 'service.post', type => $app->atom_x_content_type,
715                          href => $uri, title => $blogname });
716    }
717    require URI;
718    my $site_uri = URI->new($blog->site_url);
719    if ( $site_uri ) {
720        my $blog_created = format_ts('%Y-%m-%d', $blog->created_on, $blog, 'en', 0);
721        my $id = 'tag:'.$site_uri->host.','.$blog_created.':'.$site_uri->path.'/'.$blog->id;
722        $feed->id($id);
723    }
724    my $latest_date = 0;
725    $uri .= '/entry_id=';
726    my @entries;
727    while (my $entry = $iter->()) {
728        my $e = $app->new_with_entry($entry);
729        $e->add_link({ rel => $app->edit_link_rel, type => $app->atom_x_content_type,
730                       href => ($uri . $entry->id), title => encode_text($entry->title, undef,'utf-8') });
731        $e->add_link({ rel => 'replies', type => $app->atom_x_content_type,
732                href => $app->base . $app->app_path . $app->config->AtomScript . '/comments/blog_id=' . $blog->id . '/entry_id=' . $entry->id });
733
734        # feed/updated should be added before entries
735        # so we postpone adding them until later
736        push @entries, $e;
737        my $date = $entry->modified_on || $entry->authored_on;
738        if ( $latest_date < $date ) {
739            $latest_date = $date;
740            $feed->updated( $e->updated );
741        }
742    }
743    $feed->add_entry($_) foreach @entries;
744    ## xxx add next/prev links
745    $app->run_callbacks( 'get_posts', $feed, $blog );
746    $app->response_content_type($app->atom_content_type);
747    $feed->as_xml;
748}
749
750sub get_post {
751    my $app = shift;
752    my $blog = $app->{blog};
753    my $entry_id = $app->{param}{entry_id}
754        or return $app->error(400, "No entry_id");
755    my $entry = MT::Entry->load($entry_id)
756        or return $app->error(400, "Invalid entry_id");
757    return $app->error(403, "Access denied")
758        unless $app->{perms}->can_edit_entry($entry, $app->{user});
759    $app->response_content_type($app->atom_content_type);
760    my $atom = $app->new_with_entry($entry);
761    my $uri = $app->base . $app->uri . '/blog_id=' . $blog->id;
762    $uri .= '/entry_id=';
763    $atom->add_link({ rel => $app->edit_link_rel, type => $app->atom_x_content_type,
764        href => ($uri . $entry->id), title => encode_text($entry->title, undef,'utf-8') });
765    $atom->add_link({ rel => 'replies', type => $app->atom_x_content_type,
766        href => $app->base
767            . $app->app_path
768            . $app->config->AtomScript
769            . '/comments/blog_id=' . $blog->id
770            . '/entry_id=' . $entry->id
771    });
772    $app->run_callbacks( 'get_post', $atom, $entry );
773    $app->response_content_type($app->atom_content_type);
774    $atom->as_xml;
775}
776
777sub delete_post {
778    my $app = shift;
779    my $blog = $app->{blog};
780    my $entry_id = $app->{param}{entry_id}
781        or return $app->error(400, "No entry_id");
782    my $entry = MT::Entry->load($entry_id)
783        or return $app->error(400, "Invalid entry_id");
784    return $app->error(403, "Access denied")
785        unless $app->{perms}->can_edit_entry($entry, $app->{user});
786    $entry->remove
787        or return $app->error(500, $entry->errstr);
788    $app->publish($entry, 1) or return $app->error(500, $app->errstr);
789    '';
790}
791
792sub _upload_to_asset {
793    my $app = shift;
794    my $atom = $app->atom_body or return;
795    my $blog = $app->{blog};
796    my $user = $app->{user};
797    my %MIME2EXT = (
798        'text/plain'         => '.txt',
799        'image/jpeg'         => '.jpg',
800        'video/3gpp'         => '.3gp',
801        'application/x-mpeg' => '.mpg',
802        'video/mp4'          => '.mp4',
803        'video/quicktime'    => '.mov',
804        'audio/mpeg'         => '.mp3',
805        'audio/x-wav'        => '.wav',
806        'audio/ogg'          => '.ogg',
807        'audio/ogg-vorbis'   => '.ogg',
808    );
809
810    return $app->error(403, "Access denied") unless $app->{perms}->can_upload;
811    my $content = $atom->content;
812    my $type = $content->type
813        or return $app->error(400, "content \@type is required");
814    my $fname = $atom->title or return $app->error(400, "title is required");
815    $fname = basename($fname);
816    return $app->error(400, "Invalid or empty filename")
817        if $fname =~ m!/|\.\.|\0|\|!;
818
819    my $local_relative = File::Spec->catfile('%r', $fname);
820    my $local = File::Spec->catfile($blog->site_path, $fname);
821    my $fmgr = $blog->file_mgr;
822    my($base, $path, $ext) = File::Basename::fileparse($local, '\.[^\.]*');
823    $ext = $MIME2EXT{$type} unless $ext;
824    my $base_copy = $base;
825    my $ext_copy = $ext;
826    $ext_copy =~ s/\.//;
827    my $i = 1;
828    while ($fmgr->exists($path . $base . $ext)) {
829        $base = $base_copy . '_' . $i++;
830    }
831    $local = $path . $base . $ext;
832    my $data = $content->body;
833    defined(my $bytes = $fmgr->put_data($data, $local, 'upload'))
834        or return $app->error(500, "Error writing uploaded file");
835
836    eval { require Image::Size; };
837    return $app->error(500, MT->translate("Perl module Image::Size is required to determine width and height of uploaded images.")) if $@;
838    my ( $w, $h, $id ) = Image::Size::imgsize($local);
839
840    require MT::Asset;
841    my $asset_pkg = MT::Asset->handler_for_file($local);
842    my $is_image  = defined($w)
843      && defined($h)
844      && $asset_pkg->isa('MT::Asset::Image');
845    my $asset;
846    if (!($asset = $asset_pkg->load(
847                { file_path => $local, blog_id => $blog->id })))
848    {
849        $asset = $asset_pkg->new();
850        $asset->file_path($local_relative);
851        $asset->file_name($base.$ext);
852        $asset->file_ext($ext_copy);
853        $asset->blog_id($blog->id);
854        $asset->created_by( $user->id );
855    }
856    else {
857        $asset->modified_by( $user->id );
858    }
859    my $original = $asset->clone;
860    my $url = '%r/' . $base . $ext;
861    $asset->url($url);
862    if ($is_image) {
863        $asset->image_width($w);
864        $asset->image_height($h);
865    }
866    $asset->mime_type($type);
867    $asset->save;
868
869    MT->run_callbacks(
870        'api_upload_file.' . $asset->class,
871        File => $local, file => $local,
872        Url => $url, url => $url,
873        Size => $bytes, size => $bytes,
874        Asset => $asset, asset => $asset,
875        Type => $asset->class, type => $asset->class,
876        Blog => $blog, blog => $blog);
877    if ($is_image) {
878        MT->run_callbacks(
879            'api_upload_image',
880            File => $local, file => $local,
881            Url => $url, url => $url,
882            Size => $bytes, size => $bytes,
883            Asset => $asset, asset => $asset,
884            Height => $h, height => $h,
885            Width => $w, width => $w,
886            Type => 'image', type => 'image',
887            ImageType => $id, image_type => $id,
888            Blog => $blog, blog => $blog);
889    }
890
891    $asset;
892}
893
894sub handle_upload {
895    my $app = shift;
896    my $blog = $app->{blog};
897   
898    my $asset = $app->_upload_to_asset or return;
899
900    my $link = XML::Atom::Link->new;
901    $link->type($asset->mime_type);
902    $link->rel('alternate');
903    $link->href($asset->url);
904    my $atom = XML::Atom::Entry->new;
905    $atom->title($asset->file_name);
906    $atom->add_link($link);
907    $app->response_code(201);
908    $app->response_content_type('application/x.atom+xml');
909    $atom->as_xml;
910}
911
912package MT::AtomServer::Weblog::Legacy;
913use strict;
914
915use base qw( MT::AtomServer::Weblog );
916
917use MT::I18N qw( encode_text );
918use XML::Atom;  # for LIBXML
919use XML::Atom::Feed;
920use MT::Blog;
921use MT::Permission;
922
923use constant NS_CATEGORY => 'http://sixapart.com/atom/category#';
924use constant NS_DC => MT::AtomServer::Weblog->NS_DC();
925
926sub script { $_[0]->{cfg}->AtomScript . '/weblog' }
927
928sub atom_content_type   { 'application/xml' }
929sub atom_x_content_type { 'application/x.atom+xml' }
930
931sub edit_link_rel { 'service.edit' }
932sub get_posts_order_field { 'authored_on' }
933
934sub new_feed {
935    my $app = shift;
936    XML::Atom::Feed->new();
937}
938
939sub new_with_entry {
940    my $app = shift;
941    my ($entry) = @_;
942    MT::Atom::Entry->new_with_entry($entry);
943}
944
945sub apply_basename {}
946
947sub get_weblogs {
948    my $app = shift;
949    my $user = $app->{user};
950    my $iter = $user->is_superuser
951        ? MT::Blog->load_iter()
952        : MT::Permission->load_iter({ author_id => $user->id });
953    my $feed = $app->new_feed();
954    my $base = $app->base . $app->uri;
955    require URI;
956    my $uri = URI->new($base);
957    if ( $uri ) {
958        my $created = MT::Util::format_ts('%Y-%m-%d', $user->created_on, undef, 'en', 0);
959        my $id = 'tag:'.$uri->host.','.$created.':'.$uri->path.'/weblogs-'.$user->id;
960        $feed->id($id);
961    }
962    while (my $thing = $iter->()) {
963        if ($thing->isa('MT::Permission')) {
964            next unless $thing->can_create_post;
965        }
966        my $blog = $thing->isa('MT::Blog') ? $thing
967            : MT::Blog->load($thing->blog_id);
968        next unless $blog;
969        my $uri = $base . '/blog_id=' . $blog->id;
970        my $blogname = encode_text($blog->name . ' #' . $blog->id, undef, 'utf-8');
971        $feed->add_link({ rel => 'service.post', title => $blogname,
972                          href => $uri, type => 'application/x.atom+xml' });
973        $feed->add_link({ rel => 'service.feed', title => $blogname,
974                          href => $uri, type => 'application/x.atom+xml' });
975        $feed->add_link({ rel => 'service.upload', title => $blogname,
976                          href => $uri . '/svc=upload',
977                          type => 'application/x.atom+xml' });
978        $feed->add_link({ rel => 'service.categories', title => $blogname,
979                          href => $uri . '/svc=categories',
980                          type => 'application/x.atom+xml' });
981        $feed->add_link({ rel => 'alternate', title => $blogname,
982                          href => $blog->site_url,
983                          type => 'text/html' });
984    }
985    $app->response_code(200);
986    $app->response_content_type('application/x.atom+xml');
987    $feed->as_xml;
988}
989
990sub get_categories {
991    my $app = shift;
992    my $blog = $app->{blog};
993    my $iter = MT::Category->load_iter({ blog_id => $blog->id });
994    my $doc;
995    if (LIBXML) {
996        $doc = XML::LibXML::Document->createDocument('1.0', 'utf-8');
997        my $root = $doc->createElementNS(NS_CATEGORY, 'categories');
998        $doc->setDocumentElement($root);
999    } else {
1000        $doc = XML::XPath::Node::Element->new('categories');
1001        my $ns = XML::XPath::Node::Namespace->new('#default' => NS_CATEGORY);
1002        $doc->appendNamespace($ns);
1003    }
1004    while (my $cat = $iter->()) {
1005        my $catlabel = encode_text($cat->label, undef, 'utf-8');
1006        if (LIBXML) {
1007            my $elem = $doc->createElementNS(NS_DC, 'subject');
1008            $doc->getDocumentElement->appendChild($elem);
1009            $elem->appendChild(XML::LibXML::Text->new($catlabel));
1010        } else {
1011            my $elem = XML::XPath::Node::Element->new('subject');
1012            my $ns = XML::XPath::Node::Namespace->new('#default' => NS_DC);
1013            $elem->appendNamespace($ns);
1014            $doc->appendChild($elem);
1015            $elem->appendChild(XML::XPath::Node::Text->new($catlabel));
1016        }
1017    }
1018    $app->response_code(200);
1019    $app->response_content_type('application/x.atom+xml');
1020    if (LIBXML) {
1021        $doc->toString(1);
1022    } else {
1023        return '<?xml version="1.0" encoding="utf-8"?>' . "\n" . $doc->toString;
1024    }
1025}
1026
1027package MT::AtomServer::Comments;
1028use strict;
1029
1030use base qw( MT::AtomServer::Weblog );
1031use MT::I18N qw( encode_text );
1032
1033sub script { $_[0]->{cfg}->AtomScript . '/comments' }
1034
1035sub handle_request {
1036    my $app = shift;
1037    $app->authenticate || return;
1038    if (my $svc = $app->{param}{svc}) {
1039        if ($svc eq 'upload') {
1040            return $app->handle_upload;
1041        } elsif ($svc eq 'categories') {
1042            return $app->get_categories;
1043        }
1044    }
1045    my $method = $app->request_method;
1046    if ($method eq 'POST') {
1047#        return $app->new_comment;
1048    } elsif ($method eq 'PUT') {
1049#        return $app->edit_comment;
1050    } elsif ($method eq 'DELETE') {
1051#        return $app->delete_comment;
1052    } elsif ($method eq 'GET') {
1053        if ($app->{param}{comment_id}) {
1054            return $app->get_comment;
1055        } elsif ($app->{param}{entry_id}) {
1056            return $app->get_comments;
1057        } else {
1058            return $app->get_blog_comments;
1059        }
1060    }
1061}
1062
1063sub new_with_comment {
1064    my $app = shift;
1065    my ($comment) = @_;
1066    my $atom = MT::Atom::Entry->new_with_comment( $comment, Version => 1.0 );
1067
1068    my $mo = MT::Atom::Entry::_create_issued(
1069        $comment->modified_on || $comment->created_on, $comment->blog);
1070    $atom->set(MT::AtomServer::Weblog::NS_APP(), 'edited', $mo);
1071
1072    $atom;
1073}
1074
1075sub get_comment {
1076    my $app = shift;
1077    my $blog = $app->{blog};
1078    my $comment_id = $app->{param}{comment_id}
1079        or return $app->error(400, "No comment_id");
1080    my $comment = MT::Comment->load($comment_id)
1081        or return $app->error(400, "Invalid comment_id");
1082    my $entry = $comment->entry;
1083    my $uri = $app->base . $app->uri . '/blog_id=' . $blog->id;
1084    my $c = $app->new_with_comment($comment);
1085    $c->add_link({ rel => 'self', type => $app->atom_x_content_type,
1086                   href => $uri . '/comment_id=' . $comment->id });
1087    # feed/updated should be added before entries
1088    # so we postpone adding them until later
1089    $c->set('http://purl.org/syndication/thread/1.0', 'in-reply-to',
1090        undef,
1091        { ref => $entry->atom_id,
1092            type => 'text/html',
1093            href => $entry->permalink } );
1094    $app->run_callbacks( 'get_comment', $c, $comment );
1095    $app->response_content_type($app->atom_content_type);
1096    $c->as_xml;
1097}
1098
1099sub get_blog_comments {
1100    my $app = shift;
1101    my $blog = $app->{blog};
1102    my %terms = (blog_id => $blog->id, visible => 1);
1103    my %arg = (sort => $app->get_posts_order_field, direction => 'descend');
1104    my $Limit = 20;
1105    $arg{limit} = $Limit + 1;
1106    $arg{offset} = $app->{param}{offset} || 0;
1107
1108    my $feed = $app->new_feed();
1109    my $uri = $app->base . $app->uri . '/blog_id=' . $blog->id;
1110    my $blogname = encode_text($blog->name, undef, 'utf-8');
1111    $feed->add_link({ rel => 'alternate', type => 'text/html',
1112                      href => $blog->site_url });
1113    $feed->add_link({ rel => 'self', type => $app->atom_x_content_type,
1114                      href => $uri });
1115    $feed->title($blogname);
1116
1117    require URI;
1118    my $site_uri = URI->new($blog->site_url);
1119    if ( $site_uri ) {
1120        my $blog_created = MT::Util::format_ts('%Y-%m-%d', $blog->created_on, $blog, 'en', 0);
1121        my $id = 'tag:'.$site_uri->host.','.$blog_created.':'.$site_uri->path.'/'.$blog->id;
1122        $feed->id($id);
1123    }
1124    $app->_comments_in_atom($feed, \%terms, \%arg);
1125    $app->run_callbacks( 'get_blog_comments', $feed, $blog );
1126    ## xxx add next/prev links
1127    $app->response_content_type($app->atom_content_type);
1128    $feed->as_xml;
1129}
1130
1131sub get_comments {
1132    my $app = shift;
1133    my $blog = $app->{blog};
1134    my $entry_id = $app->{param}{entry_id}
1135        or return $app->error(400, "No entry_id");
1136    my $entry = MT::Entry->load($entry_id)
1137        or return $app->error(400, "Invalid entry_id");
1138    my %terms = (blog_id => $blog->id, entry_id => $entry->id, visible => 1);
1139    my %arg = (sort => $app->get_posts_order_field, direction => 'descend');
1140    my $Limit = 20;
1141    $arg{limit} = $Limit + 1;
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.