root/branches/release-41/lib/MT/AtomServer.pm @ 2737

Revision 2737, 44.1 kB (checked in by takayama, 17 months ago)

Fixed BugId:80382
* Changed to removes fileinfo records when entry was deleted.

  • CMS, Atom and XMLRPC
  • 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
789    # Delete archive file
790    my $blog = MT::Blog->load($entry->blog_id);
791    my %recip = $app->publisher->rebuild_deleted_entry(
792        Entry => $entry,
793        Blog  => $blog);
794
795    # Rebuild archives
796    $app->rebuild_archives(
797        Blog             => $blog,
798        Recip            => \%recip,
799    ) or die _fault($app->errstr);
800
801    # Remove object
802    $entry->remove
803        or return $app->error(500, $entry->errstr);
804    '';
805}
806
807sub _upload_to_asset {
808    my $app = shift;
809    my $atom = $app->atom_body or return;
810    my $blog = $app->{blog};
811    my $user = $app->{user};
812    my %MIME2EXT = (
813        'text/plain'         => '.txt',
814        'image/jpeg'         => '.jpg',
815        'video/3gpp'         => '.3gp',
816        'application/x-mpeg' => '.mpg',
817        'video/mp4'          => '.mp4',
818        'video/quicktime'    => '.mov',
819        'audio/mpeg'         => '.mp3',
820        'audio/x-wav'        => '.wav',
821        'audio/ogg'          => '.ogg',
822        'audio/ogg-vorbis'   => '.ogg',
823    );
824
825    return $app->error(403, "Access denied") unless $app->{perms}->can_upload;
826    my $content = $atom->content;
827    my $type = $content->type
828        or return $app->error(400, "content \@type is required");
829    my $fname = $atom->title or return $app->error(400, "title is required");
830    $fname = basename($fname);
831    return $app->error(400, "Invalid or empty filename")
832        if $fname =~ m!/|\.\.|\0|\|!;
833
834    my $local_relative = File::Spec->catfile('%r', $fname);
835    my $local = File::Spec->catfile($blog->site_path, $fname);
836    my $fmgr = $blog->file_mgr;
837    my($base, $path, $ext) = File::Basename::fileparse($local, '\.[^\.]*');
838    $ext = $MIME2EXT{$type} unless $ext;
839    my $base_copy = $base;
840    my $ext_copy = $ext;
841    $ext_copy =~ s/\.//;
842    my $i = 1;
843    while ($fmgr->exists($path . $base . $ext)) {
844        $base = $base_copy . '_' . $i++;
845    }
846    $local = $path . $base . $ext;
847    my $data = $content->body;
848    defined(my $bytes = $fmgr->put_data($data, $local, 'upload'))
849        or return $app->error(500, "Error writing uploaded file");
850
851    eval { require Image::Size; };
852    return $app->error(500, MT->translate("Perl module Image::Size is required to determine width and height of uploaded images.")) if $@;
853    my ( $w, $h, $id ) = Image::Size::imgsize($local);
854
855    require MT::Asset;
856    my $asset_pkg = MT::Asset->handler_for_file($local);
857    my $is_image  = defined($w)
858      && defined($h)
859      && $asset_pkg->isa('MT::Asset::Image');
860    my $asset;
861    if (!($asset = $asset_pkg->load(
862                { file_path => $local, blog_id => $blog->id })))
863    {
864        $asset = $asset_pkg->new();
865        $asset->file_path($local_relative);
866        $asset->file_name($base.$ext);
867        $asset->file_ext($ext_copy);
868        $asset->blog_id($blog->id);
869        $asset->created_by( $user->id );
870    }
871    else {
872        $asset->modified_by( $user->id );
873    }
874    my $original = $asset->clone;
875    my $url = '%r/' . $base . $ext;
876    $asset->url($url);
877    if ($is_image) {
878        $asset->image_width($w);
879        $asset->image_height($h);
880    }
881    $asset->mime_type($type);
882    $asset->save;
883
884    MT->run_callbacks(
885        'api_upload_file.' . $asset->class,
886        File => $local, file => $local,
887        Url => $url, url => $url,
888        Size => $bytes, size => $bytes,
889        Asset => $asset, asset => $asset,
890        Type => $asset->class, type => $asset->class,
891        Blog => $blog, blog => $blog);
892    if ($is_image) {
893        MT->run_callbacks(
894            'api_upload_image',
895            File => $local, file => $local,
896            Url => $url, url => $url,
897            Size => $bytes, size => $bytes,
898            Asset => $asset, asset => $asset,
899            Height => $h, height => $h,
900            Width => $w, width => $w,
901            Type => 'image', type => 'image',
902            ImageType => $id, image_type => $id,
903            Blog => $blog, blog => $blog);
904    }
905
906    $asset;
907}
908
909sub handle_upload {
910    my $app = shift;
911    my $blog = $app->{blog};
912   
913    my $asset = $app->_upload_to_asset or return;
914
915    my $link = XML::Atom::Link->new;
916    $link->type($asset->mime_type);
917    $link->rel('alternate');
918    $link->href($asset->url);
919    my $atom = XML::Atom::Entry->new;
920    $atom->title($asset->file_name);
921    $atom->add_link($link);
922    $app->response_code(201);
923    $app->response_content_type('application/x.atom+xml');
924    $atom->as_xml;
925}
926
927package MT::AtomServer::Weblog::Legacy;
928use strict;
929
930use base qw( MT::AtomServer::Weblog );
931
932use MT::I18N qw( encode_text );
933use XML::Atom;  # for LIBXML
934use XML::Atom::Feed;
935use MT::Blog;
936use MT::Permission;
937
938use constant NS_CATEGORY => 'http://sixapart.com/atom/category#';
939use constant NS_DC => MT::AtomServer::Weblog->NS_DC();
940
941sub script { $_[0]->{cfg}->AtomScript . '/weblog' }
942
943sub atom_content_type   { 'application/xml' }
944sub atom_x_content_type { 'application/x.atom+xml' }
945
946sub edit_link_rel { 'service.edit' }
947sub get_posts_order_field { 'authored_on' }
948
949sub new_feed {
950    my $app = shift;
951    XML::Atom::Feed->new();
952}
953
954sub new_with_entry {
955    my $app = shift;
956    my ($entry) = @_;
957    MT::Atom::Entry->new_with_entry($entry);
958}
959
960sub apply_basename {}
961
962sub get_weblogs {
963    my $app = shift;
964    my $user = $app->{user};
965    my $iter = $user->is_superuser
966        ? MT::Blog->load_iter()
967        : MT::Permission->load_iter({ author_id => $user->id });
968    my $feed = $app->new_feed();
969    my $base = $app->base . $app->uri;
970    require URI;
971    my $uri = URI->new($base);
972    if ( $uri ) {
973        my $created = MT::Util::format_ts('%Y-%m-%d', $user->created_on, undef, 'en', 0);
974        my $id = 'tag:'.$uri->host.','.$created.':'.$uri->path.'/weblogs-'.$user->id;
975        $feed->id($id);
976    }
977    while (my $thing = $iter->()) {
978        if ($thing->isa('MT::Permission')) {
979            next unless $thing->can_create_post;
980        }
981        my $blog = $thing->isa('MT::Blog') ? $thing
982            : MT::Blog->load($thing->blog_id);
983        next unless $blog;
984        my $uri = $base . '/blog_id=' . $blog->id;
985        my $blogname = encode_text($blog->name . ' #' . $blog->id, undef, 'utf-8');
986        $feed->add_link({ rel => 'service.post', title => $blogname,
987                          href => $uri, type => 'application/x.atom+xml' });
988        $feed->add_link({ rel => 'service.feed', title => $blogname,
989                          href => $uri, type => 'application/x.atom+xml' });
990        $feed->add_link({ rel => 'service.upload', title => $blogname,
991                          href => $uri . '/svc=upload',
992                          type => 'application/x.atom+xml' });
993        $feed->add_link({ rel => 'service.categories', title => $blogname,
994                          href => $uri . '/svc=categories',
995                          type => 'application/x.atom+xml' });
996        $feed->add_link({ rel => 'alternate', title => $blogname,
997                          href => $blog->site_url,
998                          type => 'text/html' });
999    }
1000    $app->response_code(200);
1001    $app->response_content_type('application/x.atom+xml');
1002    $feed->as_xml;
1003}
1004
1005sub get_categories {
1006    my $app = shift;
1007    my $blog = $app->{blog};
1008    my $iter = MT::Category->load_iter({ blog_id => $blog->id });
1009    my $doc;
1010    if (LIBXML) {
1011        $doc = XML::LibXML::Document->createDocument('1.0', 'utf-8');
1012        my $root = $doc->createElementNS(NS_CATEGORY, 'categories');
1013        $doc->setDocumentElement($root);
1014    } else {
1015        $doc = XML::XPath::Node::Element->new('categories');
1016        my $ns = XML::XPath::Node::Namespace->new('#default' => NS_CATEGORY);
1017        $doc->appendNamespace($ns);
1018    }
1019    while (my $cat = $iter->()) {
1020        my $catlabel = encode_text($cat->label, undef, 'utf-8');
1021        if (LIBXML) {
1022            my $elem = $doc->createElementNS(NS_DC, 'subject');
1023            $doc->getDocumentElement->appendChild($elem);
1024            $elem->appendChild(XML::LibXML::Text->new($catlabel));
1025        } else {
1026            my $elem = XML::XPath::Node::Element->new('subject');
1027            my $ns = XML::XPath::Node::Namespace->new('#default' => NS_DC);
1028            $elem->appendNamespace($ns);
1029            $doc->appendChild($elem);
1030            $elem->appendChild(XML::XPath::Node::Text->new($catlabel));
1031        }
1032    }
1033    $app->response_code(200);
1034    $app->response_content_type('application/x.atom+xml');
1035    if (LIBXML) {
1036        $doc->toString(1);
1037    } else {
1038        return '<?xml version="1.0" encoding="utf-8"?>' . "\n" . $doc->toString;
1039    }
1040}
1041
1042package MT::AtomServer::Comments;
1043use strict;
1044
1045use base qw( MT::AtomServer::Weblog );
1046use MT::I18N qw( encode_text );
1047
1048sub script { $_[0]->{cfg}->AtomScript . '/comments' }
1049
1050sub handle_request {
1051    my $app = shift;
1052    $app->authenticate || return;
1053    if (my $svc = $app->{param}{svc}) {
1054        if ($svc eq 'upload') {
1055            return $app->handle_upload;
1056        } elsif ($svc eq 'categories') {
1057            return $app->get_categories;
1058        }
1059    }
1060    my $method = $app->request_method;
1061    if ($method eq 'POST') {
1062#        return $app->new_comment;
1063    } elsif ($method eq 'PUT') {
1064#        return $app->edit_comment;
1065    } elsif ($method eq 'DELETE') {
1066#        return $app->delete_comment;
1067    } elsif ($method eq 'GET') {
1068        if ($app->{param}{comment_id}) {
1069            return $app->get_comment;
1070        } elsif ($app->{param}{entry_id}) {
1071            return $app->get_comments;
1072        } else {
1073            return $app->get_blog_comments;
1074        }
1075    }
1076}
1077
1078sub new_with_comment {
1079    my $app = shift;
1080    my ($comment) = @_;
1081    my $atom = MT::Atom::Entry->new_with_comment( $comment, Version => 1.0 );
1082
1083    my $mo = MT::Atom::Entry::_create_issued(
1084        $comment->modified_on || $comment->created_on, $comment->blog);
1085    $atom->set(MT::AtomServer::Weblog::NS_APP(), 'edited', $mo);
1086
1087    $atom;
1088}
1089
1090sub get_comment {
1091    my $app = shift;
1092    my $blog = $app->{blog};
1093    my $comment_id = $app->{param}{comment_id}
1094        or return $app->error(400, "No comment_id");
1095    my $comment = MT::Comment->load($comment_id)
1096        or return $app->error(400, "Invalid comment_id");
1097    my $entry = $comment->entry;
1098    my $uri = $app->base . $app->uri . '/blog_id=' . $blog->id;
1099    my $c = $app->new_with_comment($comment);
1100    $c->add_link({ rel => 'self', type => $app->atom_x_content_type,
1101                   href => $uri . '/comment_id=' . $comment->id });
1102    # feed/updated should be added before entries
1103    # so we postpone adding them until later
1104    $c->set('http://purl.org/syndication/thread/1.0', 'in-reply-to',
1105        undef,
1106        { ref => $entry->atom_id,
1107            type => 'text/html',
1108            href => $entry->permalink } );
1109    $app->run_callbacks( 'get_comment', $c, $comment );
1110    $app->response_content_type($app->atom_content_type);
1111    $c->as_xml;
1112}
1113
1114sub get_blog_comments {
1115    my $app = shift;
1116    my $blog = $app->{blog};
1117    my %terms = (blog_id => $blog->id, visible => 1);
1118    my %arg = (sort => $app->get_posts_order_field, direction => 'descend');
1119    $arg{limit}  = $app->{param}{limit}  || 21;
1120    $arg{offset} = $app->{param}{offset} || 0;
1121
1122    my $feed = $app->new_feed();
1123    my $uri = $app->base . $app->uri . '/blog_id=' . $blog->id;
1124    my $blogname = encode_text($blog->name, undef, 'utf-8');
1125    $feed->add_link({ rel => 'alternate', type => 'text/html',
1126                      href => $blog->site_url });
1127    $feed->add_link({ rel => 'self', type => $app->atom_x_content_type,
1128                      href => $uri });
1129    $feed->title($blogname);
1130
1131    require URI;
1132    my $site_uri = URI->new($blog->site_url);
1133    if ( $site_uri ) {
1134        my $blog_created = MT::Util::format_ts('%Y-%m-%d', $blog->created_on, $blog, 'en', 0);
1135        my $id = 'tag:'.$site_uri->host.','.$blog_created.':'.$site_uri->path.'/'.$blog->id;
1136        $feed->id($id);
1137    }
1138    $app->_comments_in_atom($feed, \%terms, \%arg);
1139    $app->run_callbacks( 'get_blog_comments', $feed, $blog );
1140    ## xxx add next/prev links
1141    $app->response_content_type($app->atom_content_type);
1142    $feed->as_xml;
1143}
1144
1145sub get_comments {
1146    my $app = shift;
1147    my $blog = $app->{blog};
1148    my $entry_id = $app->{param}{entry_id}
1149        or return $app->error(400, "No entry_id");
1150    my $entry = MT::Entry->load($entry_id)
1151        or return $app->error(400, "Invalid entry_id");
1152    my %terms = (blog_id => $blog->id, entry_id => $entry->id, visible => 1);
1153    my %arg = (sort => $app->get_posts_order_field, direction => 'descend');
1154    $arg{limit}  = $app->{param}{limit}  || 21;
1155    $arg{offset} = $app->{param}{offset} || 0;
1156
1157    my $feed = $app->new_feed();
1158    my $uri = $app->base . $app->uri . '/blog_id=' . $blog->id;
1159    my $blogname = encode_text($blog->name, undef, 'utf-8');
1160    $feed->add_link({ rel => 'alternate', type => 'text/html',
1161                      href => $entry->permalink });
1162    $feed->add_link({ rel => 'self', type => $app->atom_x_content_type,
1163                      href => $uri . '/entry_id=' . $entry->id });
1164    $feed->title($entry->title);
1165    $feed->id($entry->atom_id . '/comments');
1166    $app->_comments_in_atom($feed, \%terms, \%arg);
1167    $app->run_callbacks( 'get_comments', $feed, $entry );
1168    ## xxx add next/prev links
1169    $app->response_content_type($app->atom_content_type);
1170    $feed->as_xml;
1171}
1172
1173sub _comments_in_atom {
1174    my $app = shift;
1175    my ( $feed, $terms, $args ) = @_;
1176    require MT::Comment;
1177    my $iter = MT::Comment->load_iter($terms, $args);
1178    my $latest_date = 0;
1179    my @comments;
1180    while (my $comment = $iter->()) {
1181        my $c = $app->new_with_comment($comment);
1182        # feed/updated should be added before entries
1183        # so we postpone adding them until later
1184        my $entry = $comment->entry;
1185        $c->set('http://purl.org/syndication/thread/1.0', 'in-reply-to',
1186            undef,
1187            { ref => $entry->atom_id,
1188              type => 'text/html',
1189              href => $entry->permalink } );
1190        push @comments, $c;
1191        my $date = $comment->modified_on || $comment->created_on;
1192        if ( $latest_date < $date ) {
1193            $latest_date = $date;
1194            $feed->updated( $c->updated );
1195        }
1196    }
1197    $feed->add_entry($_) foreach @comments;
1198    $feed;
1199}
1200
1201
12021;
1203__END__
1204
1205=head1 NAME
1206
1207MT::AtomServer
1208
1209=head1 SYNOPSIS
1210
1211An Atom Publishing API interface for communicating with Movable Type.
1212
1213=head1 METHODS
1214
1215=head2 $app->xml_body()
1216
1217Takes the content posted to the server and parses it into an XML document.
1218Uses either XML::LibXML or XML::XPath depending on which is available.
1219
1220=head2 $app->iso2epoch($iso_ts)
1221
1222Converts C<$iso_ts> in the format of an ISO timestamp into a unix timestamp
1223(seconds since the epoch).
1224
1225=head2 $app->init
1226
1227Initializes the application.
1228
1229=head2 $app->get_auth_info
1230
1231Processes the request for WSSE authentication and returns a hash containing:
1232
1233=over 4
1234
1235=item * Username
1236
1237=item * PasswordDigest
1238
1239=item * Nonce
1240
1241=item * Created
1242
1243=back
1244
1245=head2 $app->handle_request
1246
1247The implementation of this in I<MT::AtomServer::Weblog> passes the request
1248to the proper method.
1249
1250=head2 $app->handle
1251
1252Wrapper method that determines the proper AtomServer package to pass the
1253request to.
1254
1255=head2 $app->iso2ts($iso_ts, $target_zone)
1256
1257Converts C<$iso_ts> in the format of an ISO timestamp into a MT-compatible
1258timestamp (YYYYMMDDHHMMSS) for the specified timezone C<$target_zone>.
1259
1260=head2 $app->atom_body
1261
1262Processes the request as Atom content and returns an XML::Atom object.
1263
1264=head2 $app->error($code, $message)
1265
1266Sends the HTTP headers necessary to relay an error.
1267
1268=head2 $app->authenticate()
1269
1270Checks the WSSE authentication with the local MT user database and
1271confirms the user is authorized to access the resources required by
1272the request.
1273
1274=head2 $app->show_error($message)
1275
1276Returns an XML wrapper for the error response.
1277
1278=head2 $app->auth_failure($code, $message)
1279
1280Handles the response in the event of an authentication failure.
1281
1282=head1 CALLBACKS
1283
1284=over 4
1285
1286=item api_pre_save.entry
1287
1288    callback($eh, $app, $entry, $original_entry)
1289
1290Called before saving a new or existing entry. If saving a new entry, the
1291$original_entry will have an unassigned 'id'. This callback is executed
1292as a filter, so your handler must return 1 to allow the entry to be saved.
1293
1294=item api_post_save.entry
1295
1296    callback($eh, $app, $entry, $original_entry)
1297
1298Called after saving a new or existing entry. If saving a new entry, the
1299$original_entry will have an unassigned 'id'.
1300
1301=item get_posts
1302
1303    callback($eh, $app, $feed, $blog)
1304
1305Called right before get_posts method returns atom feed response.
1306I<$feed> is a reference to XML::Atom::Feed object.
1307I<$blog> is a reference to the requested MT::Blog object.
1308
1309=item get_post
1310
1311    callback($eh, $app, $atom_entry, $entry)
1312
1313Called right before get_post method returns atom entry response.
1314I<$atom_entry> is a reference to XML::Atom::Entry object.
1315I<$entry> is a reference to the requested MT::Entry object.
1316
1317=item get_blog_comments
1318
1319    callback($eh, $app, $feed, $blog)
1320
1321Called right before get_blog_comments method returns atom feed response.
1322I<$feed> is a reference to XML::Atom::Feed object.
1323I<$blog> is a reference to the requested MT::Blog object.
1324
1325=item get_comments
1326
1327    callback($eh, $app, $feed, $entry)
1328
1329Called right before get_comments method returns atom feed response.
1330I<$feed> is a reference to XML::Atom::Feed object.
1331I<$entry> is a reference to the requested MT::Entry object.
1332
1333=item get_comment
1334
1335    callback($eh, $app, $atom_entry, $comment)
1336
1337Called right before get_comment method returns atom entry response.
1338I<$atom_entry> is a reference to XML::Atom::Entry object.
1339I<$comment> is a reference to the requested MT::Comment object.
1340
1341=back
1342
1343=cut
Note: See TracBrowser for help on using the browser.