root/branches/release-26/lib/MT/AtomServer.pm @ 1174

Revision 1174, 34.9 kB (checked in by bchoate, 23 months ago)

Updated copyright year for source.

  • 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        my @ts = MT::Util::offset_time_list(time, $blog->id);
560        my $ts = sprintf '%04d%02d%02d%02d%02d%02d',
561            $ts[5]+1900, $ts[4]+1, @ts[3,2,1,0];
562        $entry->authored_on($pub_ts);
563        if ($pub_ts > $ts) {
564            $entry->status(MT::Entry::FUTURE())
565        }
566    }
567## xxx mt/typepad-specific fields
568    $app->apply_basename($entry, $atom);
569    $entry->discover_tb_from_entry();
570
571    if (my @link = $atom->link) {
572        my $i = 0;
573        my $img_html = '';
574        my $num_links = scalar @link;
575        for my $link (@link) {
576            next unless $link->rel eq 'related';
577            my($asset_id) = $link->href =~ /asset\-(\d+)$/;
578            if ($asset_id) {
579                require MT::Asset;
580                my $a = MT::Asset->load($asset_id);
581                next unless $a;
582                my $pkg = MT::Asset->handler_for_file($a->file_name);
583                my $asset = bless $a, $pkg;
584                $img_html .= $asset->as_html({ include => 1 });
585            }
586        }
587        if ($img_html) {
588            $img_html .= qq{<br style="clear: left;" />\n\n};
589            $entry->text($img_html . $body);
590        }
591    }
592
593    MT->run_callbacks('api_pre_save.entry', $app, $entry, $orig_entry)
594        or return $app->error(500, MT->translate("PreSave failed [_1]", MT->errstr));
595
596    $entry->save or return $app->error(500, $entry->errstr);
597
598    require MT::Log;
599    $app->log({
600        message => $app->translate("User '[_1]' (user #[_2]) added [lc,_4] #[_3]", $user->name, $user->id, $entry->id, $entry->class_label),
601        level => MT::Log::INFO(),
602        class => 'entry',
603        category => 'new',
604        metadata => $entry->id
605    });
606    ## Save category, if present.
607    if ($cat) {
608        my $place = MT::Placement->new;
609        $place->is_primary(1);
610        $place->entry_id($entry->id);
611        $place->blog_id($blog->id);
612        $place->category_id($cat->id);
613        $place->save or return $app->error(500, $place->errstr);
614    }
615
616    MT->run_callbacks('api_post_save.entry', $app, $entry, $orig_entry);
617
618    $app->publish($entry);
619    $app->response_code(201);
620    $app->response_content_type('application/atom+xml');
621    my $edit_uri = $app->base . $app->uri . '/blog_id=' . $entry->blog_id . '/entry_id=' . $entry->id;
622    $app->set_header('Location', $edit_uri);
623    $atom = $app->new_with_entry($entry);
624    $atom->add_link({ rel => $app->edit_link_rel,
625                      href => $edit_uri,
626                      type => 'application/atom+xml',  # even in Legacy
627                      title => $entry->title });
628    $atom->as_xml;
629}
630
631sub edit_post {
632    my $app = shift;
633    my $atom = $app->atom_body or return;
634    my $blog = $app->{blog};
635    my $enc = $app->config('PublishCharset');
636    my $entry_id = $app->{param}{entry_id}
637        or return $app->error(400, "No entry_id");
638    my $entry = MT::Entry->load($entry_id)
639        or return $app->error(400, "Invalid entry_id");
640    return $app->error(403, "Access denied")
641        unless $app->{perms}->can_edit_entry($entry, $app->{user});
642    my $orig_entry = $entry->clone;
643    $entry->title(encode_text($atom->title,'utf-8',$enc));
644    $entry->text(encode_text(MT::I18N::utf8_off($atom->content()->body()),'utf-8',$enc));
645    $entry->excerpt(encode_text($atom->summary,'utf-8',$enc));
646    $entry->modified_by($app->{user}->id);
647    if (my $iso = $atom->issued) {
648        my $pub_ts = MT::Util::iso2ts($blog, $iso);
649        my @ts = MT::Util::offset_time_list(time, $blog->id);
650        my $ts = sprintf '%04d%02d%02d%02d%02d%02d',
651            $ts[5]+1900, $ts[4]+1, @ts[3,2,1,0];
652        $entry->authored_on($pub_ts);
653        if ($pub_ts > $ts) {
654            $entry->status(MT::Entry::FUTURE())
655        }
656    }
657## xxx mt/typepad-specific fields
658    $app->apply_basename($entry, $atom);
659    $entry->discover_tb_from_entry();
660
661    MT->run_callbacks('api_pre_save.entry', $app, $entry, $orig_entry)
662        or return $app->error(500, MT->translate("PreSave failed [_1]", MT->errstr));
663
664    $entry->save or return $app->error(500, "Entry not saved");
665
666    require MT::Log;
667    $app->log({
668        message => $app->translate("User '[_1]' (user #[_2]) edited [lc,_4] #[_3]", $app->{user}->name, $app->{user}->id, $entry->id, $entry->class_label),
669        level => MT::Log::INFO(),
670        class => 'entry',
671        category => 'new',
672        metadata => $entry->id
673    });
674
675    MT->run_callbacks('api_post_save.entry', $app, $entry, $orig_entry);
676
677    if ($entry->status == MT::Entry::RELEASE()) {
678        $app->publish($entry) or return $app->error(500, "Entry not published");
679    }
680    $app->response_code(200);
681    $app->response_content_type($app->atom_content_type);
682    $atom = $app->new_with_entry($entry);
683    $atom->as_xml;
684}
685
686sub get_posts {
687    my $app = shift;
688    my $blog = $app->{blog};
689    my %terms = (blog_id => $blog->id);
690    my %arg = (sort => $app->get_posts_order_field, direction => 'descend');
691    my $Limit = 20;
692    $arg{limit} = $Limit + 1;
693    $arg{offset} = $app->{param}{offset} || 0;
694    my $iter = MT::Entry->load_iter(\%terms, \%arg);
695    my $feed = $app->new_feed();
696    my $uri = $app->base . $app->uri . '/blog_id=' . $blog->id;
697    my $blogname = encode_text($blog->name, undef, 'utf-8');
698    $feed->add_link({ rel => 'alternate', type => 'text/html',
699                      href => $blog->site_url });
700    $feed->title($blogname);
701    $feed->add_link({ rel => 'service.post', type => 'application/x.atom+xml',
702                      href => $uri, title => $blogname });
703    $uri .= '/entry_id=';
704    while (my $entry = $iter->()) {
705        my $e = $app->new_with_entry($entry);
706        $e->add_link({ rel => $app->edit_link_rel, type => $app->atom_x_content_type,
707                       href => ($uri . $entry->id), title => encode_text($entry->title, undef,'utf-8') });
708        $feed->add_entry($e);
709    }
710    ## xxx add next/prev links
711    $app->response_content_type($app->atom_content_type);
712    $feed->as_xml;
713}
714
715sub get_post {
716    my $app = shift;
717    my $blog = $app->{blog};
718    my $entry_id = $app->{param}{entry_id}
719        or return $app->error(400, "No entry_id");
720    my $entry = MT::Entry->load($entry_id)
721        or return $app->error(400, "Invalid entry_id");
722    return $app->error(403, "Access denied")
723        unless $app->{perms}->can_edit_entry($entry, $app->{user});
724    $app->response_content_type($app->atom_content_type);
725    my $atom = $app->new_with_entry($entry);
726    my $uri = $app->base . $app->uri . '/blog_id=' . $blog->id;
727    $uri .= '/entry_id=';
728    $atom->add_link({ rel => $app->edit_link_rel, type => $app->atom_x_content_type,
729                      href => ($uri . $entry->id), title => encode_text($entry->title, undef,'utf-8') });
730    $atom->as_xml;
731}
732
733sub delete_post {
734    my $app = shift;
735    my $blog = $app->{blog};
736    my $entry_id = $app->{param}{entry_id}
737        or return $app->error(400, "No entry_id");
738    my $entry = MT::Entry->load($entry_id)
739        or return $app->error(400, "Invalid entry_id");
740    return $app->error(403, "Access denied")
741        unless $app->{perms}->can_edit_entry($entry, $app->{user});
742    $entry->remove
743        or return $app->error(500, $entry->errstr);
744    $app->publish($entry, 1) or return $app->error(500, $app->errstr);
745    '';
746}
747
748sub _upload_to_asset {
749    my $app = shift;
750    my $atom = $app->atom_body or return;
751    my $blog = $app->{blog};
752    my $user = $app->{user};
753    my %MIME2EXT = (
754        'text/plain'         => '.txt',
755        'image/jpeg'         => '.jpg',
756        'video/3gpp'         => '.3gp',
757        'application/x-mpeg' => '.mpg',
758        'video/mp4'          => '.mp4',
759        'video/quicktime'    => '.mov',
760        'audio/mpeg'         => '.mp3',
761        'audio/x-wav'        => '.wav',
762        'audio/ogg'          => '.ogg',
763        'audio/ogg-vorbis'   => '.ogg',
764    );
765
766    return $app->error(403, "Access denied") unless $app->{perms}->can_upload;
767    my $content = $atom->content;
768    my $type = $content->type
769        or return $app->error(400, "content \@type is required");
770    my $fname = $atom->title or return $app->error(400, "title is required");
771    $fname = basename($fname);
772    return $app->error(400, "Invalid or empty filename")
773        if $fname =~ m!/|\.\.|\0|\|!;
774
775    my $local_relative = File::Spec->catfile('%r', $fname);
776    my $local = File::Spec->catfile($blog->site_path, $fname);
777    my $fmgr = $blog->file_mgr;
778    my($base, $path, $ext) = File::Basename::fileparse($local, '\.[^\.]*');
779    $ext = $MIME2EXT{$type} unless $ext;
780    my $base_copy = $base;
781    my $ext_copy = $ext;
782    $ext_copy =~ s/\.//;
783    my $i = 1;
784    while ($fmgr->exists($path . $base . $ext)) {
785        $base = $base_copy . '_' . $i++;
786    }
787    $local = $path . $base . $ext;
788    my $data = $content->body;
789    defined(my $bytes = $fmgr->put_data($data, $local, 'upload'))
790        or return $app->error(500, "Error writing uploaded file");
791
792    eval { require Image::Size; };
793    return $app->error(500, MT->translate("Perl module Image::Size is required to determine width and height of uploaded images.")) if $@;
794    my ( $w, $h, $id ) = Image::Size::imgsize($local);
795
796    require MT::Asset;
797    my $asset_pkg = MT::Asset->handler_for_file($local);
798    my $is_image  = defined($w)
799      && defined($h)
800      && $asset_pkg->isa('MT::Asset::Image');
801    my $asset;
802    if (!($asset = $asset_pkg->load(
803                { file_path => $local, blog_id => $blog->id })))
804    {
805        $asset = $asset_pkg->new();
806        $asset->file_path($local_relative);
807        $asset->file_name($base.$ext);
808        $asset->file_ext($ext_copy);
809        $asset->blog_id($blog->id);
810        $asset->created_by( $user->id );
811    }
812    else {
813        $asset->modified_by( $user->id );
814    }
815    my $original = $asset->clone;
816    my $url = '%r/' . $base . $ext;
817    $asset->url($url);
818    if ($is_image) {
819        $asset->image_width($w);
820        $asset->image_height($h);
821    }
822    $asset->mime_type($type);
823    $asset->save;
824
825    MT->run_callbacks(
826        'api_upload_file.' . $asset->class,
827        File => $local, file => $local,
828        Url => $url, url => $url,
829        Size => $bytes, size => $bytes,
830        Asset => $asset, asset => $asset,
831        Type => $asset->class, type => $asset->class,
832        Blog => $blog, blog => $blog);
833    if ($is_image) {
834        MT->run_callbacks(
835            'api_upload_image',
836            File => $local, file => $local,
837            Url => $url, url => $url,
838            Size => $bytes, size => $bytes,
839            Asset => $asset, asset => $asset,
840            Height => $h, height => $h,
841            Width => $w, width => $w,
842            Type => 'image', type => 'image',
843            ImageType => $id, image_type => $id,
844            Blog => $blog, blog => $blog);
845    }
846
847    $asset;
848}
849
850sub handle_upload {
851    my $app = shift;
852    my $blog = $app->{blog};
853   
854    my $asset = $app->_upload_to_asset or return;
855
856    my $link = XML::Atom::Link->new;
857    $link->type($asset->mime_type);
858    $link->rel('alternate');
859    $link->href($asset->url);
860    my $atom = XML::Atom::Entry->new;
861    $atom->title($asset->file_name);
862    $atom->add_link($link);
863    $app->response_code(201);
864    $app->response_content_type('application/x.atom+xml');
865    $atom->as_xml;
866}
867
868package MT::AtomServer::Weblog::Legacy;
869use strict;
870
871use base qw( MT::AtomServer::Weblog );
872
873use MT::I18N qw( encode_text );
874use XML::Atom;  # for LIBXML
875use XML::Atom::Feed;
876use base qw( MT::AtomServer );
877use MT::Blog;
878use MT::Permission;
879
880use constant NS_CATEGORY => 'http://sixapart.com/atom/category#';
881use constant NS_DC => MT::AtomServer::Weblog->NS_DC();
882
883sub script { $_[0]->{cfg}->AtomScript . '/weblog' }
884
885sub atom_content_type   { 'application/xml' }
886sub atom_x_content_type { 'application/x.atom+xml' }
887
888sub edit_link_rel { 'service.edit' }
889sub get_posts_order_field { 'authored_on' }
890
891sub new_feed {
892    my $app = shift;
893    XML::Atom::Feed->new();
894}
895
896sub new_with_entry {
897    my $app = shift;
898    my ($entry) = @_;
899    MT::Atom::Entry->new_with_entry($entry);
900}
901
902sub apply_basename {}
903
904sub get_weblogs {
905    my $app = shift;
906    my $user = $app->{user};
907    my $iter = $user->is_superuser
908        ? MT::Blog->load_iter()
909        : MT::Permission->load_iter({ author_id => $user->id });
910    my $feed = $app->new_feed();
911    my $base = $app->base . $app->uri;
912    while (my $thing = $iter->()) {
913        if ($thing->isa('MT::Permission')) {
914            next unless $thing->can_create_post;
915        }
916        my $blog = $thing->isa('MT::Blog') ? $thing
917            : MT::Blog->load($thing->blog_id);
918        my $uri = $base . '/blog_id=' . $blog->id;
919        my $blogname = encode_text($blog->name . ' #' . $blog->id, undef, 'utf-8');
920        $feed->add_link({ rel => 'service.post', title => $blogname,
921                          href => $uri, type => 'application/x.atom+xml' });
922        $feed->add_link({ rel => 'service.feed', title => $blogname,
923                          href => $uri, type => 'application/x.atom+xml' });
924        $feed->add_link({ rel => 'service.upload', title => $blogname,
925                          href => $uri . '/svc=upload',
926                          type => 'application/x.atom+xml' });
927        $feed->add_link({ rel => 'service.categories', title => $blogname,
928                          href => $uri . '/svc=categories',
929                          type => 'application/x.atom+xml' });
930        $feed->add_link({ rel => 'alternate', title => $blogname,
931                          href => $blog->site_url,
932                          type => 'text/html' });
933    }
934    $app->response_code(200);
935    $app->response_content_type('application/x.atom+xml');
936    $feed->as_xml;
937}
938
939sub get_categories {
940    my $app = shift;
941    my $blog = $app->{blog};
942    my $iter = MT::Category->load_iter({ blog_id => $blog->id });
943    my $doc;
944    if (LIBXML) {
945        $doc = XML::LibXML::Document->createDocument('1.0', 'utf-8');
946        my $root = $doc->createElementNS(NS_CATEGORY, 'categories');
947        $doc->setDocumentElement($root);
948    } else {
949        $doc = XML::XPath::Node::Element->new('categories');
950        my $ns = XML::XPath::Node::Namespace->new('#default' => NS_CATEGORY);
951        $doc->appendNamespace($ns);
952    }
953    while (my $cat = $iter->()) {
954        my $catlabel = encode_text($cat->label, undef, 'utf-8');
955        if (LIBXML) {
956            my $elem = $doc->createElementNS(NS_DC, 'subject');
957            $doc->getDocumentElement->appendChild($elem);
958            $elem->appendChild(XML::LibXML::Text->new($catlabel));
959        } else {
960            my $elem = XML::XPath::Node::Element->new('subject');
961            my $ns = XML::XPath::Node::Namespace->new('#default' => NS_DC);
962            $elem->appendNamespace($ns);
963            $doc->appendChild($elem);
964            $elem->appendChild(XML::XPath::Node::Text->new($catlabel));
965        }
966    }
967    $app->response_code(200);
968    $app->response_content_type('application/x.atom+xml');
969    if (LIBXML) {
970        $doc->toString(1);
971    } else {
972        return '<?xml version="1.0" encoding="utf-8"?>' . "\n" . $doc->toString;
973    }
974}
975
9761;
977__END__
978
979=head1 NAME
980
981MT::AtomServer
982
983=head1 SYNOPSIS
984
985An Atom Publishing API interface for communicating with Movable Type.
986
987=head1 METHODS
988
989=head2 $app->xml_body()
990
991Takes the content posted to the server and parses it into an XML document.
992Uses either XML::LibXML or XML::XPath depending on which is available.
993
994=head2 $app->iso2epoch($iso_ts)
995
996Converts C<$iso_ts> in the format of an ISO timestamp into a unix timestamp
997(seconds since the epoch).
998
999=head2 $app->init
1000
1001Initializes the application.
1002
1003=head2 $app->get_auth_info
1004
1005Processes the request for WSSE authentication and returns a hash containing:
1006
1007=over 4
1008
1009=item * Username
1010
1011=item * PasswordDigest
1012
1013=item * Nonce
1014
1015=item * Created
1016
1017=back
1018
1019=head2 $app->handle_request
1020
1021The implementation of this in I<MT::AtomServer::Weblog> passes the request
1022to the proper method.
1023
1024=head2 $app->handle
1025
1026Wrapper method that determines the proper AtomServer package to pass the
1027request to.
1028
1029=head2 $app->iso2ts($iso_ts, $target_zone)
1030
1031Converts C<$iso_ts> in the format of an ISO timestamp into a MT-compatible
1032timestamp (YYYYMMDDHHMMSS) for the specified timezone C<$target_zone>.
1033
1034=head2 $app->atom_body
1035
1036Processes the request as Atom content and returns an XML::Atom object.
1037
1038=head2 $app->error($code, $message)
1039
1040Sends the HTTP headers necessary to relay an error.
1041
1042=head2 $app->authenticate()
1043
1044Checks the WSSE authentication with the local MT user database and
1045confirms the user is authorized to access the resources required by
1046the request.
1047
1048=head2 $app->show_error($message)
1049
1050Returns an XML wrapper for the error response.
1051
1052=head2 $app->auth_failure($code, $message)
1053
1054Handles the response in the event of an authentication failure.
1055
1056=head1 CALLBACKS
1057
1058=over 4
1059
1060=item api_pre_save.entry
1061
1062    callback($eh, $app, $entry, $original_entry)
1063
1064Called before saving a new or existing entry. If saving a new entry, the
1065$original_entry will have an unassigned 'id'. This callback is executed
1066as a filter, so your handler must return 1 to allow the entry to be saved.
1067
1068=item api_post_save.entry
1069
1070    callback($eh, $app, $entry, $original_entry)
1071
1072Called after saving a new or existing entry. If saving a new entry, the
1073$original_entry will have an unassigned 'id'.
1074
1075=back
1076
1077=cut
Note: See TracBrowser for help on using the browser.