root/branches/release-33/lib/MT/AtomServer.pm @ 1744

Revision 1744, 34.8 kB (checked in by fumiakiy, 20 months ago)

Implemented MT::DateTime::compare. BugId:67917

  • 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        $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(401, "Unauthorized");
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_APP => 'http://www.w3.org/2007/app';
308use constant NS_DC => 'http://purl.org/dc/elements/1.1/';
309use constant NS_TYPEPAD => 'http://sixapart.com/atom/typepad#';
310
311sub script { $_[0]->{cfg}->AtomScript . '/1.0' }
312
313sub atom_content_type   { 'application/atom+xml' }
314sub atom_x_content_type { 'application/atom+xml' }
315
316sub edit_link_rel { 'edit' }
317sub get_posts_order_field { 'modified_on' }
318
319sub new_feed {
320    my $app = shift;
321    XML::Atom::Feed->new( Version => 1.0 );
322}
323
324sub new_with_entry {
325    my $app = shift;
326    my ($entry) = @_;
327    my $atom = MT::Atom::Entry->new_with_entry( $entry, Version => 1.0 );
328
329    my $mo = MT::Atom::Entry::_create_issued($entry->modified_on, $entry->blog);
330    $atom->set(NS_APP(), 'edited', $mo);
331
332    $atom;
333}
334
335sub apply_basename {
336    my $app = shift;
337    my ($entry, $atom) = @_;
338
339    if (my $basename = $app->get_header('Slug')) {
340        my $entry_class = ref $entry;
341        my $basename_uses = $entry_class->count({
342            blog_id  => $entry->blog_id,
343            basename => $basename,
344            ($entry->id ? ( id => { op => '!=', value => $entry->id } ) : ()),
345        });
346        if ($basename_uses) {
347            $basename = MT::Util::make_unique_basename($entry);
348        }
349
350        $entry->basename($basename);
351    }
352
353    $entry;
354}
355
356sub handle_request {
357    my $app = shift;
358    $app->authenticate || return;
359    if (my $svc = $app->{param}{svc}) {
360        if ($svc eq 'upload') {
361            return $app->handle_upload;
362        } elsif ($svc eq 'categories') {
363            return $app->get_categories;
364        }
365    }
366    my $method = $app->request_method;
367    if ($method eq 'POST') {
368        return $app->new_post;
369    } elsif ($method eq 'PUT') {
370        return $app->edit_post;
371    } elsif ($method eq 'DELETE') {
372        return $app->delete_post;
373    } elsif ($method eq 'GET') {
374        if ($app->{param}{entry_id}) {
375            return $app->get_post;
376        } elsif ($app->{param}{blog_id}) {
377            return $app->get_posts;
378        } else {
379            return $app->get_weblogs;
380        }
381    }
382}
383
384sub authenticate {
385    my $app = shift;
386
387    $app->SUPER::authenticate or return;
388    if (my $blog_id = $app->{param}{blog_id}) {
389        $app->{blog} = MT::Blog->load($blog_id)
390            or return $app->error(400, "Invalid blog ID '$blog_id'");
391        $app->{user} 
392            or return $app->error(403, "Authenticate");
393        if ($app->{user}->is_superuser()) {
394            $app->{perms} = new MT::Permission;
395            $app->{perms}->blog_id($blog_id);
396            $app->{perms}->author_id($app->{user}->id);
397            $app->{perms}->can_administer_blog(1);
398            return 1;
399        }
400        my $perms = $app->{perms} = MT::Permission->load({
401                    author_id => $app->{user}->id,
402                    blog_id => $app->{blog}->id });
403        return $app->error(403, "Permission denied.") unless $perms && $perms->can_create_post;
404    }
405    1;
406}
407
408sub publish {
409    my $app = shift;
410    my($entry, $no_ping) = @_;
411    my $blog = MT::Blog->load($entry->blog_id);
412    $app->rebuild_entry( Entry => $entry, Blog => $blog,
413                         BuildDependencies => 1 ) or return;
414    unless ($no_ping) {
415        $app->ping_and_save( Entry => $entry, Blog => $blog )
416            or return;
417    }
418    1;
419}
420
421sub get_weblogs {
422    my $app = shift;
423    my $user = $app->{user};
424    my $iter = $user->is_superuser
425        ? MT::Blog->load_iter()
426        : MT::Permission->load_iter({ author_id => $user->id });
427    my $base = $app->base . $app->uri;
428
429    # TODO: libxml support? XPath should always be available...
430    require XML::XPath;
431    require XML::XPath::Node::Element;
432    require XML::XPath::Node::Namespace;
433    require XML::XPath::Node::Text;
434
435    my $doc = XML::XPath::Node::Element->new('service');
436    my $app_ns = XML::XPath::Node::Namespace->new('#default' => NS_APP());
437    $doc->appendNamespace($app_ns);
438    my $atom_ns = XML::XPath::Node::Namespace->new('atom' => 'http://www.w3.org/2005/Atom');
439    $doc->appendNamespace($atom_ns);
440
441    while (my $thing = $iter->()) {
442        # TODO: provide media collection if author can upload to this blog.
443        if ($thing->isa('MT::Permission')) {
444            next if !$thing->can_create_post;
445        }
446
447        my $blog = $thing->isa('MT::Blog') ? $thing
448            : MT::Blog->load($thing->blog_id);
449        my $uri = $base . '/blog_id=' . $blog->id;
450
451        my $workspace = XML::XPath::Node::Element->new('workspace');
452        $doc->appendChild($workspace);
453
454        my $title = XML::XPath::Node::Element->new('atom:title', 'atom');
455        $title->appendChild(XML::XPath::Node::Text->new($blog->name));
456        $workspace->appendChild($title);
457
458        my $entries = XML::XPath::Node::Element->new('collection');
459        $entries->appendAttribute(XML::XPath::Node::Attribute->new('href', $uri));
460        $workspace->appendChild($entries);
461
462        my $e_title = XML::XPath::Node::Element->new('atom:title', 'atom');
463        $e_title->appendChild(XML::XPath::Node::Text->new(MT->translate('Entries')));
464        $entries->appendChild($e_title);
465
466        my $cats = XML::XPath::Node::Element->new('categories');
467        $cats->appendAttribute(XML::XPath::Node::Attribute->new('href', $uri . '?svc=categories'));
468        $entries->appendChild($cats);
469    }
470    $app->response_code(200);
471    $app->response_content_type('application/atomsvc+xml');
472    '<?xml version="1.0" encoding="utf-8"?>' . "\n" .                                                         
473        $doc->toString;
474}
475
476sub get_categories {
477    my $app = shift;
478    my $blog = $app->{blog};
479
480    # TODO: libxml support? XPath should always be available...
481    require XML::XPath;
482    require XML::XPath::Node::Element;
483    require XML::XPath::Node::Namespace;
484    require XML::XPath::Node::Text;
485
486    my $doc = XML::XPath::Node::Element->new('categories');
487    my $app_ns = XML::XPath::Node::Namespace->new('#default' => NS_APP());
488    $doc->appendNamespace($app_ns);
489    my $atom_ns = XML::XPath::Node::Namespace->new('atom' => 'http://www.w3.org/2005/Atom');
490    $doc->appendNamespace($atom_ns);
491    $doc->appendAttribute(XML::XPath::Node::Attribute->new('fixed', 'yes'));
492
493    my $iter = MT::Category->load_iter({ blog_id => $blog->id });
494    while (my $cat = $iter->()) {
495        my $cat_node = XML::XPath::Node::Element->new('atom:category', 'atom');
496        $cat_node->appendAttribute(XML::XPath::Node::Attribute->new('term', $cat->label));
497        $doc->appendChild($cat_node);
498    }
499
500    $app->response_code(200);
501    $app->response_content_type('application/atomcat+xml');
502    '<?xml version="1.0" encoding="utf-8"?>' . "\n" .                                                         
503        $doc->toString;
504}
505
506sub new_post {
507    my $app = shift;
508    my $atom = $app->atom_body or return $app->error(500, "No body!");
509    my $blog = $app->{blog};
510    my $user = $app->{user};
511    my $perms = $app->{perms};
512    my $enc = $app->config('PublishCharset');
513    ## Check for category in dc:subject. We will save it later if
514    ## it's present, but we want to give an error now if necessary.
515    my($cat);
516    if (my $label = $atom->get(NS_DC, 'subject')) {
517        my $label_enc = encode_text($label,'utf-8',$enc);
518        $cat = MT::Category->load({ blog_id => $blog->id, label => $label_enc })
519            or return $app->error(400, "Invalid category '$label'");
520    }
521
522    my $content = $atom->content;
523    my $type = $content->type; 
524    my $body = encode_text(MT::I18N::utf8_off($content->body),'utf-8',$enc); 
525    my $asset;
526    if ($type && $type !~ m!^application/.*xml$!) {
527        if ($type !~ m!^text/!) {
528            $asset = $app->_upload_to_asset or return;
529        }
530        elsif ($type && $type eq 'text/plain') {
531            ## Check for LifeBlog Note & SMS records.
532            my $format = $atom->get(NS_DC, 'format');
533            if ($format && ($format eq 'Note' || $format eq 'SMS')) {
534                $asset = $app->_upload_to_asset or return;
535            }
536        }
537    }
538    if ( $atom->get(NS_TYPEPAD, 'standalone') && $asset ) {
539        $app->response_code(201);
540        $app->response_content_type('application/atom_xml');
541        my $a = MT::Atom::Entry->new_with_asset($asset);
542        return $a->as_xml; 
543    } 
544
545    my $entry = MT::Entry->new;
546    my $orig_entry = $entry->clone;
547    $entry->blog_id($blog->id);
548    $entry->author_id($user->id);
549    $entry->created_by($user->id);
550    $entry->status($perms->can_publish_post ? MT::Entry::RELEASE() : MT::Entry::HOLD() );
551    $entry->allow_comments($blog->allow_comments_default);
552    $entry->allow_pings($blog->allow_pings_default);
553    $entry->convert_breaks($blog->convert_paras);
554    $entry->title(encode_text($atom->title,'utf-8',$enc));
555    $entry->text(encode_text(MT::I18N::utf8_off($atom->content()->body()),'utf-8',$enc));
556    $entry->excerpt(encode_text($atom->summary,'utf-8',$enc));
557    if (my $iso = $atom->issued) {
558        my $pub_ts = MT::Util::iso2ts($blog, $iso);
559        $entry->authored_on($pub_ts);
560        if ( 0 < MT::DateTime->compare( blog => $blog,
561                a => $pub_ts,
562                b => { value => time(), type => 'epoch' } )
563           )
564        {
565            $entry->status(MT::Entry::FUTURE())
566        }
567    }
568## xxx mt/typepad-specific fields
569    $app->apply_basename($entry, $atom);
570    $entry->discover_tb_from_entry();
571
572    if (my @link = $atom->link) {
573        my $i = 0;
574        my $img_html = '';
575        my $num_links = scalar @link;
576        for my $link (@link) {
577            next unless $link->rel eq 'related';
578            my($asset_id) = $link->href =~ /asset\-(\d+)$/;
579            if ($asset_id) {
580                require MT::Asset;
581                my $a = MT::Asset->load($asset_id);
582                next unless $a;
583                my $pkg = MT::Asset->handler_for_file($a->file_name);
584                my $asset = bless $a, $pkg;
585                $img_html .= $asset->as_html({ include => 1 });
586            }
587        }
588        if ($img_html) {
589            $img_html .= qq{<br style="clear: left;" />\n\n};
590            $entry->text($img_html . $body);
591        }
592    }
593
594    MT->run_callbacks('api_pre_save.entry', $app, $entry, $orig_entry)
595        or return $app->error(500, MT->translate("PreSave failed [_1]", MT->errstr));
596
597    $entry->save or return $app->error(500, $entry->errstr);
598
599    require MT::Log;
600    $app->log({
601        message => $app->translate("User '[_1]' (user #[_2]) added [lc,_4] #[_3]", $user->name, $user->id, $entry->id, $entry->class_label),
602        level => MT::Log::INFO(),
603        class => 'entry',
604        category => 'new',
605        metadata => $entry->id
606    });
607    ## Save category, if present.
608    if ($cat) {
609        my $place = MT::Placement->new;
610        $place->is_primary(1);
611        $place->entry_id($entry->id);
612        $place->blog_id($blog->id);
613        $place->category_id($cat->id);
614        $place->save or return $app->error(500, $place->errstr);
615    }
616
617    MT->run_callbacks('api_post_save.entry', $app, $entry, $orig_entry);
618
619    $app->publish($entry);
620    $app->response_code(201);
621    $app->response_content_type('application/atom+xml');
622    my $edit_uri = $app->base . $app->uri . '/blog_id=' . $entry->blog_id . '/entry_id=' . $entry->id;
623    $app->set_header('Location', $edit_uri);
624    $atom = $app->new_with_entry($entry);
625    $atom->add_link({ rel => $app->edit_link_rel,
626                      href => $edit_uri,
627                      type => 'application/atom+xml',  # even in Legacy
628                      title => $entry->title });
629    $atom->as_xml;
630}
631
632sub edit_post {
633    my $app = shift;
634    my $atom = $app->atom_body or return;
635    my $blog = $app->{blog};
636    my $enc = $app->config('PublishCharset');
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    my $orig_entry = $entry->clone;
644    $entry->title(encode_text($atom->title,'utf-8',$enc));
645    $entry->text(encode_text(MT::I18N::utf8_off($atom->content()->body()),'utf-8',$enc));
646    $entry->excerpt(encode_text($atom->summary,'utf-8',$enc));
647    $entry->modified_by($app->{user}->id);
648    if (my $iso = $atom->issued) {
649        my $pub_ts = MT::Util::iso2ts($blog, $iso);
650        $entry->authored_on($pub_ts);
651        if ( 0 < MT::DateTime->compare( blog => $blog,
652                a => $pub_ts,
653                b => { value => time(), type => 'epoch' } )
654           )
655        {
656            $entry->status(MT::Entry::FUTURE())
657        }
658    }
659## xxx mt/typepad-specific fields
660    $app->apply_basename($entry, $atom);
661    $entry->discover_tb_from_entry();
662
663    MT->run_callbacks('api_pre_save.entry', $app, $entry, $orig_entry)
664        or return $app->error(500, MT->translate("PreSave failed [_1]", MT->errstr));
665
666    $entry->save or return $app->error(500, "Entry not saved");
667
668    require MT::Log;
669    $app->log({
670        message => $app->translate("User '[_1]' (user #[_2]) edited [lc,_4] #[_3]", $app->{user}->name, $app->{user}->id, $entry->id, $entry->class_label),
671        level => MT::Log::INFO(),
672        class => 'entry',
673        category => 'new',
674        metadata => $entry->id
675    });
676
677    MT->run_callbacks('api_post_save.entry', $app, $entry, $orig_entry);
678
679    if ($entry->status == MT::Entry::RELEASE()) {
680        $app->publish($entry) or return $app->error(500, "Entry not published");
681    }
682    $app->response_code(200);
683    $app->response_content_type($app->atom_content_type);
684    $atom = $app->new_with_entry($entry);
685    $atom->as_xml;
686}
687
688sub get_posts {
689    my $app = shift;
690    my $blog = $app->{blog};
691    my %terms = (blog_id => $blog->id);
692    my %arg = (sort => $app->get_posts_order_field, direction => 'descend');
693    my $Limit = 20;
694    $arg{limit} = $Limit + 1;
695    $arg{offset} = $app->{param}{offset} || 0;
696    my $iter = MT::Entry->load_iter(\%terms, \%arg);
697    my $feed = $app->new_feed();
698    my $uri = $app->base . $app->uri . '/blog_id=' . $blog->id;
699    my $blogname = encode_text($blog->name, undef, 'utf-8');
700    $feed->add_link({ rel => 'alternate', type => 'text/html',
701                      href => $blog->site_url });
702    $feed->title($blogname);
703    $feed->add_link({ rel => 'service.post', type => 'application/x.atom+xml',
704                      href => $uri, title => $blogname });
705    $uri .= '/entry_id=';
706    while (my $entry = $iter->()) {
707        my $e = $app->new_with_entry($entry);
708        $e->add_link({ rel => $app->edit_link_rel, type => $app->atom_x_content_type,
709                       href => ($uri . $entry->id), title => encode_text($entry->title, undef,'utf-8') });
710        $feed->add_entry($e);
711    }
712    ## xxx add next/prev links
713    $app->response_content_type($app->atom_content_type);
714    $feed->as_xml;
715}
716
717sub get_post {
718    my $app = shift;
719    my $blog = $app->{blog};
720    my $entry_id = $app->{param}{entry_id}
721        or return $app->error(400, "No entry_id");
722    my $entry = MT::Entry->load($entry_id)
723        or return $app->error(400, "Invalid entry_id");
724    return $app->error(403, "Access denied")
725        unless $app->{perms}->can_edit_entry($entry, $app->{user});
726    $app->response_content_type($app->atom_content_type);
727    my $atom = $app->new_with_entry($entry);
728    my $uri = $app->base . $app->uri . '/blog_id=' . $blog->id;
729    $uri .= '/entry_id=';
730    $atom->add_link({ rel => $app->edit_link_rel, type => $app->atom_x_content_type,
731                      href => ($uri . $entry->id), title => encode_text($entry->title, undef,'utf-8') });
732    $atom->as_xml;
733}
734
735sub delete_post {
736    my $app = shift;
737    my $blog = $app->{blog};
738    my $entry_id = $app->{param}{entry_id}
739        or return $app->error(400, "No entry_id");
740    my $entry = MT::Entry->load($entry_id)
741        or return $app->error(400, "Invalid entry_id");
742    return $app->error(403, "Access denied")
743        unless $app->{perms}->can_edit_entry($entry, $app->{user});
744    $entry->remove
745        or return $app->error(500, $entry->errstr);
746    $app->publish($entry, 1) or return $app->error(500, $app->errstr);
747    '';
748}
749
750sub _upload_to_asset {
751    my $app = shift;
752    my $atom = $app->atom_body or return;
753    my $blog = $app->{blog};
754    my $user = $app->{user};
755    my %MIME2EXT = (
756        'text/plain'         => '.txt',
757        'image/jpeg'         => '.jpg',
758        'video/3gpp'         => '.3gp',
759        'application/x-mpeg' => '.mpg',
760        'video/mp4'          => '.mp4',
761        'video/quicktime'    => '.mov',
762        'audio/mpeg'         => '.mp3',
763        'audio/x-wav'        => '.wav',
764        'audio/ogg'          => '.ogg',
765        'audio/ogg-vorbis'   => '.ogg',
766    );
767
768    return $app->error(403, "Access denied") unless $app->{perms}->can_upload;
769    my $content = $atom->content;
770    my $type = $content->type
771        or return $app->error(400, "content \@type is required");
772    my $fname = $atom->title or return $app->error(400, "title is required");
773    $fname = basename($fname);
774    return $app->error(400, "Invalid or empty filename")
775        if $fname =~ m!/|\.\.|\0|\|!;
776
777    my $local_relative = File::Spec->catfile('%r', $fname);
778    my $local = File::Spec->catfile($blog->site_path, $fname);
779    my $fmgr = $blog->file_mgr;
780    my($base, $path, $ext) = File::Basename::fileparse($local, '\.[^\.]*');
781    $ext = $MIME2EXT{$type} unless $ext;
782    my $base_copy = $base;
783    my $ext_copy = $ext;
784    $ext_copy =~ s/\.//;
785    my $i = 1;
786    while ($fmgr->exists($path . $base . $ext)) {
787        $base = $base_copy . '_' . $i++;
788    }
789    $local = $path . $base . $ext;
790    my $data = $content->body;
791    defined(my $bytes = $fmgr->put_data($data, $local, 'upload'))
792        or return $app->error(500, "Error writing uploaded file");
793
794    eval { require Image::Size; };
795    return $app->error(500, MT->translate("Perl module Image::Size is required to determine width and height of uploaded images.")) if $@;
796    my ( $w, $h, $id ) = Image::Size::imgsize($local);
797
798    require MT::Asset;
799    my $asset_pkg = MT::Asset->handler_for_file($local);
800    my $is_image  = defined($w)
801      && defined($h)
802      && $asset_pkg->isa('MT::Asset::Image');
803    my $asset;
804    if (!($asset = $asset_pkg->load(
805                { file_path => $local, blog_id => $blog->id })))
806    {
807        $asset = $asset_pkg->new();
808        $asset->file_path($local_relative);
809        $asset->file_name($base.$ext);
810        $asset->file_ext($ext_copy);
811        $asset->blog_id($blog->id);
812        $asset->created_by( $user->id );
813    }
814    else {
815        $asset->modified_by( $user->id );
816    }
817    my $original = $asset->clone;
818    my $url = '%r/' . $base . $ext;
819    $asset->url($url);
820    if ($is_image) {
821        $asset->image_width($w);
822        $asset->image_height($h);
823    }
824    $asset->mime_type($type);
825    $asset->save;
826
827    MT->run_callbacks(
828        'api_upload_file.' . $asset->class,
829        File => $local, file => $local,
830        Url => $url, url => $url,
831        Size => $bytes, size => $bytes,
832        Asset => $asset, asset => $asset,
833        Type => $asset->class, type => $asset->class,
834        Blog => $blog, blog => $blog);
835    if ($is_image) {
836        MT->run_callbacks(
837            'api_upload_image',
838            File => $local, file => $local,
839            Url => $url, url => $url,
840            Size => $bytes, size => $bytes,
841            Asset => $asset, asset => $asset,
842            Height => $h, height => $h,
843            Width => $w, width => $w,
844            Type => 'image', type => 'image',
845            ImageType => $id, image_type => $id,
846            Blog => $blog, blog => $blog);
847    }
848
849    $asset;
850}
851
852sub handle_upload {
853    my $app = shift;
854    my $blog = $app->{blog};
855   
856    my $asset = $app->_upload_to_asset or return;
857
858    my $link = XML::Atom::Link->new;
859    $link->type($asset->mime_type);
860    $link->rel('alternate');
861    $link->href($asset->url);
862    my $atom = XML::Atom::Entry->new;
863    $atom->title($asset->file_name);
864    $atom->add_link($link);
865    $app->response_code(201);
866    $app->response_content_type('application/x.atom+xml');
867    $atom->as_xml;
868}
869
870package MT::AtomServer::Weblog::Legacy;
871use strict;
872
873use base qw( MT::AtomServer::Weblog );
874
875use MT::I18N qw( encode_text );
876use XML::Atom;  # for LIBXML
877use XML::Atom::Feed;
878use base qw( MT::AtomServer );
879use MT::Blog;
880use MT::Permission;
881
882use constant NS_CATEGORY => 'http://sixapart.com/atom/category#';
883use constant NS_DC => MT::AtomServer::Weblog->NS_DC();
884
885sub script { $_[0]->{cfg}->AtomScript . '/weblog' }
886
887sub atom_content_type   { 'application/xml' }
888sub atom_x_content_type { 'application/x.atom+xml' }
889
890sub edit_link_rel { 'service.edit' }
891sub get_posts_order_field { 'authored_on' }
892
893sub new_feed {
894    my $app = shift;
895    XML::Atom::Feed->new();
896}
897
898sub new_with_entry {
899    my $app = shift;
900    my ($entry) = @_;
901    MT::Atom::Entry->new_with_entry($entry);
902}
903
904sub apply_basename {}
905
906sub get_weblogs {
907    my $app = shift;
908    my $user = $app->{user};
909    my $iter = $user->is_superuser
910        ? MT::Blog->load_iter()
911        : MT::Permission->load_iter({ author_id => $user->id });
912    my $feed = $app->new_feed();
913    my $base = $app->base . $app->uri;
914    while (my $thing = $iter->()) {
915        if ($thing->isa('MT::Permission')) {
916            next unless $thing->can_create_post;
917        }
918        my $blog = $thing->isa('MT::Blog') ? $thing
919            : MT::Blog->load($thing->blog_id);
920        my $uri = $base . '/blog_id=' . $blog->id;
921        my $blogname = encode_text($blog->name . ' #' . $blog->id, undef, 'utf-8');
922        $feed->add_link({ rel => 'service.post', title => $blogname,
923                          href => $uri, type => 'application/x.atom+xml' });
924        $feed->add_link({ rel => 'service.feed', title => $blogname,
925                          href => $uri, type => 'application/x.atom+xml' });
926        $feed->add_link({ rel => 'service.upload', title => $blogname,
927                          href => $uri . '/svc=upload',
928                          type => 'application/x.atom+xml' });
929        $feed->add_link({ rel => 'service.categories', title => $blogname,
930                          href => $uri . '/svc=categories',
931                          type => 'application/x.atom+xml' });
932        $feed->add_link({ rel => 'alternate', title => $blogname,
933                          href => $blog->site_url,
934                          type => 'text/html' });
935    }
936    $app->response_code(200);
937    $app->response_content_type('application/x.atom+xml');
938    $feed->as_xml;
939}
940
941sub get_categories {
942    my $app = shift;
943    my $blog = $app->{blog};
944    my $iter = MT::Category->load_iter({ blog_id => $blog->id });
945    my $doc;
946    if (LIBXML) {
947        $doc = XML::LibXML::Document->createDocument('1.0', 'utf-8');
948        my $root = $doc->createElementNS(NS_CATEGORY, 'categories');
949        $doc->setDocumentElement($root);
950    } else {
951        $doc = XML::XPath::Node::Element->new('categories');
952        my $ns = XML::XPath::Node::Namespace->new('#default' => NS_CATEGORY);
953        $doc->appendNamespace($ns);
954    }
955    while (my $cat = $iter->()) {
956        my $catlabel = encode_text($cat->label, undef, 'utf-8');
957        if (LIBXML) {
958            my $elem = $doc->createElementNS(NS_DC, 'subject');
959            $doc->getDocumentElement->appendChild($elem);
960            $elem->appendChild(XML::LibXML::Text->new($catlabel));
961        } else {
962            my $elem = XML::XPath::Node::Element->new('subject');
963            my $ns = XML::XPath::Node::Namespace->new('#default' => NS_DC);
964            $elem->appendNamespace($ns);
965            $doc->appendChild($elem);
966            $elem->appendChild(XML::XPath::Node::Text->new($catlabel));
967        }
968    }
969    $app->response_code(200);
970    $app->response_content_type('application/x.atom+xml');
971    if (LIBXML) {
972        $doc->toString(1);
973    } else {
974        return '<?xml version="1.0" encoding="utf-8"?>' . "\n" . $doc->toString;
975    }
976}
977
9781;
979__END__
980
981=head1 NAME
982
983MT::AtomServer
984
985=head1 SYNOPSIS
986
987An Atom Publishing API interface for communicating with Movable Type.
988
989=head1 METHODS
990
991=head2 $app->xml_body()
992
993Takes the content posted to the server and parses it into an XML document.
994Uses either XML::LibXML or XML::XPath depending on which is available.
995
996=head2 $app->iso2epoch($iso_ts)
997
998Converts C<$iso_ts> in the format of an ISO timestamp into a unix timestamp
999(seconds since the epoch).
1000
1001=head2 $app->init
1002
1003Initializes the application.
1004
1005=head2 $app->get_auth_info
1006
1007Processes the request for WSSE authentication and returns a hash containing:
1008
1009=over 4
1010
1011=item * Username
1012
1013=item * PasswordDigest
1014
1015=item * Nonce
1016
1017=item * Created
1018
1019=back
1020
1021=head2 $app->handle_request
1022
1023The implementation of this in I<MT::AtomServer::Weblog> passes the request
1024to the proper method.
1025
1026=head2 $app->handle
1027
1028Wrapper method that determines the proper AtomServer package to pass the
1029request to.
1030
1031=head2 $app->iso2ts($iso_ts, $target_zone)
1032
1033Converts C<$iso_ts> in the format of an ISO timestamp into a MT-compatible
1034timestamp (YYYYMMDDHHMMSS) for the specified timezone C<$target_zone>.
1035
1036=head2 $app->atom_body
1037
1038Processes the request as Atom content and returns an XML::Atom object.
1039
1040=head2 $app->error($code, $message)
1041
1042Sends the HTTP headers necessary to relay an error.
1043
1044=head2 $app->authenticate()
1045
1046Checks the WSSE authentication with the local MT user database and
1047confirms the user is authorized to access the resources required by
1048the request.
1049
1050=head2 $app->show_error($message)
1051
1052Returns an XML wrapper for the error response.
1053
1054=head2 $app->auth_failure($code, $message)
1055
1056Handles the response in the event of an authentication failure.
1057
1058=head1 CALLBACKS
1059
1060=over 4
1061
1062=item api_pre_save.entry
1063
1064    callback($eh, $app, $entry, $original_entry)
1065
1066Called before saving a new or existing entry. If saving a new entry, the
1067$original_entry will have an unassigned 'id'. This callback is executed
1068as a filter, so your handler must return 1 to allow the entry to be saved.
1069
1070=item api_post_save.entry
1071
1072    callback($eh, $app, $entry, $original_entry)
1073
1074Called after saving a new or existing entry. If saving a new entry, the
1075$original_entry will have an unassigned 'id'.
1076
1077=back
1078
1079=cut
Note: See TracBrowser for help on using the browser.