root/branches/release-30/lib/MT/XMLRPCServer.pm @ 1419

Revision 1419, 44.9 kB (checked in by breese, 21 months ago)

fixed bug:69143 - XMLRPCServer.pm would croak if user attempted to post an entry with no categories assigned

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