root/branches/release-38/lib/MT/XMLRPCServer.pm @ 2264

Revision 2264, 46.2 kB (checked in by bchoate, 19 months ago)

Changed future post support so that it only is applied to entries that are meant to be published (allows for future-dated entries that are saved as a draft). Thanks to Sam Greenfield for the patch. BugId:79605

  • 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::XMLRPCServer::Util;
8use strict;
9use Time::Local qw( timegm );
10use MT::Util qw( offset_time_list );
11use MT::I18N qw( encode_text first_n_text const );
12
13sub mt_new {
14    my $cfg = $ENV{MOD_PERL} ?
15        Apache->request->dir_config('MTConfig') :
16        $MT::XMLRPCServer::MT_DIR . '/mt-config.cgi';
17    my $mt = MT->new( Config => $cfg )
18        or die MT::XMLRPCServer::_fault(MT->errstr);
19    # we need to be UTF-8 here no matter which PublishCharset
20    $main::server->serializer->encoding('UTF-8');
21    $mt->run_callbacks('init_app', $mt, {App => 'xmlrpc'});
22    $mt;
23}
24
25sub iso2ts {
26    my($blog, $iso) = @_;
27    die MT::XMLRPCServer::_fault(MT->translate("Invalid timestamp format"))
28        unless $iso =~ /^(\d{4})(?:-?(\d{2})(?:-?(\d\d?)(?:T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(Z|[+-]\d{2}:\d{2})?)?)?)?/;
29    my($y, $mo, $d, $h, $m, $s, $offset) =
30    ($1, $2 || 1, $3 || 1, $4 || 0, $5 || 0, $6 || 0, $7);
31    if ($offset && !MT->config->IgnoreISOTimezones) {
32        $mo--;
33        $y -= 1900;
34        my $time = timegm($s, $m, $h, $d, $mo, $y);
35        ## If it's not already in UTC, first convert to UTC.
36        if ($offset ne 'Z') {
37            my($sign, $h, $m) = $offset =~ /([+-])(\d{2}):(\d{2})/;
38            $offset = $h * 3600 + $m * 60;
39            $offset *= -1 if $sign eq '-';
40            $time -= $offset;
41        }
42        ## Now apply the offset for this weblog.
43        ($s, $m, $h, $d, $mo, $y) = offset_time_list($time, $blog);
44        $mo++;
45        $y += 1900;
46    }
47    sprintf "%04d%02d%02d%02d%02d%02d", $y, $mo, $d, $h, $m, $s;
48}
49
50sub ts2iso {
51    my ($blog, $ts) = @_;
52    my ($yr, $mo, $dy, $hr, $mn, $sc) = unpack('A4A2A2A2A2A2A2', $ts);
53    $ts = timegm($sc, $mn, $hr, $dy, $mo, $yr);
54    ($sc, $mn, $hr, $dy, $mo, $yr) = offset_time_list($ts, $blog, '-');
55    $yr += 1900;
56    sprintf("%04d-%02d-%02d %02d:%02d:%02d", $yr, $mo, $dy, $hr, $mn, $sc);
57}
58
59package MT::XMLRPCServer;
60use strict;
61
62use MT;
63use MT::Util qw( first_n_words decode_html start_background_task archive_file_for );
64use MT::I18N qw( encode_text first_n_text const );
65use base qw( MT::ErrorHandler );
66
67our $MT_DIR;
68
69my($HAVE_XML_PARSER);
70BEGIN {
71    eval { require XML::Parser };
72    $HAVE_XML_PARSER = $@ ? 0 : 1;
73}
74
75sub _fault {
76    my $mt = MT::XMLRPCServer::Util::mt_new();
77    my $enc = $mt->config('PublishCharset');
78    SOAP::Fault->faultcode(1)->faultstring(
79        SOAP::Data->type(
80            string => encode_text($_[0], $enc, 'utf-8')));
81}
82
83## This is sort of a hack. XML::Parser automatically makes everything
84## UTF-8, and that is causing severe problems with the serialization
85## of database records (what happens is this: we construct a string
86## consisting of pack('N', length($string)) . $string. If the $string SV
87## is flagged as UTF-8, the packed length is then upgraded to UTF-8,
88## which turns characters with values greater than 128 into two bytes,
89## like v194.129. And so on. This is obviously now what we want, because
90## pack produces a series of bytes, not a string that should be mucked
91## about with.)
92##
93## The following subroutine strips the UTF8 flag from a string, thus
94## forcing it into a series of bytes. "pack 'C0'" is a magic way of
95## forcing the following string to be packed as bytes, not as UTF8.
96
97sub no_utf8 {
98    for (@_) {
99        next if ref;
100        $_ = pack 'C0A*', $_;
101    }
102}
103
104sub _make_token {
105    my @alpha = ('a'..'z', 'A'..'Z', 0..9);
106    my $token = join '', map $alpha[rand @alpha], 1..40;
107    $token;
108}
109
110sub _login {
111    my $class = shift;
112    my($user, $pass, $blog_id) = @_;
113    no_utf8($user, $pass);
114    my $mt = MT::XMLRPCServer::Util::mt_new();
115    my $enc = $mt->config('PublishCharset');
116    $user = encode_text($user, 'utf-8', $enc);
117    $pass = encode_text($pass, 'utf-8', $enc);
118    require MT::Author;
119    my $author = MT::Author->load({ name => $user, type => 1 }) or return;
120    die _fault(MT->translate("No web services password assigned.  Please see your user profile to set it.")) unless $author->api_password;
121    die _fault(MT->translate("Failed login attempt by disabled user '[_1]'")) unless $author->is_active;
122    my $auth = $author->api_password eq $pass;
123    $auth ||= crypt($pass, $author->api_password) eq $author->api_password;
124    return unless $auth;
125    return $author unless $blog_id;
126    require MT::Permission;
127    my $perms = MT::Permission->load({ author_id => $author->id,
128                                       blog_id => $blog_id });
129
130    ## update session so the user will be counted as active
131    require MT::Session;
132    my $sess_active = MT::Session->load( { kind => 'UA', name => $author->id } );
133    if (!$sess_active) {
134        $sess_active = MT::Session->new;
135        $sess_active->id(_make_token());
136        $sess_active->kind('UA'); # UA == User Activation
137        $sess_active->name($author->id);
138    }
139    $sess_active->start(time);
140    $sess_active->save;
141
142    ($author, $perms);
143}
144
145sub _publish {
146    my $class = shift;
147    my($mt, $entry, $no_ping) = @_;
148    require MT::Blog;
149    my $blog = MT::Blog->load($entry->blog_id);
150    $mt->rebuild_entry( Entry => $entry, Blog => $blog,
151                        BuildDependencies => 1 )
152        or return $class->error("Publish error: " . $mt->errstr);
153    unless ($no_ping) {
154        $mt->ping_and_save(Blog => $blog, Entry => $entry)
155            or return $class->error("Ping error: " . $mt->errstr);
156    }
157    1;
158}
159
160sub _apply_basename {
161    my $class = shift;
162    my ($entry, $item, $param) = @_;
163
164    my $basename = $item->{mt_basename} || $item->{wp_slug};
165    if ($param->{page} && $item->{permaLink}) {
166        local $entry->{column_values}->{basename} = '##s##';
167        my $real_url = $entry->archive_url();
168        my ($pre, $post) = split /##s##/, $real_url, 2;
169       
170        my $req_url = $item->{permaLink};
171        if ($req_url =~ m{ \A \Q$pre\E (.*) \Q$post\E \z }xms) {
172            my $req_base = $1;
173            my @folders = split /\//, $req_base;
174            $basename = pop @folders;
175            $param->{__permaLink_folders} = \@folders;
176        }
177        else {
178            die _fault(MT->translate("Requested permalink '[_1]' is not available for this page",
179                $req_url));
180        }
181    }
182
183    if (defined $basename) {
184        # Ensure this basename is unique.
185        my $entry_class = ref $entry;
186        my $basename_uses = $entry_class->exist({
187            blog_id  => $entry->blog_id,
188            basename => $basename,
189            ($entry->id ? ( id => { op => '!=', value => $entry->id } ) : ()),
190        });
191        if ($basename_uses) {
192            $basename = MT::Util::make_unique_basename($entry);
193        }
194
195        $entry->basename($basename);
196    }
197
198    1;
199}
200
201sub _save_placements {
202    my $class = shift;
203    my ($entry, $item, $param) = @_;
204
205    my @categories;
206    my $changed = 0;
207
208    if ($param->{page}) {
209        if (my $folders = $param->{__permaLink_folders}) {
210            my $parent_id = 0;
211            my $folder;
212            require MT::Folder;
213            for my $basename (@$folders) {
214                $folder = MT::Folder->load({
215                    blog_id  => $entry->blog_id,
216                    parent   => $parent_id,
217                    basename => $basename,
218                });
219
220                if (!$folder) {
221                    # Autovivify the folder tree.
222                    $folder = MT::Folder->new;
223                    $folder->blog_id($entry->blog_id);
224                    $folder->parent($parent_id);
225                    $folder->basename($basename);
226                    $folder->label($basename);
227                    $changed = 1;
228                    $folder->save
229                      or die _fault(MT->translate("Saving folder failed: [_1]",
230                        $folder->errstr));
231                }
232
233                $parent_id = $folder->id;
234            }
235            @categories = ($folder) if $folder;
236        }
237    }
238    elsif (my $cats = $item->{categories}) {
239        if (@$cats) {
240            my $cat_class = MT->model('category');
241            # The spec says to ignore invalid category names.
242            @categories = grep { defined } $cat_class->search({
243                blog_id => $entry->blog_id,
244                label   => $cats,
245            });
246        }
247    }
248
249    require MT::Placement;
250    my $is_primary_placement = 1;
251    CATEGORY: for my $category (@categories) {
252        my $place;
253        if ($is_primary_placement) {
254            $place = MT::Placement->load({
255                entry_id   => $entry->id,
256                is_primary => 1,
257            });
258        }
259        if (!$place) {
260            $place = MT::Placement->new;
261            $place->blog_id($entry->blog_id);
262            $place->entry_id($entry->id);
263            $place->is_primary($is_primary_placement ? 1 : 0);
264        }
265        $place->category_id($category->id);
266        $place->save
267          or die _fault(MT->translate("Saving placement failed: [_1]",
268            $place->errstr));
269
270        if ($is_primary_placement) {
271            # Delete all the secondary placements, so each of the remaining
272            # iterations of the loop make a brand new placement.
273            my @old_places = MT::Placement->load({
274                entry_id => $entry->id,
275                is_primary => 0,
276            });
277            for my $place (@old_places) {
278                $place->remove;
279            }
280        }
281
282        $is_primary_placement = 0;
283    }
284
285    $changed;
286}
287
288sub _new_entry {
289    my $class = shift;
290    my %param = @_;
291    my($blog_id, $user, $pass, $item, $publish) =
292        @param{qw( blog_id user pass item publish )};
293    my $obj_type = $param{page} ? 'page' : 'entry';
294    die _fault(MT->translate("No blog_id")) unless $blog_id;
295    my $mt = MT::XMLRPCServer::Util::mt_new();   ## Will die if MT->new fails.
296    no_utf8($blog_id, values %$item);
297    for my $f (qw( title description mt_text_more
298                   mt_excerpt mt_keywords mt_tags mt_basename wp_slug )) {
299        next unless defined $item->{$f}; 
300        my $enc = $mt->{cfg}->PublishCharset;
301        $item->{$f} = encode_text($item->{$f}, 'utf-8', $enc);
302        unless ($HAVE_XML_PARSER) {
303            $item->{$f} = decode_html($item->{$f});
304            $item->{$f} =~ s!'!'!g;  #'
305        }
306    }
307    require MT::Blog;
308    my $blog = MT::Blog->load($blog_id)
309        or die _fault(MT->translate("Invalid blog ID '[_1]'", $blog_id));
310    my($author, $perms) = $class->_login($user, $pass, $blog_id);
311    die _fault(MT->translate("Invalid login")) unless $author;
312    die _fault(MT->translate("Permission denied.")) unless $perms && $perms->can_create_post;
313    my $entry = MT->model($obj_type)->new;
314    my $orig_entry = $entry->clone;
315    $entry->blog_id($blog_id);
316    $entry->author_id($author->id);
317
318    ## In 2.1 we changed the behavior of the $publish flag. Previously,
319    ## it was used to determine the post status. That was a bad idea.
320    ## So now entries added through XML-RPC are always set to publish,
321    ## *unless* the user has set "NoPublishMeansDraft 1" in mt.cfg, which
322    ## enables the old behavior.
323    if ($mt->{cfg}->NoPublishMeansDraft) {
324        $entry->status($publish && $perms->can_publish_post ? MT::Entry::RELEASE() : MT::Entry::HOLD());
325    } else {
326        $entry->status($perms->can_publish_post ? MT::Entry::RELEASE() : MT::Entry::HOLD() );
327    }
328    $entry->allow_comments($blog->allow_comments_default);
329    $entry->allow_pings($blog->allow_pings_default);
330    $entry->convert_breaks(defined $item->{mt_convert_breaks} ? $item->{mt_convert_breaks} : $blog->convert_paras);
331    $entry->allow_comments($item->{mt_allow_comments})
332        if exists $item->{mt_allow_comments};
333    $entry->title($item->{title})
334        if exists $item->{title};
335
336    $class->_apply_basename($entry, $item, \%param);
337
338    $entry->text($item->{description});
339    for my $field (qw( allow_pings )) {
340        my $val = $item->{"mt_$field"};
341        next unless defined $val;
342        die _fault(MT->translate("Value for 'mt_[_1]' must be either 0 or 1 (was '[_2]')", $field, $val))
343            unless $val == 0 || $val == 1;
344        $entry->$field($val);
345    }
346    $entry->excerpt($item->{mt_excerpt}) if $item->{mt_excerpt};
347    $entry->text_more($item->{mt_text_more}) if $item->{mt_text_more};
348    $entry->keywords($item->{mt_keywords}) if $item->{mt_keywords};
349
350    if (my $tags = $item->{mt_tags}) {
351        require MT::Tag;
352        my $tag_delim = chr($author->entry_prefs->{tag_delim});
353        my @tags = MT::Tag->split($tag_delim, $tags);
354        $entry->set_tags(@tags);
355    }
356    if (my $urls = $item->{mt_tb_ping_urls}) {
357        no_utf8(@$urls);
358        $entry->to_ping_urls(join "\n", @$urls);
359    }
360    if (my $iso = $item->{dateCreated}) {
361        $entry->authored_on(MT::XMLRPCServer::Util::iso2ts($blog, $iso))
362            || die MT::XMLRPCServer::_fault(MT->translate("Invalid timestamp format"));
363        require MT::DateTime;
364        $entry->status(MT::Entry::FUTURE())
365            if ($entry->status == MT::Entry::RELEASE()) &&
366                (MT::DateTime->compare(
367                    blog => $blog,
368                    a => $entry->authored_on,
369                    b => { value => time(), type => 'epoch' } ) > 0);
370    }
371    $entry->discover_tb_from_entry();
372
373    MT->run_callbacks("api_pre_save.$obj_type", $mt, $entry, $orig_entry)
374        || die MT::XMLRPCServer::_fault(MT->translate("PreSave failed [_1]", MT->errstr));
375
376    $entry->save;
377
378    my $changed = $class->_save_placements($entry, $item, \%param);
379
380    my @types = ( $obj_type );
381    if ($changed) {
382        push @types, 'folder'; # folders are the only type that can be
383                               # created in _save_placements
384    }
385    $blog->touch( @types );
386    $blog->save;
387
388    MT->run_callbacks("api_post_save.$obj_type", $mt, $entry, $orig_entry);
389
390    require MT::Log;
391    $mt->log({
392        message => $mt->translate("User '[_1]' (user #[_2]) added [lc,_4] #[_3]", $author->name, $author->id, $entry->id, $entry->class_label),
393        level => MT::Log::INFO(),
394        class => $obj_type,
395        category => 'new',
396        metadata => $entry->id
397    });
398
399    if ($publish) {
400        $class->_publish($mt, $entry) or die _fault($class->errstr);
401    }
402    delete $ENV{SERVER_SOFTWARE};
403    SOAP::Data->type(string => $entry->id);
404}
405
406sub newPost {
407    my $class = shift;
408    my($appkey, $blog_id, $user, $pass, $item, $publish);
409    if ($class eq 'blogger') {
410        ($appkey, $blog_id, $user, $pass, my($content), $publish) = @_;
411        $item->{description} = $content;
412    } else {
413        ($blog_id, $user, $pass, $item, $publish) = @_;
414    }
415    $class->_new_entry( blog_id => $blog_id, user => $user, pass => $pass,
416        item => $item, publish => $publish );
417}
418
419sub newPage {
420    my $class = shift;
421    my ($blog_id, $user, $pass, $item, $publish) = @_;
422    $class->_new_entry( blog_id => $blog_id, user => $user, pass => $pass,
423        item => $item, publish => $publish, page => 1 );
424}
425
426sub _edit_entry {
427    my $class = shift;
428    my %param = @_;
429    my ($blog_id, $entry_id, $user, $pass, $item, $publish) =
430        @param{qw( blog_id entry_id user pass item publish )};
431    my $obj_type = $param{page} ? 'page' : 'entry';
432    die _fault(MT->translate("No entry_id")) unless $entry_id;
433    my $mt = MT::XMLRPCServer::Util::mt_new();   ## Will die if MT->new fails.
434    no_utf8(values %$item);
435    for my $f (qw( title description mt_text_more
436                   mt_excerpt mt_keywords mt_tags mt_basename wp_slug )) {
437        next unless defined $item->{$f}; 
438        my $enc = $mt->config('PublishCharset');
439        $item->{$f} = encode_text($item->{$f}, 'utf-8', $enc);
440        unless ($HAVE_XML_PARSER) {
441            $item->{$f} = decode_html($item->{$f});
442            $item->{$f} =~ s!'!'!g;  #'
443        }
444    }
445    my $entry = MT->model($obj_type)->load($entry_id)
446        or die _fault(MT->translate("Invalid entry ID '[_1]'", $entry_id));
447    if ($blog_id && $blog_id != $entry->blog_id) {
448        die _fault(MT->translate("Invalid entry ID '[_1]'", $entry_id));
449    }
450    require MT::Blog;
451    my $blog = MT::Blog->load($blog_id)
452        or die _fault(MT->translate("Invalid blog ID '[_1]'", $blog_id));
453    my($author, $perms) = $class->_login($user, $pass, $entry->blog_id);
454    die _fault(MT->translate("Invalid login")) unless $author;
455    die _fault(MT->translate("Not privileged to edit entry"))
456        unless $perms && $perms->can_edit_entry($entry, $author);
457    my $orig_entry = $entry->clone;
458    $entry->status(MT::Entry::RELEASE()) if $publish && $perms->can_publish_post;
459    $entry->title($item->{title}) if $item->{title};
460
461    $class->_apply_basename($entry, $item, \%param);
462
463    $entry->text($item->{description});
464    $entry->convert_breaks($item->{mt_convert_breaks})
465        if exists $item->{mt_convert_breaks};
466    $entry->allow_comments($item->{mt_allow_comments})
467        if exists $item->{mt_allow_comments};
468    for my $field (qw( allow_pings )) {
469        my $val = $item->{"mt_$field"};
470        next unless defined $val;
471        die _fault(MT->translate("Value for 'mt_[_1]' must be either 0 or 1 (was '[_2]')", $field, $val))
472            unless $val == 0 || $val == 1;
473        $entry->$field($val);
474    }
475    $entry->excerpt($item->{mt_excerpt}) if defined $item->{mt_excerpt};
476    $entry->text_more($item->{mt_text_more}) if defined $item->{mt_text_more};
477    $entry->keywords($item->{mt_keywords}) if defined $item->{mt_keywords};
478    if (my $tags = $item->{mt_tags}) {
479        require MT::Tag;
480        my $tag_delim = chr($author->entry_prefs->{tag_delim});
481        my @tags = MT::Tag->split($tag_delim, $tags);
482        $entry->set_tags(@tags);
483    }
484    if (my $urls = $item->{mt_tb_ping_urls}) {
485        no_utf8(@$urls);
486        $entry->to_ping_urls(join "\n", @$urls);
487    }
488    if (my $iso = $item->{dateCreated}) {
489        $entry->authored_on(MT::XMLRPCServer::Util::iso2ts($entry->blog_id, $iso))
490           || die MT::XMLRPCServer::_fault(MT->translate("Invalid timestamp format"));
491        require MT::DateTime;
492        $entry->status(MT::Entry::FUTURE())
493            if MT::DateTime->compare(
494                a => $entry->authored_on,
495                b => { value => time(), type => 'epoch' } ) > 0;
496    }
497    $entry->discover_tb_from_entry();
498
499    MT->run_callbacks("api_pre_save.$obj_type", $mt, $entry,
500        $orig_entry)
501        || die MT::XMLRPCServer::_fault(MT->translate("PreSave failed [_1]", MT->errstr));
502
503    $entry->save;
504
505    my $changed = $class->_save_placements($entry, $item, \%param);
506    my @types = ( $obj_type );
507    if ($changed) {
508        push @types, 'folder'; # folders are the only type that can be
509                               # created in _save_placements
510    }
511    $blog->touch( @types );
512
513    MT->run_callbacks("api_post_save.$obj_type", $mt, $entry,
514        $orig_entry);
515
516    require MT::Log;
517    $mt->log({
518        message => $mt->translate("User '[_1]' (user #[_2]) edited [lc,_4] #[_3]", $author->name, $author->id, $entry->id, $entry->class_label),
519        level => MT::Log::INFO(),
520        class => $obj_type,
521        category => 'new',
522        metadata => $entry->id
523    });
524
525    if ($publish) {
526        $class->_publish($mt, $entry) or die _fault($class->errstr);
527    }
528    SOAP::Data->type(boolean => 1);
529}
530
531sub editPost {
532    my $class = shift;
533    my($entry_id, $user, $pass, $item, $publish);
534    if ($class eq 'blogger') {
535        (my($appkey), $entry_id, $user, $pass, my($content), $publish) = @_;
536        $item->{description} = $content;
537    }
538    else {
539        ($entry_id, $user, $pass, $item, $publish) = @_;
540    }
541    $class->_edit_entry( entry_id => $entry_id, user => $user, pass => $pass,
542        item => $item, publish => $publish );
543}
544
545sub editPage {
546    my $class = shift;
547    my($blog_id, $entry_id, $user, $pass, $item, $publish) = @_;
548    $class->_edit_entry( blog_id => $blog_id, entry_id => $entry_id,
549        user => $user, pass => $pass, item => $item, publish => $publish,
550        page => 1 );
551}
552
553sub getUsersBlogs {
554    my $class;
555    if (UNIVERSAL::isa($_[0] => __PACKAGE__)) {
556        $class = shift;
557    } else {
558        $class = __PACKAGE__;
559    }
560    my($appkey, $user, $pass) = @_;
561    my $mt = MT::XMLRPCServer::Util::mt_new();   ## Will die if MT->new fails.
562    my($author) = $class->_login($user, $pass);
563    die _fault(MT->translate("Invalid login")) unless $author;
564    require MT::Permission;
565    require MT::Blog;
566    my $iter = MT::Permission->load_iter({ author_id => $author->id });
567    my @res;
568    while (my $perms = $iter->()) {
569        next unless $perms->can_create_post;
570        my $blog = MT::Blog->load($perms->blog_id);
571        next unless $blog;
572        push @res, { url => SOAP::Data->type(string => $blog->site_url),
573                     blogid => SOAP::Data->type(string => $blog->id),
574                     blogName => SOAP::Data->type(string => encode_text($blog->name, undef, 'utf-8')) };
575    }
576    \@res;
577}
578
579sub getUserInfo {
580    my $class;
581    if (UNIVERSAL::isa($_[0] => __PACKAGE__)) {
582        $class = shift;
583    } else {
584        $class = __PACKAGE__;
585    }
586    my($appkey, $user, $pass) = @_;
587    my $mt = MT::XMLRPCServer::Util::mt_new();   ## Will die if MT->new fails.
588    my($author) = $class->_login($user, $pass);
589    die _fault(MT->translate("Invalid login")) unless $author;
590    my($fname, $lname) = map { encode_text($_, undef, 'utf-8') } split /\s+/, $author->name;
591    $lname ||= '';
592    { userid => SOAP::Data->type(string => $author->id),
593      firstname => SOAP::Data->type(string => $fname),
594      lastname => SOAP::Data->type(string => $lname),
595      nickname => SOAP::Data->type(string => encode_text($author->nickname, undef, 'utf-8')),
596      email => SOAP::Data->type(string => $author->email),
597      url => SOAP::Data->type(string => $author->url) };
598}
599
600sub _get_entries {
601    my $class = shift;
602    my %param = @_;
603    my($blog_id, $user, $pass, $num, $titles_only) =
604        @param{qw( blog_id user pass num titles_only )};
605    my $obj_type = $param{page} ? 'page' : 'entry';
606    my $mt = MT::XMLRPCServer::Util::mt_new();   ## Will die if MT->new fails.
607    my($author, $perms) = $class->_login($user, $pass, $blog_id);
608    die _fault(MT->translate("Invalid login")) unless $author;
609    die _fault(MT->translate("Permission denied.")) unless $perms && $perms->can_create_post;
610    my $iter = MT->model($obj_type)->load_iter({ blog_id => $blog_id },
611        { 'sort' => 'authored_on',
612          direction => 'descend',
613          limit => $num });
614    my @res;
615    while (my $entry = $iter->()) {
616        my $co = sprintf "%04d%02d%02dT%02d:%02d:%02d",
617            unpack 'A4A2A2A2A2A2', $entry->authored_on;
618        my $row = { dateCreated => SOAP::Data->type(dateTime => $co),
619                    userid => SOAP::Data->type(string => $entry->author_id) };
620        $row->{ $param{page} ? 'page_id' : 'postid' } =
621            SOAP::Data->type(string => $entry->id);
622        if ($class eq 'blogger') {
623            $row->{content} = SOAP::Data->type(string => encode_text($entry->text, undef, 'utf-8'));
624        } else {
625            $row->{title} = SOAP::Data->type(string => encode_text($entry->title, undef, 'utf-8'));
626            unless ($titles_only) {
627                require MT::Tag;
628                my $tag_delim = chr($author->entry_prefs->{tag_delim});
629                my $tags = MT::Tag->join($tag_delim, $entry->tags);
630                $row->{description} = SOAP::Data->type(string => encode_text($entry->text, undef, 'utf-8'));
631                my $link = $entry->permalink;
632                $row->{link} = SOAP::Data->type(string => $link);
633                $row->{permaLink} = SOAP::Data->type(string => $link),
634                $row->{mt_basename} = SOAP::Data->type(string => encode_text($entry->basename, undef, 'utf-8'));
635                $row->{mt_allow_comments} = SOAP::Data->type(int => $entry->allow_comments);
636                $row->{mt_allow_pings} = SOAP::Data->type(int => $entry->allow_pings);
637                $row->{mt_convert_breaks} = SOAP::Data->type(string => $entry->convert_breaks);
638                $row->{mt_text_more} = SOAP::Data->type(string => encode_text($entry->text_more, undef, 'utf-8'));
639                $row->{mt_excerpt} = SOAP::Data->type(string => encode_text($entry->excerpt, undef, 'utf-8'));
640                $row->{mt_keywords} = SOAP::Data->type(string => encode_text($entry->keywords, undef, 'utf-8'));
641                $row->{mt_tags} = SOAP::Data->type(string => encode_text($tags, undef, 'utf-8'));
642            }
643        }
644        push @res, $row;
645    }
646    \@res;
647}
648
649sub getRecentPosts {
650    my $class = shift;
651    my ($blog_id, $user, $pass, $num);
652    if ($class eq 'blogger') {
653        (my($appkey), $blog_id, $user, $pass, $num) = @_;
654    }
655    else {
656        ($blog_id, $user, $pass, $num) = @_;
657    }
658    $class->_get_entries( blog_id => $blog_id, user => $user,
659        pass => $pass, num => $num );
660}
661
662sub getRecentPostTitles {
663    my $class = shift;
664    my ($blog_id, $user, $pass, $num) = @_;
665    $class->_get_entries( blog_id => $blog_id, user => $user,
666        pass => $pass, num => $num, titles_only => 1 );
667}
668
669sub getPages {
670    my $class = shift;
671    my ($blog_id, $user, $pass) = @_;
672    $class->_get_entries( blog_id => $blog_id, user => $user,
673        pass => $pass, page => 1 );
674}
675
676sub _delete_entry {
677    my $class = shift;
678    my %param = @_;
679    my ($blog_id, $entry_id, $user, $pass, $publish) =
680        @param{qw( blog_id entry_id user pass publish )};
681    my $mt = MT::XMLRPCServer::Util::mt_new();   ## Will die if MT->new fails.
682    my $obj_type = $param{page} ? 'page' : 'entry';
683    my $entry = MT->model($obj_type)->load($entry_id)
684        or die _fault(MT->translate("Invalid entry ID '[_1]'", $entry_id));
685    my($author, $perms) = $class->_login($user, $pass, $entry->blog_id);
686    die _fault(MT->translate("Invalid login")) unless $author;
687    die _fault(MT->translate("Permission denied."))
688        unless $perms && $perms->can_edit_entry($entry, $author);
689    $entry->remove;
690
691    $mt->log({
692        message => $mt->translate("Entry '[_1]' ([lc,_5] #[_2]) deleted by '[_3]' (user #[_4]) from xml-rpc", $entry->title, $entry->id, $author->name, $author->id, $entry->class_label),
693        level => MT::Log::INFO(),
694        class => 'system',
695        category => 'delete' 
696    });
697
698    if ($publish) {
699        $class->_publish($mt, $entry, 1) or die _fault($class->errstr);
700    }
701    SOAP::Data->type(boolean => 1);
702}
703
704sub deletePost {
705    my $class;
706    if (UNIVERSAL::isa($_[0] => __PACKAGE__)) {
707        $class = shift;
708    } else {
709        $class = __PACKAGE__;
710    }
711    my($appkey, $entry_id, $user, $pass, $publish) = @_;
712    $class->_delete_entry( entry_id => $entry_id, user => $user,
713        pass => $pass, publish => $publish );
714}
715
716sub deletePage {
717    my $class = shift;
718    my ($blog_id, $user, $pass, $entry_id) = @_;
719    $class->_delete_entry( blog_id => $blog_id, entry_id => $entry_id,
720        user => $user, pass => $pass, publish => 1, page => 1 );
721}
722
723sub _get_entry {
724    my $class = shift;
725    my %param = @_;
726    my($blog_id, $entry_id, $user, $pass) =
727        @param{qw( blog_id entry_id user pass )};
728    my $obj_type = $param{page} ? 'page' : 'entry';
729    my $mt = MT::XMLRPCServer::Util::mt_new();   ## Will die if MT->new fails.
730    my $entry = MT->model($obj_type)->load($entry_id)
731        or die _fault(MT->translate("Invalid entry ID '[_1]'", $entry_id));
732    if ($blog_id && $blog_id != $entry->blog_id) {
733        die _fault(MT->translate("Invalid entry ID '[_1]'", $entry_id));
734    }
735    my($author, $perms) = $class->_login($user, $pass, $entry->blog_id);
736    die _fault(MT->translate("Invalid login")) unless $author;
737    die _fault(MT->translate("Not privileged to get entry"))
738        unless $perms && $perms->can_edit_entry($entry, $author);
739    my $co = sprintf "%04d%02d%02dT%02d:%02d:%02d",
740        unpack 'A4A2A2A2A2A2', $entry->authored_on;
741    my $link = $entry->permalink;
742    require MT::Tag;
743    my $delim = chr($author->entry_prefs->{tag_delim});
744    my $tags = MT::Tag->join($delim, $entry->tags);
745
746    my $cats = [];
747    my $cat_data = $entry->__load_category_data();
748    if (scalar @$cat_data) {
749        my ($first_cat) = grep {  $_->[1] } @$cat_data;
750        my @cat_ids     = grep { !$_->[1] } @$cat_data;
751        unshift @cat_ids, $first_cat if $first_cat;
752        $cats = MT->model('category')->lookup_multi(
753            [ map { $_->[0] } @cat_ids ]);
754    }
755
756    my $basename = SOAP::Data->type(string => encode_text($entry->basename, undef, 'utf-8'));
757    {
758        dateCreated => SOAP::Data->type(dateTime => $co),
759        userid => SOAP::Data->type(string => $entry->author_id),
760        ($param{page} ? 'page_id' : 'postid') => SOAP::Data->type(string => $entry->id),
761        description => SOAP::Data->type(string => encode_text($entry->text, undef, 'utf-8')),
762        title => SOAP::Data->type(string => encode_text($entry->title, undef, 'utf-8')),
763        mt_basename => $basename,
764        wp_slug => $basename,
765        link => SOAP::Data->type(string => $link),
766        permaLink => SOAP::Data->type(string => $link),
767        categories => [ map { SOAP::Data->type(string => $_->label) } @$cats ],
768        mt_allow_comments => SOAP::Data->type(int => $entry->allow_comments),
769        mt_allow_pings => SOAP::Data->type(int => $entry->allow_pings),
770        mt_convert_breaks => SOAP::Data->type(string => $entry->convert_breaks),
771        mt_text_more => SOAP::Data->type(string => encode_text($entry->text_more, undef, 'utf-8')),
772        mt_excerpt => SOAP::Data->type(string => encode_text($entry->excerpt, undef, 'utf-8')),
773        mt_keywords => SOAP::Data->type(string => encode_text($entry->keywords, undef, 'utf-8')),
774        mt_tags => SOAP::Data->type(string => encode_text($tags, undef, 'utf-8')),
775    }
776}
777
778sub getPost {
779    my $class = shift;
780    my($entry_id, $user, $pass) = @_;
781    $class->_get_entry( entry_id => $entry_id, user => $user, pass => $pass );
782}
783
784sub getPage {
785    my $class = shift;
786    my ($blog_id, $entry_id, $user, $pass) = @_;
787    $class->_get_entry( blog_id => $blog_id, entry_id => $entry_id,
788        user => $user, pass => $pass, page => 1 );
789}
790
791sub supportedMethods {
792    [ 'blogger.newPost', 'blogger.editPost', 'blogger.getRecentPosts',
793      'blogger.getUsersBlogs', 'blogger.getUserInfo', 'blogger.deletePost',
794      'metaWeblog.getPost', 'metaWeblog.newPost', 'metaWeblog.editPost',
795      'metaWeblog.getRecentPosts', 'metaWeblog.newMediaObject',
796      'metaWeblog.getCategories', 'metaWeblog.deletePost',
797      'metaWeblog.getUsersBlogs',
798      'wp.newPage', 'wp.getPages', 'wp.getPage', 'wp.editPage',
799      'wp.deletePage',
800       # not yet supported: metaWeblog.getTemplate, metaWeblog.setTemplate
801      'mt.getCategoryList', 'mt.setPostCategories', 'mt.getPostCategories',
802      'mt.getTrackbackPings', 'mt.supportedTextFilters',
803      'mt.getRecentPostTitles', 'mt.publishPost', 'mt.getTagList' ];
804}
805
806sub supportedTextFilters {
807    my $mt = MT::XMLRPCServer::Util::mt_new();   ## Will die if MT->new fails.
808    my $filters = $mt->all_text_filters;
809    my @res;
810    for my $filter (keys %$filters) {
811        my $label = $filters->{$filter}{label};
812        if ('CODE' eq ref($label)) {
813            $label = $label->();
814        }
815        push @res, {
816            key => SOAP::Data->type(string => $filter),
817            label => SOAP::Data->type(string => $label)
818        };
819    }
820    \@res;
821}
822
823## getCategoryList, getPostCategories, and setPostCategories were
824## originally written by Daniel Drucker with the assistance of
825## Six Apart, then later modified by Six Apart.
826
827sub getCategoryList {
828    my $class = shift;
829    my($blog_id, $user, $pass) = @_;
830    my $mt = MT::XMLRPCServer::Util::mt_new();   ## Will die if MT->new fails.
831    my($author, $perms) = $class->_login($user, $pass, $blog_id);
832    die _fault(MT->translate("Invalid login")) unless $author;
833    die _fault(MT->translate("Permission denied."))
834        unless $perms && $perms->can_create_post;
835    require MT::Category;
836    my $iter = MT::Category->load_iter({ blog_id => $blog_id });
837    my @data;
838    while (my $cat = $iter->()) {
839        push @data, {
840            categoryName => SOAP::Data->type(string => encode_text($cat->label, undef, 'utf-8')),
841            categoryId => SOAP::Data->type(string => $cat->id)
842        };
843    }
844    \@data;
845}
846
847sub getCategories {
848    my $class = shift;
849    my($blog_id, $user, $pass) = @_;
850    my $mt = MT::XMLRPCServer::Util::mt_new();   ## Will die if MT->new fails.
851    my($author, $perms) = $class->_login($user, $pass, $blog_id);
852    die _fault(MT->translate("Invalid login")) unless $author;
853    die _fault(MT->translate("Permission denied."))
854        unless $perms && $perms->can_create_post;
855    require MT::Category;
856    my $iter = MT::Category->load_iter({ blog_id => $blog_id });
857    my @data;
858    my $blog = MT::Blog->load($blog_id);
859    require File::Spec;
860    while (my $cat = $iter->()) {
861        my $url = File::Spec->catfile($blog->site_url, archive_file_for( undef, $blog, 'Category', $cat ));
862        push @data, {
863            categoryId => SOAP::Data->type(string => encode_text($cat->id, undef, 'utf-8')),
864            parentId => ($cat->parent_category ? SOAP::Data->type(string => encode_text($cat->parent_category->id, undef, 'utf-8')) : undef),
865            categoryName => SOAP::Data->type(string => encode_text($cat->label, undef, 'utf-8')),
866            title => SOAP::Data->type(string => encode_text($cat->label, undef, 'utf-8')),
867            description => SOAP::Data->type(string => encode_text($cat->description, undef, 'utf-8')),
868            htmlUrl => SOAP::Data->type(string => encode_text($url, undef, 'utf-8')),
869        };
870    }
871    \@data;
872}
873
874sub getTagList {
875    my $class = shift;
876    my($blog_id, $user, $pass) = @_;
877    my $mt = MT::XMLRPCServer::Util::mt_new();   ## Will die if MT->new fails.
878    my($author, $perms) = $class->_login($user, $pass, $blog_id);
879    die _fault(MT->translate("Invalid login")) unless $author;
880    die _fault(MT->translate("Permission denied."))
881        unless $perms && $perms->can_create_post;
882    require MT::Tag;
883    require MT::ObjectTag;
884    my $iter = MT::Tag->load_iter(undef, { join => ['MT::ObjectTag', 'tag_id', { blog_id => $blog_id }, { unique => 1 } ] } );
885    my @data;
886    while (my $tag = $iter->()) {
887        push @data, {
888            tagName => SOAP::Data->type(string => encode_text($tag->name, undef, 'utf-8')),
889            tagId => SOAP::Data->type(string => $tag->id)
890        };
891    }
892    \@data;
893}
894
895sub getPostCategories {
896    my $class = shift;
897    my($entry_id, $user, $pass) = @_;
898    my $mt = MT::XMLRPCServer::Util::mt_new();   ## Will die if MT->new fails.
899    require MT::Entry;
900    my $entry = MT::Entry->load($entry_id)
901        or die _fault(MT->translate("Invalid entry ID '[_1]'", $entry_id));
902    my($author, $perms) = $class->_login($user, $pass, $entry->blog_id);
903    die _fault(MT->translate("Invalid login")) unless $author;
904    die _fault(MT->translate("Permission denied.")) unless $perms && $perms->can_create_post;
905    my @data;
906    my $prim = $entry->category;
907    my $cats = $entry->categories;
908    for my $cat (@$cats) {
909        my $is_primary = $prim && $cat->id == $prim->id ? 1 : 0;
910        push @data, {
911            categoryName => SOAP::Data->type(string => encode_text($cat->label, undef, 'utf-8')),
912            categoryId => SOAP::Data->type(string => $cat->id),
913            isPrimary => SOAP::Data->type(boolean => $is_primary),
914        };
915    }
916    \@data;
917}
918
919sub setPostCategories {
920    my $class = shift;
921    my($entry_id, $user, $pass, $cats) = @_;
922    my $mt = MT::XMLRPCServer::Util::mt_new();   ## Will die if MT->new fails.
923    require MT::Entry;
924    require MT::Placement;
925    my $entry = MT::Entry->load($entry_id)
926         or die _fault(MT->translate("Invalid entry ID '[_1]'", $entry_id));
927    my($author, $perms) = $class->_login($user, $pass, $entry->blog_id);
928    die _fault(MT->translate("Invalid login")) unless $author;
929    die _fault(MT->translate("Not privileged to set entry categories"))
930        unless $perms && $perms->can_edit_entry($entry, $author);
931    my @place = MT::Placement->load({ entry_id => $entry_id });
932    for my $place (@place) {
933         $place->remove;
934    }
935    ## Keep track of which category is named the primary category.
936    ## If the first structure in the array does not have an isPrimary
937    ## key, we just make it the primary category; if it does, we use
938    ## that flag to determine the primary category.
939    my $is_primary = 1;
940    for my $cat (@$cats) {
941         my $place = MT::Placement->new;
942         $place->entry_id($entry_id);
943         $place->blog_id($entry->blog_id);
944         if (defined $cat->{isPrimary} && $is_primary) {
945             $place->is_primary($cat->{isPrimary});
946         } else {
947             $place->is_primary($is_primary);
948         }
949         ## If we just set the is_primary flag to 1, we don't want to
950         ## make any other categories primary.
951         $is_primary = 0 if $place->is_primary;
952         $place->category_id($cat->{categoryId});
953         $place->save
954              or die _fault(MT->translate("Saving placement failed: [_1]", $place->errstr));
955    }
956    SOAP::Data->type(boolean => 1);
957}
958
959sub getTrackbackPings {
960    my $class = shift;
961    my($entry_id) = @_;
962    require MT::Trackback;
963    require MT::TBPing;
964    my $mt = MT::XMLRPCServer::Util::mt_new();   ## Will die if MT->new fails.
965    my $tb = MT::Trackback->load({ entry_id => $entry_id })
966        or return [];
967    my $iter = MT::TBPing->load_iter({ tb_id => $tb->id });
968    my @data;
969    while (my $ping = $iter->()) {
970        push @data, {
971            pingTitle => SOAP::Data->type(string => encode_text($ping->title, undef, 'utf-8')),
972            pingURL => SOAP::Data->type(string => $ping->source_url),
973            pingIP => SOAP::Data->type(string => $ping->ip),
974        };
975    }
976    \@data;
977}
978
979sub publishPost {
980    my $class = shift;
981    my($entry_id, $user, $pass) = @_;
982    my $mt = MT::XMLRPCServer::Util::mt_new();   ## Will die if MT->new fails.
983    require MT::Entry;
984    my $entry = MT::Entry->load($entry_id)
985        or die _fault(MT->translate("Invalid entry ID '[_1]'", $entry_id));
986    my($author, $perms) = $class->_login($user, $pass, $entry->blog_id);
987    die _fault(MT->translate("Invalid login")) unless $author;
988    die _fault(MT->translate("Not privileged to edit entry"))
989        unless $perms && $perms->can_edit_entry($entry, $author);
990    $mt->rebuild_entry( Entry => $entry, BuildDependencies => 1 )
991        or die _fault(MT->translate("Publish failed: [_1]", $mt->errstr));
992    SOAP::Data->type(boolean => 1);
993}
994
995sub runPeriodicTasks {
996    my $class = shift;
997    my ($user, $pass) = @_;
998
999    my $mt = MT::XMLRPCServer::Util::mt_new();
1000    my $author = $class->_login($user, $pass);
1001    die _fault(MT->translate("Invalid login")) unless $author;
1002
1003    $mt->run_tasks;
1004
1005    { responseCode => 'success' }
1006}
1007
1008sub publishScheduledFuturePosts {
1009    my $class = shift;
1010    my ($blog_id, $user, $pass) = @_;
1011
1012    my $mt = MT::XMLRPCServer::Util::mt_new();
1013    my $author = $class->_login($user, $pass);
1014    die _fault(MT->translate("Invalid login")) unless $author;
1015    my $blog = MT::Blog->load($blog_id)
1016        or die _fault(MT->translate('Can\'t load blog #[_1].', $blog_id));
1017
1018    my $now = time;
1019    # Convert $now to user's timezone, which is how future post dates
1020    # are stored.
1021    $now = MT::Util::offset_time($now);
1022    $now = strftime("%Y%m%d%H%M%S", gmtime($now));
1023
1024    my $iter = MT::Entry->load_iter({
1025        blog_id => $blog->id,
1026        class => '*',
1027        status => MT::Entry::FUTURE()},
1028        {'sort' => 'authored_on',
1029         direction => 'descend'
1030    });
1031    my @queue;
1032    while (my $i = $iter->()) {
1033        push @queue, $i->id();
1034    }
1035
1036    my $changed = 0;
1037    my $total_changed = 0;
1038    my @results;
1039    my %types;
1040    foreach my $entry_id (@queue) {
1041        my $entry = MT::Entry->load($entry_id);
1042        if ($entry && $entry->authored_on <= $now) {
1043            $entry->status(MT::Entry::RELEASE());
1044            $entry->discover_tb_from_entry();
1045            $entry->save
1046                or die $entry->errstr;
1047
1048            $types{$entry->class} = 1;
1049            start_background_task(sub {
1050                $mt->rebuild_entry( Entry => $entry, Blog => $blog )
1051                    or die $mt->errstr;
1052            });
1053            $changed++;
1054            $total_changed++;
1055        }
1056    }
1057    $blog->touch( keys %types ) if $changed;
1058    $blog->save if $changed && (keys %types);
1059
1060    if ($changed) {
1061        $mt->rebuild_indexes( Blog => $blog )
1062            or die $mt->errstr;
1063    }
1064    { responseCode => 'success', 
1065      publishedCount => $total_changed,
1066    };
1067}
1068
1069sub getNextScheduled {
1070    my $class = shift;
1071    my ($user, $pass) = @_;
1072
1073    my $mt = MT::XMLRPCServer::Util::mt_new();
1074    my $author = $class->_login($user, $pass);
1075    die _fault(MT->translate("Invalid login")) unless $author;
1076
1077    my $next_scheduled = MT::get_next_sched_post_for_user($author->id());
1078
1079    { nextScheduledTime => $next_scheduled };
1080}
1081
1082sub setRemoteAuthToken {
1083    my $class = shift;
1084    my ($user, $pass, $remote_auth_username, $remote_auth_token) = @_;
1085    my $mt = MT::XMLRPCServer::Util::mt_new();   ## Will die if MT->new fails.
1086    my($author) = $class->_login($user, $pass);
1087    die _fault(MT->translate("Invalid login")) unless $author;
1088    $author->remote_auth_username($remote_auth_username);
1089    $author->remote_auth_token($remote_auth_token);
1090    $author->save();
1091    1;
1092}
1093
1094sub newMediaObject {
1095    my $class = shift;
1096    my($blog_id, $user, $pass, $file) = @_;
1097    my $mt = MT::XMLRPCServer::Util::mt_new();   ## Will die if MT->new fails.
1098    my($author, $perms) = $class->_login($user, $pass, $blog_id);
1099    die _fault(MT->translate("Invalid login")) unless $author;
1100    die _fault(MT->translate("Not privileged to upload files"))
1101        unless $perms && $perms->can_upload;
1102    require MT::Blog;
1103    require File::Spec;
1104    my $blog = MT::Blog->load($blog_id)
1105        or die _fault(MT->translate('Can\'t load blog #[_1].', $blog_id));
1106
1107    my $fname = $file->{name} or die _fault(MT->translate("No filename provided"));
1108    if ($fname =~ m!\.\.|\0|\|!) {
1109        die _fault(MT->translate("Invalid filename '[_1]'", $fname));
1110    }
1111    my $local_file = File::Spec->catfile($blog->site_path, $file->{name});
1112    my $fmgr = $blog->file_mgr;
1113    my($vol, $path, $name) = File::Spec->splitpath($local_file);
1114    $path =~ s!/$!! unless $path eq '/';  ## OS X doesn't like / at the end in mkdir().
1115    unless ($fmgr->exists($path)) {
1116        $fmgr->mkpath($path)
1117            or die _fault(MT->translate("Error making path '[_1]': [_2]", $path, $fmgr->errstr));
1118    }
1119    defined(my $bytes = $fmgr->put_data($file->{bits}, $local_file, 'upload'))
1120        or die _fault(MT->translate("Error writing uploaded file: [_1]", $fmgr->errstr));
1121    my $url = $blog->site_url . $fname;
1122
1123    require File::Basename;
1124    my $local_basename = File::Basename::basename($local_file);
1125    my $ext =
1126        ( File::Basename::fileparse( $local_file, qr/[A-Za-z0-9]+$/ ) )[2];
1127    eval { require Image::Size; };
1128    die _fault(MT->translate("Perl module Image::Size is required to determine width and height of uploaded images.")) if $@;
1129    my ( $w, $h, $id ) = Image::Size::imgsize($local_file);
1130
1131    require MT::Asset;
1132    my $asset_pkg = MT::Asset->handler_for_file($local_basename);
1133    my $is_image  = defined($w)
1134      && defined($h)
1135      && $asset_pkg->isa('MT::Asset::Image');
1136    my $asset;
1137    if (!($asset = $asset_pkg->load(
1138                { file_path => $local_file, blog_id => $blog_id })))
1139    {
1140        $asset = $asset_pkg->new();
1141        $asset->file_path($local_file);
1142        $asset->file_name($local_basename);
1143        $asset->file_ext($ext);
1144        $asset->blog_id($blog_id);
1145        $asset->created_by( $author->id );
1146    }
1147    else {
1148        $asset->modified_by( $author->id );
1149    }
1150    my $original = $asset->clone;
1151    $asset->url($url);
1152    if ($is_image) {
1153        $asset->image_width($w);
1154        $asset->image_height($h);
1155    }
1156    $asset->mime_type($file->{type});
1157    $asset->save;
1158
1159    $blog->touch('asset');
1160    $blog->save;
1161
1162    MT->run_callbacks(
1163        'api_upload_file.' . $asset->class,
1164        File => $local_file, file => $local_file,
1165        Url => $url, url => $url,
1166        Size => $bytes, size => $bytes,
1167        Asset => $asset, asset => $asset,
1168        Type => $asset->class, type => $asset->class,
1169        Blog => $blog,blog => $blog);
1170    if ($is_image) {
1171        MT->run_callbacks(
1172            'api_upload_image',
1173            File => $local_file, file => $local_file,
1174            Url => $url, url => $url,
1175            Size => $bytes, size => $bytes,
1176            Asset => $asset, asset => $asset,
1177            Height => $h, height => $h,
1178            Width => $w, width => $w,
1179            Type => 'image', type => 'image',
1180            ImageType => $id, image_type => $id,
1181            Blog => $blog, blog => $blog);
1182    }
1183
1184    { url => SOAP::Data->type(string => $url) };
1185}
1186
1187## getTemplate and setTemplate are not applicable in MT's template
1188## structure, so they are unimplemented (they return a fault).
1189## We assign it twice to get rid of "setTemplate used only once" warnings.
1190
1191sub getTemplate {
1192    die _fault(MT->translate(
1193        "Template methods are not implemented, due to differences between the Blogger API and the Movable Type API."));
1194}
1195*setTemplate = *setTemplate = \&getTemplate;
1196
1197## The above methods will be called as blogger.newPost, blogger.editPost,
1198## etc., because we are implementing Blogger's API. Thus, the empty
1199## subclass.
1200package blogger;
1201BEGIN { @blogger::ISA = qw( MT::XMLRPCServer ); }
1202
1203package metaWeblog;
1204BEGIN { @metaWeblog::ISA = qw( MT::XMLRPCServer ); }
1205
1206package mt;
1207BEGIN { @mt::ISA = qw( MT::XMLRPCServer ); }
1208
1209package wp;
1210BEGIN { @wp::ISA = qw( MT::XMLRPCServer ); }
1211
12121;
1213__END__
1214
1215=head1 NAME
1216
1217MT::XMLRPCServer
1218
1219=head1 SYNOPSIS
1220
1221An XMLRPC API interface for communicating with Movable Type.
1222
1223=head1 CALLBACKS
1224
1225=over 4
1226
1227=item api_pre_save.entry
1228=item api_pre_save.page
1229
1230    callback($eh, $mt, $entry, $original_entry)
1231
1232Called before saving a new or existing entry. If saving a new entry, the
1233$original_entry will have an unassigned 'id'. This callback is executed
1234as a filter, so your handler must return 1 to allow the entry to be saved.
1235
1236=item api_post_save.entry
1237=item api_post_save.page
1238
1239    callback($eh, $mt, $entry, $original_entry)
1240
1241Called after saving a new or existing entry. If saving a new entry, the
1242$original_entry will have an unassigned 'id'.
1243
1244=item api_upload_file
1245
1246    callback($eh, %params)
1247
1248This callback is invoked for each file the user uploads to the weblog.
1249This callback is similar to the CMSUploadFile callback found in
1250C<MT::App::CMS>.
1251
1252=back
1253
1254=head2 Parameters
1255
1256=over 4
1257
1258=item File
1259
1260The full physical file path of the uploaded file.
1261
1262=item Url
1263
1264The full URL to the file that has been uploaded.
1265
1266=item Type
1267
1268For this callback, this value is currently always 'file'.
1269
1270=item Blog
1271
1272The C<MT::Blog> object associated with the newly uploaded file.
1273
1274=back
1275
1276=cut
Note: See TracBrowser for help on using the browser.