root/branches/athena/lib/MT/AtomServer.pm @ 1092

Revision 1092, 29.3 kB (checked in by hachi, 2 years ago)

Merging release-15 to athena branch. svn merge -r59987:60375 http://svn.sixapart.com/repos/eng/movabletype/branches/release-15 .

  • Property svn:keywords set to Author Date Id Revision
Line 
1# Copyright 2001-2007 Six Apart. This code cannot be redistributed without
2# permission from www.sixapart.com.  For more information, consult your
3# Movable Type license.
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        $app->response_code($code);
91        $app->response_message($msg);
92    }
93    elsif ($code) {
94        return $app->SUPER::error($code);
95    }
96    return undef;
97}
98
99sub show_error {
100    my $app = shift;
101    my($err) = @_;
102    chomp($err = encode_xml($err));
103    if ($app->{is_soap}) {
104        my $code = $app->response_code;
105        if ($code >= 400) {
106            $app->response_code(500);
107            $app->response_message($err);
108        }
109        return <<FAULT;
110<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
111  <soap:Body>
112    <soap:Fault>
113      <faultcode>$code</faultcode>
114      <faultstring>$err</faultstring>
115    </soap:Fault>
116  </soap:Body>
117</soap:Envelope>
118FAULT
119    } else {
120        return <<ERR;
121<error>$err</error>
122ERR
123    }
124}
125
126sub get_auth_info {
127    my $app = shift;
128    my %param;
129    if ($app->{is_soap}) {
130        my $xml = $app->xml_body;
131        my $auth = first($xml, NS_WSSE, 'UsernameToken');
132        $param{Username} = textValue($auth, NS_WSSE, 'Username');
133        $param{PasswordDigest} = textValue($auth, NS_WSSE, 'Password');
134        $param{Nonce} = textValue($auth, NS_WSSE, 'Nonce');
135        $param{Created} = textValue($auth, NS_WSU, 'Created');
136    } else {
137        my $req = $app->get_header('X-WSSE')
138            or return $app->auth_failure(401, 'X-WSSE authentication required');
139        $req =~ s/^WSSE //;
140        my ($profile);
141        ($profile, $req) = $req =~ /(\S+),?\s+(.*)/;
142        return $app->error(400, "Unsupported WSSE authentication profile") 
143            if $profile !~ /\bUsernameToken\b/i;
144        for my $i (split /,\s*/, $req) {
145            my($k, $v) = split /=/, $i, 2;
146            $v =~ s/^"//;
147            $v =~ s/"$//;
148            $param{$k} = $v;
149        }
150    }
151    \%param;
152}
153
154sub authenticate {
155    my $app = shift;
156    my $auth = $app->get_auth_info
157        or return $app->auth_failure(400, "No authentication info");
158    for my $f (qw( Username PasswordDigest Nonce Created )) {
159        return $app->auth_failure(400, "X-WSSE requires $f")
160            unless $auth->{$f};
161    }
162    require MT::Session;
163    my $nonce_record = MT::Session->load($auth->{Nonce});
164   
165    if ($nonce_record && $nonce_record->id eq $auth->{Nonce}) {
166        return $app->auth_failure(403, "Nonce already used");
167    }
168    $nonce_record = new MT::Session();
169    $nonce_record->set_values({
170        id => $auth->{Nonce},
171        start => time,
172        kind => 'AN'
173    });
174    $nonce_record->save();
175# xxx Expire sessions on shorter timeout?
176    my $enc = $app->config('PublishCharset');
177    my $username = encode_text($auth->{Username},undef,$enc);
178    my $user = MT::Author->load({ name => $username, type => 1 })
179        or return $app->auth_failure(403, 'Invalid login');
180    return $app->auth_failure(403, 'Invalid login')
181        unless $user->api_password;
182    return $app->auth_failure(403, 'Invalid login')
183        unless $user->is_active;
184    my $created_on_epoch = $app->iso2epoch($auth->{Created});
185    if (abs(time - $created_on_epoch) > $app->config('WSSETimeout')) {
186        return $app->auth_failure(403, 'X-WSSE UsernameToken timed out');
187    }
188    $auth->{Nonce} = MIME::Base64::decode_base64($auth->{Nonce});
189    my $expected = Digest::SHA1::sha1_base64(
190         $auth->{Nonce} . $auth->{Created} . $user->api_password);
191    # Some base64 implementors do it wrong and don't put the =
192    # padding on the end. This should protect us against that without
193    # creating any holes.
194    $expected =~ s/=*$//;
195    $auth->{PasswordDigest} =~ s/=*$//;
196    #print STDERR "expected $expected and got " . $auth->{PasswordDigest} . "\n";
197    return $app->auth_failure(403, 'X-WSSE PasswordDigest is incorrect')
198        unless $expected eq $auth->{PasswordDigest};
199    $app->{user} = $user;
200
201    ## update session so the user will be counted as active
202    require MT::Session;
203    my $sess_active = MT::Session->load( { kind => 'UA', name => $user->id } );
204    if (!$sess_active) {
205        $sess_active = MT::Session->new;
206        $sess_active->id($app->make_magic_token());
207        $sess_active->kind('UA'); # UA == User Activation
208        $sess_active->name($user->id);
209    }
210    $sess_active->start(time);
211    $sess_active->save;
212    return 1;
213}
214
215sub auth_failure {
216    my $app = shift;
217    $app->set_header('WWW-Authenticate', 'WSSE profile="UsernameToken"');
218    return $app->error(@_);
219}
220
221sub xml_body {
222    my $app = shift;
223    unless (exists $app->{xml_body}) {
224        if (LIBXML) {
225            my $parser = XML::LibXML->new;
226            $app->{xml_body} = $parser->parse_string($app->request_content);
227        } else {
228            my $xp = XML::XPath->new(xml => $app->request_content);
229            $app->{xml_body} = ($xp->find('/')->get_nodelist)[0];
230        }
231    }
232    $app->{xml_body};
233}
234
235sub atom_body {
236    my $app = shift;
237    my $atom;
238    if ($app->{is_soap}) {
239        my $xml = $app->xml_body;
240        $atom = MT::Atom::Entry->new(Elem => first($xml, NS_SOAP, 'Body'))
241            or return $app->error(500, MT::Atom::Entry->errstr);
242    } else {
243        $atom = MT::Atom::Entry->new(Stream => \$app->request_content)
244            or return $app->error(500, MT::Atom::Entry->errstr);
245    }
246    $atom;
247}
248
249# $target_zone is expected to be a number of hours from GMT
250sub iso2ts {
251    my $app = shift;
252    my($ts, $target_zone) = @_;
253    return unless $ts =~ /^(\d{4})(?:-?(\d{2})(?:-?(\d\d?)(?:T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|([+-]\d{2}:\d{2}))?)?)?)?/;
254    my($y, $mo, $d, $h, $m, $s, $zone) =
255        ($1, $2 || 1, $3 || 1, $4 || 0, $5 || 0, $6 || 0, $7);
256    if ($zone) {
257        my ($zh, $zm) = $zone =~ /([+-]\d\d):(\d\d)/;
258        use Time::Local qw( timegm );
259        my $ts = timegm( $s, $m, $h, $d, $mo - 1, $y - 1900 );
260        if ($zone ne 'Z') {
261            require MT::DateTime;
262            my $tz_secs = MT::DateTime->tz_offset_as_seconds($zone);
263            $ts -= $tz_secs;
264        }
265        if ($target_zone) {
266            my $tz_secs = (3600 * int($target_zone) + 
267                           60 * abs($target_zone - int($target_zone)));
268            $ts += $tz_secs;
269        }
270        ($s, $m, $h, $d, $mo, $y) = gmtime( $ts );
271        $y += 1900; $mo++;
272    }
273    sprintf("%04d%02d%02d%02d%02d%02d", $y, $mo, $d, $h, $m, $s);
274}
275
276sub iso2epoch {
277    my $app = shift;
278    my($ts) = @_;
279    return unless $ts =~ /^(\d{4})(?:-?(\d{2})(?:-?(\d\d?)(?:T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|([+-]\d{2}:\d{2}))?)?)?)?/;
280    my($y, $mo, $d, $h, $m, $s, $zone) =
281        ($1, $2 || 1, $3 || 1, $4 || 0, $5 || 0, $6 || 0, $7);
282
283    use Time::Local;
284    my $dt = timegm($s, $m, $h, $d, $mo-1, $y);
285    if ($zone && $zone ne 'Z') {
286        require MT::DateTime;
287        my $tz_secs = MT::DateTime->tz_offset_as_seconds($zone);
288        $dt -= $tz_secs;
289    }
290    $dt;
291}
292
293package MT::AtomServer::Weblog;
294use strict;
295
296use MT::I18N qw( encode_text );
297use XML::Atom;
298use XML::Atom::Feed;
299use base qw( MT::AtomServer );
300use MT::Blog;
301use MT::Entry;
302use MT::Util qw( encode_xml );
303use MT::Permission;
304use File::Spec;
305use File::Basename;
306
307use constant NS_CATEGORY => 'http://sixapart.com/atom/category#';
308use constant NS_DC => 'http://purl.org/dc/elements/1.1/';
309use constant NS_PHOTOS => 'http://sixapart.com/atom/photo#';
310use constant NS_TYPEPAD => 'http://sixapart.com/atom/typepad#';
311
312sub script { $_[0]->{cfg}->AtomScript . '/weblog' }
313
314sub handle_request {
315    my $app = shift;
316    $app->authenticate || return;
317    if (my $svc = $app->{param}{svc}) {
318        if ($svc eq 'upload') {
319            return $app->handle_upload;
320        } elsif ($svc eq 'categories') {
321            return $app->get_categories;
322        }
323    }
324    my $method = $app->request_method;
325    if ($method eq 'POST') {
326        return $app->new_post;
327    } elsif ($method eq 'PUT') {
328        return $app->edit_post;
329    } elsif ($method eq 'DELETE') {
330        return $app->delete_post;
331    } elsif ($method eq 'GET') {
332        if ($app->{param}{entry_id}) {
333            return $app->get_post;
334        } elsif ($app->{param}{blog_id}) {
335            return $app->get_posts;
336        } else {
337            return $app->get_weblogs;
338        }
339    }
340}
341
342sub authenticate {
343    my $app = shift;
344
345    $app->SUPER::authenticate or return;
346    if (my $blog_id = $app->{param}{blog_id}) {
347        $app->{blog} = MT::Blog->load($blog_id)
348            or return $app->error(400, "Invalid blog ID '$blog_id'");
349        $app->{user} 
350            or return $app->error(403, "Authenticate");
351        if ($app->{user}->is_superuser()) {
352            $app->{perms} = new MT::Permission;
353            $app->{perms}->blog_id($blog_id);
354            $app->{perms}->author_id($app->{user}->id);
355            $app->{perms}->can_administer_blog(1);
356            return 1;
357        }
358        my $perms = $app->{perms} = MT::Permission->load({
359                    author_id => $app->{user}->id,
360                    blog_id => $app->{blog}->id });
361        return $app->error(403, "No permissions") unless $perms && $perms->can_post;
362    }
363    1;
364}
365
366sub publish {
367    my $app = shift;
368    my($entry, $no_ping) = @_;
369    my $blog = MT::Blog->load($entry->blog_id);
370    $app->rebuild_entry( Entry => $entry, Blog => $blog,
371                         BuildDependencies => 1 ) or return;
372    unless ($no_ping) {
373        $app->ping_and_save( Entry => $entry, Blog => $blog )
374            or return;
375    }
376    1;
377}
378
379sub get_weblogs {
380    my $app = shift;
381    my $user = $app->{user};
382    my $iter = $user->is_superuser
383        ? MT::Blog->load_iter()
384        : MT::Permission->load_iter({ author_id => $user->id });
385    my $feed = XML::Atom::Feed->new;
386    my $base = $app->base . $app->uri;
387    while (my $thing = $iter->()) {
388        if ($thing->isa('MT::Permission')) {
389            next unless $thing->can_post;
390        }
391        my $blog = $thing->isa('MT::Blog') ? $thing
392            : MT::Blog->load($thing->blog_id);
393        my $uri = $base . '/blog_id=' . $blog->id;
394        my $blogname = encode_text($blog->name . ' #' . $blog->id, undef, 'utf-8');
395        $feed->add_link({ rel => 'service.post', title => $blogname,
396                          href => $uri, type => 'application/x.atom+xml' });
397        $feed->add_link({ rel => 'service.feed', title => $blogname,
398                          href => $uri, type => 'application/x.atom+xml' });
399        $feed->add_link({ rel => 'service.upload', title => $blogname,
400                          href => $uri . '/svc=upload',
401                          type => 'application/x.atom+xml' });
402        $feed->add_link({ rel => 'service.categories', title => $blogname,
403                          href => $uri . '/svc=categories',
404                          type => 'application/x.atom+xml' });
405        $feed->add_link({ rel => 'alternate', title => $blogname,
406                          href => $blog->site_url,
407                          type => 'text/html' });
408    }
409    $app->response_code(200);
410    $app->response_content_type('application/x.atom+xml');
411    $feed->as_xml;
412}
413
414sub new_post {
415    my $app = shift;
416    my $atom = $app->atom_body or return $app->error(500, "No body!");
417    my $blog = $app->{blog};
418    my $user = $app->{user};
419    my $enc = $app->config('PublishCharset');
420    ## Check for category in dc:subject. We will save it later if
421    ## it's present, but we want to give an error now if necessary.
422    my($cat);
423    if (my $label = $atom->get(NS_DC, 'subject')) {
424        my $label_enc = encode_text($label,'utf-8',$enc);
425        $cat = MT::Category->load({ blog_id => $blog->id, label => $label_enc })
426            or return $app->error(400, "Invalid category '$label'");
427    }
428
429    my $content = $atom->content;
430    my $type = $content->type; 
431    my $body = encode_text(MT::I18N::utf8_off($content->body),'utf-8',$enc); 
432    my $asset;
433    if ($type && $type !~ m!^application/.*xml$!) {
434        if ($type !~ m!^text/!) {
435            $asset = $app->_upload_to_asset or return;
436        }
437        elsif ($type && $type eq 'text/plain') {
438            ## Check for LifeBlog Note & SMS records.
439            my $format = $atom->get(NS_DC, 'format');
440            if ($format && ($format eq 'Note' || $format eq 'SMS')) {
441                $asset = $app->_upload_to_asset or return;
442            }
443        }
444    }
445    if ( $atom->get(NS_TYPEPAD, 'standalone') && $asset ) {
446        $app->response_code(201);
447        $app->response_content_type('application/atom_xml');
448        my $a = MT::Atom::Entry->new_with_asset($asset);
449        return $a->as_xml; 
450    } 
451
452    my $entry = MT::Entry->new;
453    my $orig_entry = $entry->clone;
454    $entry->blog_id($blog->id);
455    $entry->author_id($user->id);
456    $entry->created_by($user->id);
457    $entry->status(MT::Entry::RELEASE());
458    $entry->allow_comments($blog->allow_comments_default);
459    $entry->allow_pings($blog->allow_pings_default);
460    $entry->convert_breaks($blog->convert_paras);
461    $entry->title(encode_text($atom->title,'utf-8',$enc));
462    $entry->text(encode_text(MT::I18N::utf8_off($atom->content()->body()),'utf-8',$enc));
463    $entry->excerpt(encode_text($atom->summary,'utf-8',$enc));
464    if (my $iso = $atom->issued) {
465        my $pub_ts = MT::Util::iso2ts($blog, $iso);
466        my @ts = MT::Util::offset_time_list(time, $blog->id);
467        my $ts = sprintf '%04d%02d%02d%02d%02d%02d',
468            $ts[5]+1900, $ts[4]+1, @ts[3,2,1,0];
469        $entry->authored_on($pub_ts);
470        if ($pub_ts > $ts) {
471            $entry->status(MT::Entry::FUTURE())
472        }
473    }
474## xxx mt/typepad-specific fields
475    $entry->discover_tb_from_entry();
476
477    if (my @link = $atom->link) {
478        my $i = 0;
479        my $img_html = '';
480        my $num_links = scalar @link;
481        for my $link (@link) {
482            next unless $link->rel eq 'related';
483            my($asset_id) = $link->href =~ /asset\-(\d+)$/;
484            if ($asset_id) {
485                require MT::Asset;
486                my $a = MT::Asset->load($asset_id);
487                next unless $a;
488                my $pkg = MT::Asset->handler_for_file($a->file_name);
489                my $asset = bless $a, $pkg;
490                $img_html .= $asset->as_html({ include => 1 });
491            }
492        }
493        if ($img_html) {
494            $img_html .= qq{<br style="clear: left;" />\n\n};
495            $entry->text($img_html . $body);
496        }
497    }
498
499    MT->run_callbacks('api_pre_save.entry', $app, $entry, $orig_entry)
500        or return $app->error(500, MT->translate("PreSave failed [_1]", MT->errstr));
501
502    $entry->save or return $app->error(500, $entry->errstr);
503
504    require MT::Log;
505    $app->log({
506        message => $app->translate("User '[_1]' (user #[_2]) added entry #[_3]", $user->name, $user->id, $entry->id),
507        level => MT::Log::INFO(),
508        class => 'entry',
509        category => 'new',
510        metadata => $entry->id
511    });
512    ## Save category, if present.
513    if ($cat) {
514        my $place = MT::Placement->new;
515        $place->is_primary(1);
516        $place->entry_id($entry->id);
517        $place->blog_id($blog->id);
518        $place->category_id($cat->id);
519        $place->save or return $app->error(500, $place->errstr);
520    }
521
522    MT->run_callbacks('api_post_save.entry', $app, $entry, $orig_entry);
523
524    $app->publish($entry);
525    $app->response_code(201);
526    $app->response_content_type('application/atom+xml');
527        my $edit_uri = $app->base . $app->uri . '/blog_id=' . $entry->blog_id . '/entry_id=' . $entry->id;
528    $app->set_header('Location', $edit_uri);
529    $atom = MT::Atom::Entry->new_with_entry($entry);
530    $atom->add_link({ rel => 'service.edit',
531                      href => $edit_uri,
532                      type => 'application/atom+xml',
533                      title => $entry->title });
534    $atom->as_xml;
535}
536
537sub edit_post {
538    my $app = shift;
539    my $atom = $app->atom_body or return;
540    my $blog = $app->{blog};
541    my $enc = $app->config('PublishCharset');
542    my $entry_id = $app->{param}{entry_id}
543        or return $app->error(400, "No entry_id");
544    my $entry = MT::Entry->load($entry_id)
545        or return $app->error(400, "Invalid entry_id");
546    return $app->error(403, "Access denied")
547        unless $app->{perms}->can_edit_entry($entry, $app->{user});
548    my $orig_entry = $entry->clone;
549    $entry->title(encode_text($atom->title,'utf-8',$enc));
550    $entry->text(encode_text(MT::I18N::utf8_off($atom->content()->body()),'utf-8',$enc));
551    $entry->excerpt(encode_text($atom->summary,'utf-8',$enc));
552    $entry->modified_by($app->{user}->id);
553    if (my $iso = $atom->issued) {
554        my $pub_ts = MT::Util::iso2ts($blog, $iso);
555        my @ts = MT::Util::offset_time_list(time, $blog->id);
556        my $ts = sprintf '%04d%02d%02d%02d%02d%02d',
557            $ts[5]+1900, $ts[4]+1, @ts[3,2,1,0];
558        $entry->authored_on($pub_ts);
559        if ($pub_ts > $ts) {
560            $entry->status(MT::Entry::FUTURE())
561        }
562    }
563## xxx mt/typepad-specific fields
564    $entry->discover_tb_from_entry();
565
566    MT->run_callbacks('api_pre_save.entry', $app, $entry, $orig_entry)
567        or return $app->error(500, MT->translate("PreSave failed [_1]", MT->errstr));
568
569    $entry->save or return $app->error(500, "Entry not saved");
570
571    require MT::Log;
572    $app->log({
573        message => $app->translate("User '[_1]' (user #[_2]) edited entry #[_3]", $app->{user}->name, $app->{user}->id, $entry->id),
574        level => MT::Log::INFO(),
575        class => 'entry',
576        category => 'new',
577        metadata => $entry->id
578    });
579
580    MT->run_callbacks('api_post_save.entry', $app, $entry, $orig_entry);
581
582    if ($entry->status == MT::Entry::RELEASE()) {
583        $app->publish($entry) or return $app->error(500, "Entry not published");
584    }
585    $app->response_code(200);
586    $app->response_content_type('application/xml');
587    $atom = MT::Atom::Entry->new_with_entry($entry);
588    $atom->as_xml;
589}
590
591sub get_posts {
592    my $app = shift;
593    my $blog = $app->{blog};
594    my %terms = (blog_id => $blog->id);
595    my %arg = (sort => 'authored_on', direction => 'descend');
596    my $Limit = 20;
597    $arg{limit} = $Limit + 1;
598    $arg{offset} = $app->{param}{offset} || 0;
599    my $iter = MT::Entry->load_iter(\%terms, \%arg);
600    my $feed = XML::Atom::Feed->new;
601    my $uri = $app->base . $app->uri . '/blog_id=' . $blog->id;
602    my $blogname = encode_text($blog->name, undef, 'utf-8');
603    $feed->add_link({ rel => 'alternate', type => 'text/html',
604                      href => $blog->site_url });
605    $feed->title($blogname);
606    $feed->add_link({ rel => 'service.post', type => 'application/x.atom+xml',
607                      href => $uri, title => $blogname });
608    $uri .= '/entry_id=';
609    while (my $entry = $iter->()) {
610        my $e = MT::Atom::Entry->new_with_entry($entry);
611        $e->add_link({ rel => 'service.edit', type => 'application/x.atom+xml',
612                       href => ($uri . $entry->id), title => encode_text($entry->title, undef,'utf-8') });
613        $feed->add_entry($e);
614    }
615    ## xxx add next/prev links
616    $app->response_content_type('application/xml');
617    $feed->as_xml;
618}
619
620sub get_post {
621    my $app = shift;
622    my $blog = $app->{blog};
623    my $entry_id = $app->{param}{entry_id}
624        or return $app->error(400, "No entry_id");
625    my $entry = MT::Entry->load($entry_id)
626        or return $app->error(400, "Invalid entry_id");
627    return $app->error(403, "Access denied")
628        unless $app->{perms}->can_edit_entry($entry, $app->{user});
629    $app->response_content_type('application/xml');
630    my $atom = MT::Atom::Entry->new_with_entry($entry);
631    $atom->as_xml;
632}
633
634sub delete_post {
635    my $app = shift;
636    my $blog = $app->{blog};
637    my $entry_id = $app->{param}{entry_id}
638        or return $app->error(400, "No entry_id");
639    my $entry = MT::Entry->load($entry_id)
640        or return $app->error(400, "Invalid entry_id");
641    return $app->error(403, "Access denied")
642        unless $app->{perms}->can_edit_entry($entry, $app->{user});
643    $entry->remove
644        or return $app->error(500, $entry->errstr);
645    $app->publish($entry, 1) or return $app->error(500, $app->errstr);
646    '';
647}
648
649sub _upload_to_asset {
650    my $app = shift;
651    my $atom = $app->atom_body or return;
652    my $blog = $app->{blog};
653    my $user = $app->{user};
654    my %MIME2EXT = (
655        'text/plain'         => '.txt',
656            'image/jpeg'         => '.jpg',
657        'video/3gpp'         => '.3gp',
658        'application/x-mpeg' => '.mpg',
659        'video/mp4'          => '.mp4',
660        'video/quicktime'    => '.mov',
661        'audio/mpeg'         => '.mp3',
662        'audio/x-wav'        => '.wav',
663        'audio/ogg'          => '.ogg',
664        'audio/ogg-vorbis'   => '.ogg',
665    );
666
667    return $app->error(403, "Access denied") unless $app->{perms}->can_upload;
668    my $content = $atom->content;
669    my $type = $content->type
670        or return $app->error(400, "content \@type is required");
671    my $fname = $atom->title or return $app->error(400, "title is required");
672    $fname = basename($fname);
673    return $app->error(400, "Invalid or empty filename")
674        if $fname =~ m!/|\.\.|\0|\|!;
675
676    my $local_relative = File::Spec->catfile('%r', $fname);
677    my $local = File::Spec->catfile($blog->site_path, $fname);
678    my $fmgr = $blog->file_mgr;
679    my($base, $path, $ext) = File::Basename::fileparse($local, '\.[^\.]*');
680    $ext = $MIME2EXT{$type} unless $ext;
681    my $base_copy = $base;
682    my $ext_copy = $ext;
683    $ext_copy =~ s/\.//;
684    my $i = 1;
685    while ($fmgr->exists($path . $base . $ext)) {
686        $base = $base_copy . '_' . $i++;
687    }
688    $local = $path . $base . $ext;
689    my $data = $content->body;
690    defined(my $bytes = $fmgr->put_data($data, $local, 'upload'))
691        or return $app->error(500, "Error writing uploaded file");
692
693    eval { require Image::Size; };
694    return $app->error(500, MT->translate("Perl module Image::Size is required to determine width and height of uploaded images.")) if $@;
695    my ( $w, $h, $id ) = Image::Size::imgsize($local);
696
697    require MT::Asset;
698    my $asset_pkg = MT::Asset->handler_for_file($local);
699    my $is_image  = defined($w)
700      && defined($h)
701      && $asset_pkg->isa('MT::Asset::Image');
702    my $asset;
703    if (!($asset = $asset_pkg->load(
704                { file_path => $local, blog_id => $blog->id })))
705    {
706        $asset = $asset_pkg->new();
707        $asset->file_path($local_relative);
708        $asset->file_name($base.$ext);
709        $asset->file_ext($ext_copy);
710        $asset->blog_id($blog->id);
711        $asset->created_by( $user->id );
712    }
713    else {
714        $asset->modified_by( $user->id );
715    }
716    my $original = $asset->clone;
717    my $url = '%r/' . $base . $ext;
718    $asset->url($url);
719    if ($is_image) {
720        $asset->image_width($w);
721        $asset->image_height($h);
722    }
723    $asset->mime_type($type);
724    $asset->save;
725
726    MT->run_callbacks(
727        'api_upload_file.' . $asset->class,
728        File => $local, file => $local,
729        Url => $url, url => $url,
730        Size => $bytes, size => $bytes,
731        Asset => $asset, asset => $asset,
732        Type => $asset->class, type => $asset->class,
733        Blog => $blog, blog => $blog);
734    if ($is_image) {
735        MT->run_callbacks(
736            'api_upload_image',
737            File => $local, file => $local,
738            Url => $url, url => $url,
739            Size => $bytes, size => $bytes,
740            Asset => $asset, asset => $asset,
741            Height => $h, height => $h,
742            Width => $w, width => $w,
743            Type => 'image', type => 'image',
744            ImageType => $id, image_type => $id,
745            Blog => $blog, blog => $blog);
746    }
747
748    $asset;
749}
750
751sub handle_upload {
752    my $app = shift;
753    my $blog = $app->{blog};
754   
755    my $asset = $app->_upload_to_asset or return;
756
757    my $link = XML::Atom::Link->new;
758    $link->type($asset->mime_type);
759    $link->rel('alternate');
760    $link->href($asset->url);
761    my $atom = XML::Atom::Entry->new;
762    $atom->title($asset->file_name);
763    $atom->add_link($link);
764    $app->response_code(201);
765    $app->response_content_type('application/x.atom+xml');
766    $atom->as_xml;
767}
768
769sub get_categories {
770    my $app = shift;
771    my $blog = $app->{blog};
772    my $iter = MT::Category->load_iter({ blog_id => $blog->id });
773    my $doc;
774    if (LIBXML) {
775        $doc = XML::LibXML::Document->createDocument('1.0', 'utf-8');
776        my $root = $doc->createElementNS(NS_CATEGORY, 'categories');
777        $doc->setDocumentElement($root);
778    } else {
779        $doc = XML::XPath::Node::Element->new('categories');
780        my $ns = XML::XPath::Node::Namespace->new('#default' => NS_CATEGORY);
781        $doc->appendNamespace($ns);
782    }
783    while (my $cat = $iter->()) {
784        my $catlabel = encode_text($cat->label, undef, 'utf-8');
785        if (LIBXML) {
786            my $elem = $doc->createElementNS(NS_DC, 'subject');
787            $doc->getDocumentElement->appendChild($elem);
788            $elem->appendChild(XML::LibXML::Text->new($catlabel));
789        } else {
790            my $elem = XML::XPath::Node::Element->new('subject');
791            my $ns = XML::XPath::Node::Namespace->new('#default' => NS_DC);
792            $elem->appendNamespace($ns);
793            $doc->appendChild($elem);
794            $elem->appendChild(XML::XPath::Node::Text->new($catlabel));
795        }
796    }
797    $app->response_code(200);
798    $app->response_content_type('application/x.atom+xml');
799    if (LIBXML) {
800        $doc->toString(1);
801    } else {
802        return '<?xml version="1.0" encoding="utf-8"?>' . "\n" . $doc->toString;
803    }
804}
805
8061;
807__END__
808
809=head1 NAME
810
811MT::AtomServer
812
813=head1 SYNOPSIS
814
815An Atom Publishing API interface for communicating with Movable Type.
816
817=head1 METHODS
818
819=head2 $app->xml_body()
820
821Takes the content posted to the server and parses it into an XML document.
822Uses either XML::LibXML or XML::XPath depending on which is available.
823
824=head2 $app->iso2epoch($iso_ts)
825
826Converts C<$iso_ts> in the format of an ISO timestamp into a unix timestamp
827(seconds since the epoch).
828
829=head2 $app->init
830
831Initializes the application.
832
833=head2 $app->get_auth_info
834
835Processes the request for WSSE authentication and returns a hash containing:
836
837=over 4
838
839=item * Username
840
841=item * PasswordDigest
842
843=item * Nonce
844
845=item * Created
846
847=back
848
849=head2 $app->handle_request
850
851The implementation of this in I<MT::AtomServer::Weblog> passes the request
852to the proper method.
853
854=head2 $app->handle
855
856Wrapper method that determines the proper AtomServer package to pass the
857request to.
858
859=head2 $app->iso2ts($iso_ts, $target_zone)
860
861Converts C<$iso_ts> in the format of an ISO timestamp into a MT-compatible
862timestamp (YYYYMMDDHHMMSS) for the specified timezone C<$target_zone>.
863
864=head2 $app->atom_body
865
866Processes the request as Atom content and returns an XML::Atom object.
867
868=head2 $app->error($code, $message)
869
870Sends the HTTP headers necessary to relay an error.
871
872=head2 $app->authenticate()
873
874Checks the WSSE authentication with the local MT user database and
875confirms the user is authorized to access the resources required by
876the request.
877
878=head2 $app->show_error($message)
879
880Returns an XML wrapper for the error response.
881
882=head2 $app->auth_failure($code, $message)
883
884Handles the response in the event of an authentication failure.
885
886=head1 CALLBACKS
887
888=over 4
889
890=item api_pre_save.entry
891
892    callback($eh, $app, $entry, $original_entry)
893
894Called before saving a new or existing entry. If saving a new entry, the
895$original_entry will have an unassigned 'id'. This callback is executed
896as a filter, so your handler must return 1 to allow the entry to be saved.
897
898=item api_post_save.entry
899
900    callback($eh, $app, $entry, $original_entry)
901
902Called after saving a new or existing entry. If saving a new entry, the
903$original_entry will have an unassigned 'id'.
904
905=back
906
907=cut
Note: See TracBrowser for help on using the browser.