root/branches/release-41/lib/MT/XMLRPCServer.pm @ 2737

Revision 2737, 46.5 kB (checked in by takayama, 17 months ago)

Fixed BugId:80382
* Changed to removes fileinfo records when entry was deleted.

  • CMS, Atom and XMLRPC
  • 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        ($ENV{MT_CONFIG} || $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
690    # Delete archive file
691    my $blog = MT::Blog->load($entry->blog_id);
692    my %recip = $mt->publisher->rebuild_deleted_entry(
693        Entry => $entry,
694        Blog  => $blog);
695
696    # Rebuild archives
697    $mt->rebuild_archives(
698        Blog             => $blog,
699        Recip            => \%recip,
700    ) or die _fault($class->errstr);
701
702    # Remove object
703    $entry->remove;
704
705    $mt->log({
706        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),
707        level => MT::Log::INFO(),
708        class => 'system',
709        category => 'delete' 
710    });
711
712    SOAP::Data->type(boolean => 1);
713}
714
715sub deletePost {
716    my $class;
717    if (UNIVERSAL::isa($_[0] => __PACKAGE__)) {
718        $class = shift;
719    } else {
720        $class = __PACKAGE__;
721    }
722    my($appkey, $entry_id, $user, $pass, $publish) = @_;
723    $class->_delete_entry( entry_id => $entry_id, user => $user,
724        pass => $pass, publish => $publish );
725}
726
727sub deletePage {
728    my $class = shift;
729    my ($blog_id, $user, $pass, $entry_id) = @_;
730    $class->_delete_entry( blog_id => $blog_id, entry_id => $entry_id,
731        user => $user, pass => $pass, publish => 1, page => 1 );
732}
733
734sub _get_entry {
735    my $class = shift;
736    my %param = @_;
737    my($blog_id, $entry_id, $user, $pass) =
738        @param{qw( blog_id entry_id user pass )};
739    my $obj_type = $param{page} ? 'page' : 'entry';
740    my $mt = MT::XMLRPCServer::Util::mt_new();   ## Will die if MT->new fails.
741    my $entry = MT->model($obj_type)->load($entry_id)
742        or die _fault(MT->translate("Invalid entry ID '[_1]'", $entry_id));
743    if ($blog_id && $blog_id != $entry->blog_id) {
744        die _fault(MT->translate("Invalid entry ID '[_1]'", $entry_id));
745    }
746    my($author, $perms) = $class->_login($user, $pass, $entry->blog_id);
747    die _fault(MT->translate("Invalid login")) unless $author;
748    die _fault(MT->translate("Not privileged to get entry"))
749        unless $perms && $perms->can_edit_entry($entry, $author);
750    my $co = sprintf "%04d%02d%02dT%02d:%02d:%02d",
751        unpack 'A4A2A2A2A2A2', $entry->authored_on;
752    my $link = $entry->permalink;
753    require MT::Tag;
754    my $delim = chr($author->entry_prefs->{tag_delim});
755    my $tags = MT::Tag->join($delim, $entry->tags);
756
757    my $cats = [];
758    my $cat_data = $entry->__load_category_data();
759    if (scalar @$cat_data) {
760        my ($first_cat) = grep {  $_->[1] } @$cat_data;
761        my @cat_ids     = grep { !$_->[1] } @$cat_data;
762        unshift @cat_ids, $first_cat if $first_cat;
763        $cats = MT->model('category')->lookup_multi(
764            [ map { $_->[0] } @cat_ids ]);
765    }
766
767    my $basename = SOAP::Data->type(string => encode_text($entry->basename, undef, 'utf-8'));
768    {
769        dateCreated => SOAP::Data->type(dateTime => $co),
770        userid => SOAP::Data->type(string => $entry->author_id),
771        ($param{page} ? 'page_id' : 'postid') => SOAP::Data->type(string => $entry->id),
772        description => SOAP::Data->type(string => encode_text($entry->text, undef, 'utf-8')),
773        title => SOAP::Data->type(string => encode_text($entry->title, undef, 'utf-8')),
774        mt_basename => $basename,
775        wp_slug => $basename,
776        link => SOAP::Data->type(string => $link),
777        permaLink => SOAP::Data->type(string => $link),
778        categories => [ map { SOAP::Data->type(string => $_->label) } @$cats ],
779        mt_allow_comments => SOAP::Data->type(int => $entry->allow_comments),
780        mt_allow_pings => SOAP::Data->type(int => $entry->allow_pings),
781        mt_convert_breaks => SOAP::Data->type(string => $entry->convert_breaks),
782        mt_text_more => SOAP::Data->type(string => encode_text($entry->text_more, undef, 'utf-8')),
783        mt_excerpt => SOAP::Data->type(string => encode_text($entry->excerpt, undef, 'utf-8')),
784        mt_keywords => SOAP::Data->type(string => encode_text($entry->keywords, undef, 'utf-8')),
785        mt_tags => SOAP::Data->type(string => encode_text($tags, undef, 'utf-8')),
786    }
787}
788
789sub getPost {
790    my $class = shift;
791    my($entry_id, $user, $pass) = @_;
792    $class->_get_entry( entry_id => $entry_id, user => $user, pass => $pass );
793}
794
795sub getPage {
796    my $class = shift;
797    my ($blog_id, $entry_id, $user, $pass) = @_;
798    $class->_get_entry( blog_id => $blog_id, entry_id => $entry_id,
799        user => $user, pass => $pass, page => 1 );
800}
801
802sub supportedMethods {
803    [ 'blogger.newPost', 'blogger.editPost', 'blogger.getRecentPosts',
804      'blogger.getUsersBlogs', 'blogger.getUserInfo', 'blogger.deletePost',
805      'metaWeblog.getPost', 'metaWeblog.newPost', 'metaWeblog.editPost',
806      'metaWeblog.getRecentPosts', 'metaWeblog.newMediaObject',
807      'metaWeblog.getCategories', 'metaWeblog.deletePost',
808      'metaWeblog.getUsersBlogs',
809      'wp.newPage', 'wp.getPages', 'wp.getPage', 'wp.editPage',
810      'wp.deletePage',
811       # not yet supported: metaWeblog.getTemplate, metaWeblog.setTemplate
812      'mt.getCategoryList', 'mt.setPostCategories', 'mt.getPostCategories',
813      'mt.getTrackbackPings', 'mt.supportedTextFilters',
814      'mt.getRecentPostTitles', 'mt.publishPost', 'mt.getTagList' ];
815}
816
817sub supportedTextFilters {
818    my $mt = MT::XMLRPCServer::Util::mt_new();   ## Will die if MT->new fails.
819    my $filters = $mt->all_text_filters;
820    my @res;
821    for my $filter (keys %$filters) {
822        my $label = $filters->{$filter}{label};
823        if ('CODE' eq ref($label)) {
824            $label = $label->();
825        }
826        push @res, {
827            key => SOAP::Data->type(string => $filter),
828            label => SOAP::Data->type(string => $label)
829        };
830    }
831    \@res;
832}
833
834## getCategoryList, getPostCategories, and setPostCategories were
835## originally written by Daniel Drucker with the assistance of
836## Six Apart, then later modified by Six Apart.
837
838sub getCategoryList {
839    my $class = shift;
840    my($blog_id, $user, $pass) = @_;
841    my $mt = MT::XMLRPCServer::Util::mt_new();   ## Will die if MT->new fails.
842    my($author, $perms) = $class->_login($user, $pass, $blog_id);
843    die _fault(MT->translate("Invalid login")) unless $author;
844    die _fault(MT->translate("Permission denied."))
845        unless $perms && $perms->can_create_post;
846    require MT::Category;
847    my $iter = MT::Category->load_iter({ blog_id => $blog_id });
848    my @data;
849    while (my $cat = $iter->()) {
850        push @data, {
851            categoryName => SOAP::Data->type(string => encode_text($cat->label, undef, 'utf-8')),
852            categoryId => SOAP::Data->type(string => $cat->id)
853        };
854    }
855    \@data;
856}
857
858sub getCategories {
859    my $class = shift;
860    my($blog_id, $user, $pass) = @_;
861    my $mt = MT::XMLRPCServer::Util::mt_new();   ## Will die if MT->new fails.
862    my($author, $perms) = $class->_login($user, $pass, $blog_id);
863    die _fault(MT->translate("Invalid login")) unless $author;
864    die _fault(MT->translate("Permission denied."))
865        unless $perms && $perms->can_create_post;
866    require MT::Category;
867    my $iter = MT::Category->load_iter({ blog_id => $blog_id });
868    my @data;
869    my $blog = MT::Blog->load($blog_id);
870    require File::Spec;
871    while (my $cat = $iter->()) {
872        my $url = File::Spec->catfile($blog->site_url, archive_file_for( undef, $blog, 'Category', $cat ));
873        push @data, {
874            categoryId => SOAP::Data->type(string => encode_text($cat->id, undef, 'utf-8')),
875            parentId => ($cat->parent_category ? SOAP::Data->type(string => encode_text($cat->parent_category->id, undef, 'utf-8')) : undef),
876            categoryName => SOAP::Data->type(string => encode_text($cat->label, undef, 'utf-8')),
877            title => SOAP::Data->type(string => encode_text($cat->label, undef, 'utf-8')),
878            description => SOAP::Data->type(string => encode_text($cat->description, undef, 'utf-8')),
879            htmlUrl => SOAP::Data->type(string => encode_text($url, undef, 'utf-8')),
880        };
881    }
882    \@data;
883}
884
885sub getTagList {
886    my $class = shift;
887    my($blog_id, $user, $pass) = @_;
888    my $mt = MT::XMLRPCServer::Util::mt_new();   ## Will die if MT->new fails.
889    my($author, $perms) = $class->_login($user, $pass, $blog_id);
890    die _fault(MT->translate("Invalid login")) unless $author;
891    die _fault(MT->translate("Permission denied."))
892        unless $perms && $perms->can_create_post;
893    require MT::Tag;
894    require MT::ObjectTag;
895    my $iter = MT::Tag->load_iter(undef, { join => ['MT::ObjectTag', 'tag_id', { blog_id => $blog_id }, { unique => 1 } ] } );
896    my @data;
897    while (my $tag = $iter->()) {
898        push @data, {
899            tagName => SOAP::Data->type(string => encode_text($tag->name, undef, 'utf-8')),
900            tagId => SOAP::Data->type(string => $tag->id)
901        };
902    }
903    \@data;
904}
905
906sub getPostCategories {
907    my $class = shift;
908    my($entry_id, $user, $pass) = @_;
909    my $mt = MT::XMLRPCServer::Util::mt_new();   ## Will die if MT->new fails.
910    require MT::Entry;
911    my $entry = MT::Entry->load($entry_id)
912        or die _fault(MT->translate("Invalid entry ID '[_1]'", $entry_id));
913    my($author, $perms) = $class->_login($user, $pass, $entry->blog_id);
914    die _fault(MT->translate("Invalid login")) unless $author;
915    die _fault(MT->translate("Permission denied.")) unless $perms && $perms->can_create_post;
916    my @data;
917    my $prim = $entry->category;
918    my $cats = $entry->categories;
919    for my $cat (@$cats) {
920        my $is_primary = $prim && $cat->id == $prim->id ? 1 : 0;
921        push @data, {
922            categoryName => SOAP::Data->type(string => encode_text($cat->label, undef, 'utf-8')),
923            categoryId => SOAP::Data->type(string => $cat->id),
924            isPrimary => SOAP::Data->type(boolean => $is_primary),
925        };
926    }
927    \@data;
928}
929
930sub setPostCategories {
931    my $class = shift;
932    my($entry_id, $user, $pass, $cats) = @_;
933    my $mt = MT::XMLRPCServer::Util::mt_new();   ## Will die if MT->new fails.
934    require MT::Entry;
935    require MT::Placement;
936    my $entry = MT::Entry->load($entry_id)
937         or die _fault(MT->translate("Invalid entry ID '[_1]'", $entry_id));
938    my($author, $perms) = $class->_login($user, $pass, $entry->blog_id);
939    die _fault(MT->translate("Invalid login")) unless $author;
940    die _fault(MT->translate("Not privileged to set entry categories"))
941        unless $perms && $perms->can_edit_entry($entry, $author);
942    my @place = MT::Placement->load({ entry_id => $entry_id });
943    for my $place (@place) {
944         $place->remove;
945    }
946    ## Keep track of which category is named the primary category.
947    ## If the first structure in the array does not have an isPrimary
948    ## key, we just make it the primary category; if it does, we use
949    ## that flag to determine the primary category.
950    my $is_primary = 1;
951    for my $cat (@$cats) {
952         my $place = MT::Placement->new;
953         $place->entry_id($entry_id);
954         $place->blog_id($entry->blog_id);
955         if (defined $cat->{isPrimary} && $is_primary) {
956             $place->is_primary($cat->{isPrimary});
957         } else {
958             $place->is_primary($is_primary);
959         }
960         ## If we just set the is_primary flag to 1, we don't want to
961         ## make any other categories primary.
962         $is_primary = 0 if $place->is_primary;
963         $place->category_id($cat->{categoryId});
964         $place->save
965              or die _fault(MT->translate("Saving placement failed: [_1]", $place->errstr));
966    }
967    SOAP::Data->type(boolean => 1);
968}
969
970sub getTrackbackPings {
971    my $class = shift;
972    my($entry_id) = @_;
973    require MT::Trackback;
974    require MT::TBPing;
975    my $mt = MT::XMLRPCServer::Util::mt_new();   ## Will die if MT->new fails.
976    my $tb = MT::Trackback->load({ entry_id => $entry_id })
977        or return [];
978    my $iter = MT::TBPing->load_iter({ tb_id => $tb->id });
979    my @data;
980    while (my $ping = $iter->()) {
981        push @data, {
982            pingTitle => SOAP::Data->type(string => encode_text($ping->title, undef, 'utf-8')),
983            pingURL => SOAP::Data->type(string => $ping->source_url),
984            pingIP => SOAP::Data->type(string => $ping->ip),
985        };
986    }
987    \@data;
988}
989
990sub publishPost {
991    my $class = shift;
992    my($entry_id, $user, $pass) = @_;
993    my $mt = MT::XMLRPCServer::Util::mt_new();   ## Will die if MT->new fails.
994    require MT::Entry;
995    my $entry = MT::Entry->load($entry_id)
996        or die _fault(MT->translate("Invalid entry ID '[_1]'", $entry_id));
997    my($author, $perms) = $class->_login($user, $pass, $entry->blog_id);
998    die _fault(MT->translate("Invalid login")) unless $author;
999    die _fault(MT->translate("Not privileged to edit entry"))
1000        unless $perms && $perms->can_edit_entry($entry, $author);
1001    $mt->rebuild_entry( Entry => $entry, BuildDependencies => 1 )
1002        or die _fault(MT->translate("Publish failed: [_1]", $mt->errstr));
1003    SOAP::Data->type(boolean => 1);
1004}
1005
1006sub runPeriodicTasks {
1007    my $class = shift;
1008    my ($user, $pass) = @_;
1009
1010    my $mt = MT::XMLRPCServer::Util::mt_new();
1011    my $author = $class->_login($user, $pass);
1012    die _fault(MT->translate("Invalid login")) unless $author;
1013
1014    $mt->run_tasks;
1015
1016    { responseCode => 'success' }
1017}
1018
1019sub publishScheduledFuturePosts {
1020    my $class = shift;
1021    my ($blog_id, $user, $pass) = @_;
1022
1023    my $mt = MT::XMLRPCServer::Util::mt_new();
1024    my $author = $class->_login($user, $pass);
1025    die _fault(MT->translate("Invalid login")) unless $author;
1026    my $blog = MT::Blog->load($blog_id)
1027        or die _fault(MT->translate('Can\'t load blog #[_1].', $blog_id));
1028
1029    my $now = time;
1030    # Convert $now to user's timezone, which is how future post dates
1031    # are stored.
1032    $now = MT::Util::offset_time($now);
1033    $now = strftime("%Y%m%d%H%M%S", gmtime($now));
1034
1035    my $iter = MT::Entry->load_iter({
1036        blog_id => $blog->id,
1037        class => '*',
1038        status => MT::Entry::FUTURE()},
1039        {'sort' => 'authored_on',
1040         direction => 'descend'
1041    });
1042    my @queue;
1043    while (my $i = $iter->()) {
1044        push @queue, $i->id();
1045    }
1046
1047    my $changed = 0;
1048    my $total_changed = 0;
1049    my @results;
1050    my %types;
1051    foreach my $entry_id (@queue) {
1052        my $entry = MT::Entry->load($entry_id);
1053        if ($entry && $entry->authored_on <= $now) {
1054            $entry->status(MT::Entry::RELEASE());
1055            $entry->discover_tb_from_entry();
1056            $entry->save
1057                or die $entry->errstr;
1058
1059            $types{$entry->class} = 1;
1060            start_background_task(sub {
1061                $mt->rebuild_entry( Entry => $entry, Blog => $blog )
1062                    or die $mt->errstr;
1063            });
1064            $changed++;
1065            $total_changed++;
1066        }
1067    }
1068    $blog->touch( keys %types ) if $changed;
1069    $blog->save if $changed && (keys %types);
1070
1071    if ($changed) {
1072        $mt->rebuild_indexes( Blog => $blog )
1073            or die $mt->errstr;
1074    }
1075    { responseCode => 'success', 
1076      publishedCount => $total_changed,
1077    };
1078}
1079
1080sub getNextScheduled {
1081    my $class = shift;
1082    my ($user, $pass) = @_;
1083
1084    my $mt = MT::XMLRPCServer::Util::mt_new();
1085    my $author = $class->_login($user, $pass);
1086    die _fault(MT->translate("Invalid login")) unless $author;
1087
1088    my $next_scheduled = MT::get_next_sched_post_for_user($author->id());
1089
1090    { nextScheduledTime => $next_scheduled };
1091}
1092
1093sub setRemoteAuthToken {
1094    my $class = shift;
1095    my ($user, $pass, $remote_auth_username, $remote_auth_token) = @_;
1096    my $mt = MT::XMLRPCServer::Util::mt_new();   ## Will die if MT->new fails.
1097    my($author) = $class->_login($user, $pass);
1098    die _fault(MT->translate("Invalid login")) unless $author;
1099    $author->remote_auth_username($remote_auth_username);
1100    $author->remote_auth_token($remote_auth_token);
1101    $author->save();
1102    1;
1103}
1104
1105sub newMediaObject {
1106    my $class = shift;
1107    my($blog_id, $user, $pass, $file) = @_;
1108    my $mt = MT::XMLRPCServer::Util::mt_new();   ## Will die if MT->new fails.
1109    my($author, $perms) = $class->_login($user, $pass, $blog_id);
1110    die _fault(MT->translate("Invalid login")) unless $author;
1111    die _fault(MT->translate("Not privileged to upload files"))
1112        unless $perms && $perms->can_upload;
1113    require MT::Blog;
1114    require File::Spec;
1115    my $blog = MT::Blog->load($blog_id)
1116        or die _fault(MT->translate('Can\'t load blog #[_1].', $blog_id));
1117
1118    my $fname = $file->{name} or die _fault(MT->translate("No filename provided"));
1119    if ($fname =~ m!\.\.|\0|\|!) {
1120        die _fault(MT->translate("Invalid filename '[_1]'", $fname));
1121    }
1122    my $local_file = File::Spec->catfile($blog->site_path, $file->{name});
1123    my $fmgr = $blog->file_mgr;
1124    my($vol, $path, $name) = File::Spec->splitpath($local_file);
1125    $path =~ s!/$!! unless $path eq '/';  ## OS X doesn't like / at the end in mkdir().
1126    unless ($fmgr->exists($path)) {
1127        $fmgr->mkpath($path)
1128            or die _fault(MT->translate("Error making path '[_1]': [_2]", $path, $fmgr->errstr));
1129    }
1130    defined(my $bytes = $fmgr->put_data($file->{bits}, $local_file, 'upload'))
1131        or die _fault(MT->translate("Error writing uploaded file: [_1]", $fmgr->errstr));
1132    my $url = $blog->site_url . $fname;
1133
1134    require File::Basename;
1135    my $local_basename = File::Basename::basename($local_file);
1136    my $ext =
1137        ( File::Basename::fileparse( $local_file, qr/[A-Za-z0-9]+$/ ) )[2];
1138    eval { require Image::Size; };
1139    die _fault(MT->translate("Perl module Image::Size is required to determine width and height of uploaded images.")) if $@;
1140    my ( $w, $h, $id ) = Image::Size::imgsize($local_file);
1141
1142    require MT::Asset;
1143    my $asset_pkg = MT::Asset->handler_for_file($local_basename);
1144    my $is_image  = defined($w)
1145      && defined($h)
1146      && $asset_pkg->isa('MT::Asset::Image');
1147    my $asset;
1148    if (!($asset = $asset_pkg->load(
1149                { file_path => $local_file, blog_id => $blog_id })))
1150    {
1151        $asset = $asset_pkg->new();
1152        $asset->file_path($local_file);
1153        $asset->file_name($local_basename);
1154        $asset->file_ext($ext);
1155        $asset->blog_id($blog_id);
1156        $asset->created_by( $author->id );
1157    }
1158    else {
1159        $asset->modified_by( $author->id );
1160    }
1161    my $original = $asset->clone;
1162    $asset->url($url);
1163    if ($is_image) {
1164        $asset->image_width($w);
1165        $asset->image_height($h);
1166    }
1167    $asset->mime_type($file->{type});
1168    $asset->save;
1169
1170    $blog->touch('asset');
1171    $blog->save;
1172
1173    MT->run_callbacks(
1174        'api_upload_file.' . $asset->class,
1175        File => $local_file, file => $local_file,
1176        Url => $url, url => $url,
1177        Size => $bytes, size => $bytes,
1178        Asset => $asset, asset => $asset,
1179        Type => $asset->class, type => $asset->class,
1180        Blog => $blog,blog => $blog);
1181    if ($is_image) {
1182        MT->run_callbacks(
1183            'api_upload_image',
1184            File => $local_file, file => $local_file,
1185            Url => $url, url => $url,
1186            Size => $bytes, size => $bytes,
1187            Asset => $asset, asset => $asset,
1188            Height => $h, height => $h,
1189            Width => $w, width => $w,
1190            Type => 'image', type => 'image',
1191            ImageType => $id, image_type => $id,
1192            Blog => $blog, blog => $blog);
1193    }
1194
1195    { url => SOAP::Data->type(string => $url) };
1196}
1197
1198## getTemplate and setTemplate are not applicable in MT's template
1199## structure, so they are unimplemented (they return a fault).
1200## We assign it twice to get rid of "setTemplate used only once" warnings.
1201
1202sub getTemplate {
1203    die _fault(MT->translate(
1204        "Template methods are not implemented, due to differences between the Blogger API and the Movable Type API."));
1205}
1206*setTemplate = *setTemplate = \&getTemplate;
1207
1208## The above methods will be called as blogger.newPost, blogger.editPost,
1209## etc., because we are implementing Blogger's API. Thus, the empty
1210## subclass.
1211package blogger;
1212BEGIN { @blogger::ISA = qw( MT::XMLRPCServer ); }
1213
1214package metaWeblog;
1215BEGIN { @metaWeblog::ISA = qw( MT::XMLRPCServer ); }
1216
1217package mt;
1218BEGIN { @mt::ISA = qw( MT::XMLRPCServer ); }
1219
1220package wp;
1221BEGIN { @wp::ISA = qw( MT::XMLRPCServer ); }
1222
12231;
1224__END__
1225
1226=head1 NAME
1227
1228MT::XMLRPCServer
1229
1230=head1 SYNOPSIS
1231
1232An XMLRPC API interface for communicating with Movable Type.
1233
1234=head1 CALLBACKS
1235
1236=over 4
1237
1238=item api_pre_save.entry
1239=item api_pre_save.page
1240
1241    callback($eh, $mt, $entry, $original_entry)
1242
1243Called before saving a new or existing entry. If saving a new entry, the
1244$original_entry will have an unassigned 'id'. This callback is executed
1245as a filter, so your handler must return 1 to allow the entry to be saved.
1246
1247=item api_post_save.entry
1248=item api_post_save.page
1249
1250    callback($eh, $mt, $entry, $original_entry)
1251
1252Called after saving a new or existing entry. If saving a new entry, the
1253$original_entry will have an unassigned 'id'.
1254
1255=item api_upload_file
1256
1257    callback($eh, %params)
1258
1259This callback is invoked for each file the user uploads to the weblog.
1260This callback is similar to the CMSUploadFile callback found in
1261C<MT::App::CMS>.
1262
1263=back
1264
1265=head2 Parameters
1266
1267=over 4
1268
1269=item File
1270
1271The full physical file path of the uploaded file.
1272
1273=item Url
1274
1275The full URL to the file that has been uploaded.
1276
1277=item Type
1278
1279For this callback, this value is currently always 'file'.
1280
1281=item Blog
1282
1283The C<MT::Blog> object associated with the newly uploaded file.
1284
1285=back
1286
1287=cut
Note: See TracBrowser for help on using the browser.