root/branches/release-41/lib/MT/BackupRestore.pm @ 2730

Revision 2730, 43.5 kB (checked in by fumiakiy, 17 months ago)

Unit test update - more strict checking of columns of MT::Trackback, MT::Folder and MT::Category.

Changed the order of the backup so the triggers won't affect the restored objects.

  • Property svn:keywords set to Id Author Date 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::BackupRestore;
8use strict;
9
10use MT::Util qw( encode_url );
11use Symbol;
12use base qw( MT::ErrorHandler );
13
14sub NS_MOVABLETYPE { 'http://www.sixapart.com/ns/movabletype' };
15
16use File::Spec;
17use File::Copy;
18
19MT->add_callback('restore', 3, MT->instance, sub {
20    my ($cb, $objects, $deferred, $errors, $callback) = @_;
21    MT::BackupRestore->cb_restore_objects( $objects, $callback );
22});
23
24MT->add_callback('restore_asset', 3, MT->instance, sub {
25    my ($cb, $asset, $callback) = @_;
26    MT::BackupRestore->cb_restore_asset( $asset, $callback );
27});
28
29sub core_backup_instructions {
30    # The list of classes that require specific orders
31    # and/or special instructions.
32    # Every other class will have the order of '500'.
33    return {
34        'blog'          => {
35            'order' => 400
36        },
37        'author'        => {
38            'order' => 420
39        },
40        # These 'association' classes should be backed up
41        # after the object classes.
42        'association'   => {
43            'order' => 510
44        },
45        'placement'     => {
46            'order' => 510
47        },
48        'trackback'     => {
49            'order' => 510
50        },
51        'objecttag'     => {
52            'order' => 510
53        },
54        'objectscore'   => {
55            'order' => 510
56        },
57        'objectasset'   => {
58            'order' => 510
59        },
60        # Ping should be backed up after Trackback.
61        'tbping'        => {
62            'order' => 520
63        },
64        'ping'          =>  {
65            'order' => 520
66        },
67        'ping_cat'      => {
68            'order' => 520
69        },
70        # Comment should be backed up after TBPing
71        # because saving a comment ultimately triggers
72        # MT::TBPing::save.
73        'comment'        => {
74            'order' => 530
75        },
76        # Session, config and TheSchwartz packages are never backed up.
77        'session'       => {
78            'skip' => 1
79        },
80        'config'        => {
81            'skip' => 1
82        },
83        'ts_job'        => {
84            'skip' => 1
85        },
86        'ts_error'      => {
87            'skip' => 1
88        },
89        'ts_exitstatus' => {
90            'skip' => 1
91        },
92        'ts_funcmap'    => {
93            'skip' => 1
94        },
95    };
96}
97
98sub _populate_obj_to_backup {
99    my $pkg = shift;
100    my ($blog_ids) = @_;
101
102    my %populated;
103    if ( defined($blog_ids) && scalar(@$blog_ids) ) {
104        # author will be handled at last
105        $populated{MT->model('author')} = 1;
106    }
107
108    my @object_hashes;
109    my $types = MT->registry('object_types');
110    my $instructions = MT->registry('backup_instructions');
111    foreach my $key (keys %$types) {
112        next if $key =~ /\w+\.\w+/; # skip subclasses
113        my $class = MT->model($key);
114        next unless $class;
115        next if $class eq $key; # FIXME: to remove plugin object_classes
116        next if exists($instructions->{$key})
117             && exists($instructions->{$key}{skip})
118             && $instructions->{$key}{skip};
119        next if exists $populated{$class};
120        my $order = exists($instructions->{$key})
121                 && exists($instructions->{$key}{order})
122            ? $instructions->{$key}{order}
123            : 500;
124        $pkg->_create_obj_to_backup(
125            $class, $blog_ids, \@object_hashes, \%populated, $order);
126    }
127
128    if ( defined($blog_ids) && scalar(@$blog_ids) ) {
129        # Author has two ways to be associated to a blog
130        my $class = MT->model('author');
131        unshift @object_hashes, {
132            $class => { 
133                terms => undef, 
134                args => { 'join' => 
135                    [ MT->model('association'), 'author_id', { blog_id => $blog_ids }, { unique => 1 } ] 
136                }
137            },
138            'order' => 500
139        }; 
140        unshift @object_hashes, {
141            $class => { 
142                terms => undef, 
143                args => { 'join' => 
144                    [ MT->model('permission'), 'author_id', { blog_id => $blog_ids }, { unique => 1 } ] 
145                }
146            },
147            'order' => 500
148        };
149    }
150    @object_hashes = sort { $a->{order} <=> $b->{order} } @object_hashes;
151    my @obj_to_backup;
152    foreach my $hash ( @object_hashes ) {
153        delete $hash->{order};
154        push @obj_to_backup, $hash;
155    }
156    return \@obj_to_backup;
157}
158
159sub _create_obj_to_backup {
160    my $pkg = shift;
161    my ($class, $blog_ids, $obj_to_backup, $populated, $order) = @_;
162
163    my $instructions = MT->registry('backup_instructions');
164    my $columns = $class->column_names;
165    foreach my $column (@$columns) {
166        if ( $column =~ /^(\w+)_id$/ ) {
167            my $parent = $1;
168            my $p_class = MT->model($parent);
169            next unless $p_class;
170            next if exists $populated->{$p_class};
171            next if exists($instructions->{$parent})
172                 && exists($instructions->{$parent}{skip})
173                 && $instructions->{$parent}{skip};
174            my $p_order = exists($instructions->{$parent})
175                       && exists($instructions->{$parent}{order})
176                ? $instructions->{$parent}{order}
177                : 500;
178            $pkg->_create_obj_to_backup(
179                $p_class, $blog_ids, $obj_to_backup, $populated, $p_order);
180        }
181    }
182   
183    if ( $class->can('backup_terms_args') ) {
184        push @$obj_to_backup, {
185            $class  => $class->backup_terms_args($blog_ids),
186            'order' => $order
187        };
188    }
189    else {
190        push @$obj_to_backup, $pkg->_default_terms_args($class, $blog_ids, $order);
191    }
192
193    $populated->{$class} = 1;
194}
195
196sub _default_terms_args {
197    my $pkg = shift;
198    my ($class, $blog_ids, $order) = @_;
199
200    if (defined($blog_ids) && scalar(@$blog_ids)) {
201        return {
202            $class => {
203                terms => { 'blog_id' => $blog_ids }, 
204                args => undef
205            },
206            'order' => $order,
207        };
208    }
209    else {
210        return {
211            $class  => { terms => undef, args => undef },
212            'order' => $order,
213        };
214    }
215}
216   
217
218sub backup {
219    my $class = shift;
220    my ($blog_ids, $printer, $splitter, $finisher, $progress, $size, $enc, $metadata) = @_;
221    push @$blog_ids, '0'
222        if defined($blog_ids) && scalar(@$blog_ids);
223    my $obj_to_backup = $class->_populate_obj_to_backup( $blog_ids );
224
225    my $header .= "<movabletype xmlns='" . NS_MOVABLETYPE . "'\n";
226    $header .= join ' ', map { $_ . "='" . $metadata->{$_} . "'" } keys %$metadata;
227    $header .= ">\n";
228    $header = "<?xml version='1.0' encoding='$enc'?>\n$header" if $enc !~ m/utf-?8/i;
229    $printer->($header);
230
231    my $files = {};
232    _loop_through_objects(
233        $printer, $splitter, $finisher, $progress, $size, $obj_to_backup, $files);
234
235    my $else_xml = MT->run_callbacks('Backup', $blog_ids, $progress);
236    $printer->($else_xml) if $else_xml ne '1';
237
238    $printer->('</movabletype>');
239    $finisher->($files);
240}
241
242sub _loop_through_objects {
243    my ($printer, $splitter, $finisher, $progress, $size, $obj_to_backup, $files) = @_;
244
245    my $counter = 1;
246    my $bytes = 0;
247    my %authors_seen;
248    my $author_pkg = MT->model('author');
249    for my $class_hash (@$obj_to_backup) {
250        my ($class, $term_arg) = each(%$class_hash);
251        eval "require $class;";
252        my $children = $class->properties->{child_classes} || {};
253        for my $child_class (keys %$children) {
254            eval "require $child_class;";
255        }
256        if (my $err = $@) {
257            $progress->("$err\n", 'Error');
258            next;
259        }
260        my @metacolumns;
261        if ( exists( $class->properties->{meta} )
262          && $class->properties->{meta} ) {
263            require MT::Meta;
264            @metacolumns = MT::Meta->metadata_by_class( $class );
265        }
266        my $records = 0;
267        my $state = MT->translate('Backing up [_1] records:', $class);
268        $progress->($state, $class->class_type || $class->datasource);
269        my $limit = 50;
270        my $offset = 0;
271        my $terms = $term_arg->{terms} || {};
272        my $args = $term_arg->{args};
273        unless ( exists $args->{sort} ) {
274            $args->{sort} = 'id';
275            $args->{direction} = 'ascend';
276        }
277        while (1) {
278            $args->{offset} = $offset;
279            $args->{limit} = $limit + 1;
280            my $iter;
281            eval {
282                $iter = $class->load_iter($terms, $args);
283            };
284            if (my $err = $@) {
285                $progress->("$class:$err\n", 'Error');
286            }
287            last unless $iter;
288            my $count = 0;
289            my $next  = 0;
290            while ( my $object = $iter->() ) {
291                if ( $count == $limit ) {
292                    $iter->end;
293                    $next = 1;
294                    $offset += $count;
295                    last;
296                }
297                $count++;
298                if ( ( $class eq $author_pkg )
299                  && ( exists $authors_seen{$object->id} ) ) {
300                    next;
301                }
302                $bytes += $printer->($object->to_xml(undef, \@metacolumns) . "\n");
303                $records++;
304                if ($size && ($bytes >= $size)) {
305                    $splitter->(++$counter);
306                    $bytes = 0;
307                }
308                if ( $class eq $author_pkg ) {
309                    # Authors may be duplicated because of how terms and args are created.
310                    $authors_seen{$object->id} = 1;
311                } elsif ( $class->datasource eq 'asset' ) {
312                    $files->{$object->id} = [$object->url, $object->file_path, $object->file_name];
313                }
314            }
315            last unless $next;
316            $progress->($state . " " . MT->translate("[_1] records backed up...", $records), $class->datasource)
317                if $records && ($records % 100 == 0);
318        }
319        if ( $class eq $author_pkg && %authors_seen ) {
320            my $num_authors = scalar(keys %authors_seen);
321            $progress->($state . " " . MT->translate("[_1] records backed up.", $num_authors), $class->class_type || $class->datasource);
322        } elsif ($records) {
323            $progress->($state . " " . MT->translate("[_1] records backed up.", $records), $class->class_type || $class->datasource);
324        } else {
325            $progress->($state . " " . MT->translate("There were no [_1] records to be backed up.", $class), $class->class_type || $class->datasource);
326        }
327    }
328}
329
330sub restore_file {
331    my $class = shift;
332    my ($fh, $errormsg, $schema_version, $overwrite, $callback) = @_;
333
334    my $objects = {};
335    my $deferred = {};
336    my $errors = [];
337
338    my ($blog_ids, $asset_ids) = eval { $class->restore_process_single_file(
339        $fh, $objects, $deferred, $errors, $schema_version, $overwrite, $callback
340    ); };
341
342    unless ( $@ ) {
343        MT->run_callbacks('restore', $objects, $deferred, $errors, $callback);
344    }
345    $$errormsg = join('; ', @$errors);
346    ($deferred, $blog_ids);
347}
348 
349sub restore_process_single_file {
350    my $class = shift;
351    my ($fh, $objects, $deferred, $errors, $schema_version, $overwrite,  $callback) = @_;
352   
353    my %restored_blogs = map { $objects->{$_}->id => 1; } grep { 'blog' eq $objects->{$_}->datasource } keys %$objects;
354
355    require XML::SAX;
356    require MT::BackupRestore::BackupFileHandler;
357    my $handler = MT::BackupRestore::BackupFileHandler->new(
358        callback => $callback,
359        objects => $objects,
360        deferred => $deferred,
361        errors => $errors,
362        schema_version => $schema_version,
363        overwrite_template => $overwrite,
364    );
365
366    require MT::Util;
367    my $parser = MT::Util::sax_parser();
368    $callback->(ref($parser) . "\n") if MT->config->DebugMode;
369    $parser->{Handler} = $handler;
370    eval { $parser->parse_file($fh); };
371    if (my $e = $@) {
372        push @$errors, $e;
373        $callback->($e);
374        die $e if $handler->{critical}; 
375    }
376
377    my @blog_ids;
378    my @asset_ids;
379
380    while (my ($key, $value) = each %$objects) {
381        if ('blog' eq $value->datasource) {
382            push @blog_ids, $value->id unless exists $restored_blogs{$value->id};
383        } elsif ('asset' eq $value->datasource) {
384            my ($old_id) = $key =~ /^.+#(\d+)$/;
385            push @asset_ids, $value->id, $old_id;
386        }
387    }
388    my $blog_ids = scalar(@blog_ids) ? \@blog_ids : undef;
389    my $asset_ids = scalar(@asset_ids) ? \@asset_ids : undef;
390    ($blog_ids, $asset_ids);
391}
392
393sub restore_directory {
394    my $class = shift;
395    my ($dir, $errors, $error_assets, $schema_version, $overwrite, $callback) = @_;
396
397    my $manifest;
398    my @files;
399    opendir my $dh, $dir or push(@$errors, MT->translate("Can't open directory '[_1]': [_2]", $dir, "$!")), return undef;
400    for my $f (readdir $dh) {
401        next if $f !~ /^.+\.manifest$/i;
402        $manifest = File::Spec->catfile($dir, $f);
403        last;
404    }
405    closedir $dh;
406    unless ($manifest) {
407        push @$errors, MT->translate("No manifest file could be found in your import directory [_1].", $dir);
408        return (undef, undef);
409    }
410
411    my $fh = gensym;
412    open $fh, "<$manifest" or push(@$errors, MT->translate("Can't open [_1].", $manifest)), return 0;
413    my $backups = __PACKAGE__->process_manifest($fh);
414    close $fh;
415    unless($backups) {
416        push @$errors, MT->translate("Manifest file [_1] was not a valid Movable Type backup manifest file.", $manifest);
417        return (undef, undef);
418    }
419
420    $callback->(MT->translate("Manifest file: [_1]", $manifest) . "\n");
421
422    my %objects;
423    my $deferred = {};
424
425    my $files = $backups->{files};
426    my @blog_ids;
427    my @asset_ids;
428    for my $file (@$files) {
429        my $fh = gensym;
430        my $filepath = File::Spec->catfile($dir, $file);
431        open $fh, "<$filepath" or push @$errors, MT->translate("Can't open [_1]."), next;
432
433        my ($tmp_blog_ids, $tmp_asset_ids) = eval { __PACKAGE__->restore_process_single_file(
434            $fh, \%objects, $deferred, $errors, $schema_version, $overwrite, $callback); };
435
436        close $fh;
437        last if $@;
438
439        push @blog_ids, @$tmp_blog_ids if defined $tmp_blog_ids;
440        push @asset_ids, @$tmp_asset_ids if defined $tmp_asset_ids;
441    }
442
443    unless ( $@ ) {
444        MT->run_callbacks('restore', \%objects, $deferred, $errors, $callback);
445    }
446    my $blog_ids = scalar(@blog_ids) ? \@blog_ids : undef;
447    my $asset_ids = scalar(@asset_ids) ? \@asset_ids : undef;
448    ($deferred, $blog_ids, $asset_ids);
449}
450
451sub process_manifest {
452    my $class = shift;
453    my ($stream) = @_;
454
455    if ((ref($stream) eq 'Fh') || (ref($stream) eq 'GLOB')){
456        seek($stream, 0, 0) or return undef;
457        require XML::SAX;
458        require MT::BackupRestore::ManifestFileHandler;
459        my $handler = MT::BackupRestore::ManifestFileHandler->new();
460
461        require MT::Util;
462        my $parser = MT::Util::sax_parser();
463        $parser->{Handler} = $handler;
464        eval { $parser->parse_file($stream); };
465        if (my $e = $@) {
466            die $e;
467        }
468        return $handler->{backups};
469    }
470    return undef;
471}
472
473sub restore_asset {
474    my $class = shift;
475    my ($file, $asset, $old_id, $fmgr, $errors, $callback) = @_;
476
477    my $id = $asset->id;
478
479    my $path = $asset->file_path;
480    unless (defined($path)) {
481        $callback->(MT->translate('Path was not found for the file ([_1]).', $id));
482        return 0;
483    }
484    my ($vol, $dir, $fn) = File::Spec->splitpath($path);
485    my $voldir =  "$vol$dir";
486    if (!-w $voldir) {
487        unless (defined $fmgr) {
488            my $blog = MT->model('blog')->load($asset->blog_id);
489            $fmgr = $blog->file_mgr if $blog;
490        }
491        unless (defined $fmgr) {
492            # we do need utf8_off here
493            $errors->{$id} = MT->translate('[_1] is not writable.', MT::I18N::utf8_off($voldir)) ;
494        } else {
495            $voldir =~ s|/$|| unless $voldir eq '/';  ## OS X doesn't like / at the end in mkdir().
496            unless ($fmgr->exists($voldir)) {
497                $fmgr->mkpath($voldir) or
498                    $errors->{$id} = MT->translate("Error making path '[_1]': [_2]", $path, $fmgr->errstr);
499            }
500        }
501    }
502    if (-w $voldir) {
503        my $filename = "$old_id-" . $asset->file_name;
504        $callback->(MT->translate("Copying [_1] to [_2]...", $filename, $path));
505        copy($file, $path)
506            or $errors->{$id} = $!;
507    }
508
509    if ( exists $errors->{$id} ) {
510        return $callback->( MT->translate('Failed: ') . $errors->{$id} . "\n" );
511    }
512
513    $callback->( MT->translate("Done.") . "\n" );
514
515    MT->run_callbacks('restore_asset', $asset, $callback);
516   
517    1;
518}
519
520sub _sync_asset_id {
521    my ($text, $related) = @_;
522
523    my $new_text = $text;
524
525    $new_text =~ s!<form([^>]*?\s)mt:asset-id=(["'])(\d+)(["'])([^>]*?)>(.+?)</form>!
526        my $old_id = $3;
527        my $result = '';
528        if ( my $asset = $related->{$old_id} ) {
529            $result = '<form' . $1 . 'mt:asset-id=' . $2 . $asset->id . $4 . $5 . '>';
530            my $html = $6;
531            my $filename = quotemeta(encode_url($asset->file_name));
532            my $url = $asset->url;
533            my @children = MT->model('asset')->load(
534                { parent => $asset->id, blog_id => $asset->blog_id, class => '*' }
535            );
536            my %children = map {
537                $_->id => {
538                    'filename' => quotemeta(encode_url($_->file_name)),
539                    'url' => $_->url
540                }
541            } @children;
542            $result .= $html . '</form>';
543        }
544        $result;
545    !igem;
546    return $new_text ? $new_text : $text;
547}
548
549sub cb_restore_objects {
550    my $pkg = shift;
551    my ($all_objects, $callback) = @_;
552
553    my %entries;
554    my %assets;
555    my %old_ids;
556    for my $key ( keys %$all_objects ) {
557        if ( $key =~ /^MT::Entry#(\d+)$/ ) {
558            my $new_id = $all_objects->{$key}->id;
559            $entries{$new_id} = $all_objects->{$key};
560        } elsif ( $key =~ /^MT::Asset#(\d+)$/ ) {
561            my $old_id = $1;
562            my $new_id = $all_objects->{$key}->id;
563            $assets{$new_id} = {
564                object => $all_objects->{$key},
565                old_id => $old_id,
566            };
567        } elsif ( $key =~ /^MT::Author#(\d+)$/ ) {
568            # restore userpic association now
569            my $new_author = $all_objects->{$key};
570            if ( my $userpic_id = $new_author->userpic_asset_id ) {
571                if ( my $new_asset = $all_objects->{'MT::Asset#' . $userpic_id} ) {
572                    $new_author->userpic_asset_id( $new_asset->id );
573                }
574            }
575            # also restore ids of favorite blogs
576            if ( my $favorites = $new_author->favorite_blogs ) {
577                my @new_favs;
578                if ( @$favorites ) {
579                    foreach my $old_id ( @$favorites ) {
580                        my $blog = $all_objects->{'MT::Blog#' . $old_id};
581                        push @new_favs, $blog->id if $blog;
582                    }
583                }
584                $new_author->favorite_blogs(\@new_favs) if @new_favs;
585            }
586            $new_author->update;
587            # call trigger to save meta
588            $new_author->call_trigger('post_save', $new_author);
589        }
590    }
591
592    my $i = 0;
593    $callback->(
594        MT->translate("Restoring asset associations ... ( [_1] )", $i++),
595        'cb-restore-entry-asset'
596    );
597    for my $obj_id ( keys %entries ) {
598        my $entry = $entries{$obj_id};
599
600        my @placements = MT->model('objectasset')->load( {
601            object_id => $obj_id, 
602            object_ds => 'entry', 
603            blog_id => $entry->blog_id
604        });
605        next unless @placements;
606
607        my %related;
608        for my $placement ( @placements ) {
609            my $asset_hash = $assets{$placement->asset_id};
610            next unless $asset_hash;
611            $related{ $asset_hash->{old_id} } = $asset_hash->{object};
612        }
613
614        if ($entry->class == 'entry') {
615            $callback->(
616                MT->translate("Restoring asset associations in entry ... ( [_1] )", $i++),
617                'cb-restore-entry-asset'
618            );
619        } else {
620            $callback->(
621                MT->translate("Restoring asset associations in page ... ( [_1] )", $i++),
622                'cb-restore-entry-asset'
623            );
624        }
625       
626        for my $col ( qw( text text_more ) ) {
627            my $text = $entry->$col;
628            next unless $text;
629            $text = _sync_asset_id( $text, \%related ); 
630            $entry->$col($text);
631        }
632        $entry->update();  # directly call update to bypass processing in save()
633    }
634    $callback->( MT->translate("Done.") . "\n" );
635    1;
636}
637
638sub _sync_asset_url {
639    my ($text, $asset) = @_;
640
641    my $filename = quotemeta(encode_url($asset->file_name));
642    my $url = $asset->url;
643    my $id = $asset->id;
644    my @children = MT->model('asset')->load(
645        { parent => $asset->id, blog_id => $asset->blog_id, class => '*' }
646    );
647    my %children = map {
648        $_->id => {
649            'filename' => quotemeta(encode_url($_->file_name)),
650            'url' => $_->url
651        }
652    } @children;
653
654    $text =~ s!<form([^>]*?\s)mt:asset-id=(["'])$id(["'])([^>]*?)>(.+?)</form>!
655        my $result = '<form' . $1 . 'mt:asset-id=' . $2 . $id . $3 . $4 . '>';
656        my $html = $5;
657        $html =~ s#<a([^>]*? )href=(["'])[^>]+?/$filename(["'])([^>]*?)>#<a$1href=$2$url$3$4>#gim;
658        $html =~ s#<img([^>]*? )src=(["'])[^>]+?/$filename(["'])([^>]*?)(/? *)>#<img$1src=$2$url$3$4$5>#gim;
659        if ( %children ) {
660            for my $child (values %children) {
661                my $child_filename = $child->{filename};
662                my $child_url = $child->{url};
663                $html =~ s#<img([^>]*? )src=(["'])[^>]+?/$child_filename(["'])([^>]*?)>#<img$1src=$2$child_url$3$4>#gim;
664                $html =~ s#<a([^>]*? )href=(["'])[^>]+?/$child_filename(["'])([^>]*?)>#<a$1href=$2$child_url$3$4>#gim;
665                $html =~ s#<a([^>]*? )onclick=(["'])[^>]+?/$child_filename(["'])([^>]*?)>#<a$1onclick=$2$child_url$3$4>#gim;
666            }
667        }
668        $result .= $html . '</form>';
669        $result;
670    !igem;
671    $text;
672}
673
674sub cb_restore_asset {
675    my $pkg = shift;
676    my ($asset, $callback) = @_;
677   
678    my @placements = MT->model('objectasset')->load( {
679        asset_id => $asset->id, 
680        blog_id => $asset->blog_id
681    });
682
683    my $i = 0;
684    $callback->(
685        MT->translate('Restoring url of the assets ( [_1] )...', $i++),
686        'cb-restore-asset-url'
687    );
688    for my $placement (@placements) { 
689        next unless 'entry' eq $placement->object_ds; 
690        my $entry = MT->model('entry')->load( $placement->object_id ); 
691        next unless $entry; 
692       
693        if ($entry->class == 'entry') {
694            $callback->(
695                MT->translate('Restoring url of the assets in entry ( [_1] )...', $i++),
696                'cb-restore-asset-url'
697            );
698        } else {
699            $callback->(
700                MT->translate('Restoring url of the assets in page ( [_1] )...', $i++),
701                'cb-restore-asset-url'
702            );
703        }
704        for my $col ( qw( text text_more ) ) {
705            my $text = $entry->$col;
706            next unless $text;
707            $text = _sync_asset_url( $text, $asset );
708            $entry->$col($text);
709        }
710        $entry->update();  # directly call update to bypass processing in save()
711    }
712    $callback->( MT->translate("Done.") . "\n" );
713    1;
714}
715
716sub _restore_asset_multi {
717    my $class = shift;
718    my ($asset_element, $objects, $errors, $callback, $blogs_meta) = @_;
719
720    my $old_id = $asset_element->{asset_id};
721    if (!defined($old_id)) {
722        $callback->(MT->translate('ID for the file was not set.'));
723        return 0;
724    }
725    my $asset_class = MT->model('asset');
726    my $asset = $objects->{"$asset_class#$old_id"};
727    unless (defined($asset)) {
728        $callback->(MT->translate('The file ([_1]) was not restored.', $old_id));
729        return 0;
730    }
731
732    my $fmgr;
733    if (exists $blogs_meta->{$asset->blog_id}) {
734        my $blog = MT->model('blog')->load($asset->blog_id)
735            or return 0;
736
737        my $meta = $blogs_meta->{$asset->blog_id};
738        my $path = $asset->file_path;
739        my $url = $asset->url;
740        if (my $archive_path = $meta->{'archive_path'}) {
741            my $old_archive_path = $meta->{'old_archive_path'};
742            $path =~ s/\Q$old_archive_path\E/$archive_path/i;
743            $asset->file_path($path);
744        }
745        if (my $archive_url = $meta->{'archive_url'}) {
746            my $old_archive_url = $meta->{'old_archive_url'};
747            $url =~ s/\Q$old_archive_url\E/$archive_url/i;
748            $asset->url($url);
749        }
750        if (my $site_path = $meta->{'site_path'}) {
751            my $old_site_path = $meta->{'old_site_path'};
752            $path =~ s/\Q$old_site_path\E/$site_path/i;
753            $asset->file_path($path);
754        }
755        if (my $site_url = $meta->{'site_url'}) {
756            my $old_site_url = $meta->{'old_site_url'};
757            $url =~ s/\Q$old_site_url\E/$site_url/i;
758            $asset->url($url);
759        }
760        $callback->(MT->translate("Changing path for the file '[_1]' (ID:[_2])...", $asset->label, $asset->id));
761        $asset->save or $callback->(MT->translate("failed") . "\n");
762        $callback->(MT->translate("ok") . "\n");
763
764        $fmgr = $blog->file_mgr;
765    }
766    my $file = $asset_element->{fh};
767    $class->restore_asset($file, $asset, $old_id, $fmgr, $errors, $callback);
768}
769
770package MT::Object;
771
772sub _is_element {
773    my $obj = shift;
774    my ($def) = @_;
775    return (('text' eq $def->{type}) || (('string' eq $def->{type}) && (255 < $def->{size}))) ? 1 : 0;
776}
777
778sub to_xml {
779    my $obj = shift;
780    my ($namespace, $metacolumns) = @_;
781
782    my $coldefs = $obj->column_defs;
783    my $colnames = $obj->column_names;
784    my $xml;
785
786    my $elem = $obj->datasource;
787    unless ( UNIVERSAL::isa( $obj, 'MT::Log' ) ) {
788        if ( $obj->properties
789          && ( my $ccol = $obj->properties->{class_column} ) ) {
790            my $class = $obj->$ccol;
791            $elem = $class if $class;
792        }
793    }
794
795    $xml = '<' . $elem;
796    $xml .= " xmlns='$namespace'" if defined($namespace) && $namespace;
797
798    my (@elements, @blobs, @meta);
799    for my $name (@$colnames) {
800        if ($obj->column($name) || (defined($obj->column($name)) && ('0' eq $obj->column($name)))) {
801            if ( ( $obj->properties->{meta_column} || '' ) eq $name ) {
802                push @meta, $name;
803                next;
804            }
805            elsif ($obj->_is_element($coldefs->{$name})) {
806                push @elements, $name;
807                next;
808            } elsif ('blob' eq $coldefs->{$name}->{type}) {
809                push @blobs, $name;
810                next;
811            }
812            $xml .= " $name='" . MT::Util::encode_xml($obj->column($name), 1) . "'";
813        }
814    }
815    my ( @meta_elements, @meta_blobs );
816    if ( defined( $metacolumns ) && @$metacolumns ) {
817        foreach my $metacolumn ( @$metacolumns ) {
818            my $name = $metacolumn->{name};
819            if ($obj->$name || (defined($obj->$name) && ('0' eq $obj->$name))) {
820                if ( 'vclob' eq $metacolumn->{type} ) {
821                    push @meta_elements, $name;
822                }
823                elsif ( 'vblob' eq $metacolumn->{type} ) {
824                    push @meta_blobs, $name;
825                }
826                else {
827                    $xml .= " $name='" . MT::Util::encode_xml($obj->$name, 1) . "'";
828                }
829            }
830        }
831    }
832    $xml .= '>';
833    $xml .= "<$_>" . MT::Util::encode_xml($obj->column($_), 1) . "</$_>" foreach @elements;
834    require MIME::Base64;
835    $xml .= "<$_>" . MIME::Base64::encode_base64($obj->column($_), '') . "</$_>" foreach @blobs;
836    foreach my $meta_col (@meta) {
837        my $hashref = $obj->$meta_col;
838        $xml .= "<$meta_col>" . 
839                MIME::Base64::encode_base64(MT::Serialize->serialize(\$hashref), '') .
840                "</$meta_col>";
841    }
842    $xml .= "<$_>" . MT::Util::encode_xml($obj->$_, 1) . "</$_>" foreach @meta_elements;
843    foreach my $vblob_col (@meta_blobs) {
844        my $vblob = $obj->$vblob_col;
845        $xml .= "<$vblob_col>" . 
846                MIME::Base64::encode_base64(MT::Serialize->serialize(\$vblob), '') .
847                "</$vblob_col>";
848    }
849    $xml .= '</' . $elem . '>';
850    $xml;
851}
852
853sub parents {
854    my $obj = shift;
855    {};
856}
857
858sub _restore_id {
859    my $obj = shift;
860    my ($key, $val, $data, $objects) = @_;
861
862    return 0 unless 'ARRAY' eq ref($val);
863    return 1 if 0 == $data->{$key}; 
864
865    my $new_obj;
866    my $old_id = $data->{$key};
867    foreach (@$val) {
868        $new_obj = $objects->{"$_#$old_id"};
869        last if $new_obj;
870    }
871    return 0 unless $new_obj;
872    $data->{$key} = $new_obj->id;
873    return 1;
874}
875
876sub restore_parent_ids {
877    my $obj = shift;
878    my ($data, $objects) = @_;
879
880    my $parents = $obj->parents;
881    my $count = scalar(keys %$parents);
882
883    my $done = 0;
884    while (my ($key, $val) = each(%$parents)) {
885        $val = [ $val ] unless (ref $val);
886        if ('ARRAY' eq ref($val)) {
887            $done += $obj->_restore_id($key, $val, $data, $objects);
888        }
889        elsif ('HASH' eq ref($val)) {
890            my $v = $val->{class};
891            $v = [ $v ] unless (ref $v);
892            my $result = 0;
893            if (my $relations = $val->{relations}) {
894                my $col = $relations->{key};
895                my $ds = $data->{$col};
896                my $ev = $relations->{$ds . '_id'};
897                $ev = MT->model($ds) unless $ev;
898                return 0 unless $ev;
899                $ev = [ $ev ] unless (ref $ev);
900                $done += $obj->_restore_id($key, $ev, $data, $objects);
901            }
902            else {
903                $result = $obj->_restore_id($key, $v, $data, $objects);
904                $result = 1 if exists($val->{optional}) && $val->{optional};
905                $data->{$key} = -1 
906                  if !$result && (exists($val->{orphanize}) && $val->{orphanize});
907                $done += $result;
908            }
909        }
910    }
911    ($count == $done) ? 1 : 0;   
912}
913
914package MT::Blog;
915
916sub backup_terms_args {
917    my $class = shift;
918    my ($blog_ids) = @_;
919
920    if ( defined($blog_ids) && scalar(@$blog_ids) ) {
921        return
922          {
923            terms => { id => $blog_ids }, 
924            args => undef,
925          };
926    }
927    else {
928        return { terms => undef, args => undef };
929    }
930}
931
932package MT::Tag;
933
934sub backup_terms_args {
935    my $class = shift;
936    my ($blog_ids) = @_;
937
938    if ( defined($blog_ids) && scalar(@$blog_ids) ) {
939        return
940          {
941            terms => undef, 
942            args =>
943              {
944                'join' =>
945                  [
946                    'MT::ObjectTag', 
947                    'tag_id',
948                    { blog_id => $blog_ids }, 
949                    { unique => 1 }
950                  ]
951              }
952          };
953    }
954    else {
955        return { terms => undef, args => undef };
956    }
957}
958
959package MT::Role;
960
961sub backup_terms_args {
962    my $class = shift;
963    my ($blog_ids) = @_;
964
965    if ( defined($blog_ids) && scalar(@$blog_ids) ) {
966        return
967          {
968            terms => undef, 
969            args =>
970              {
971                'join' =>
972                  [
973                    'MT::Association',
974                    'role_id',
975                    { blog_id => $blog_ids },
976                    { unique => 1 }
977                  ]
978              }
979          };
980    }
981    else {
982        return { terms => undef, args => undef };
983    }
984}
985
986package MT::Asset;
987
988sub backup_terms_args {
989    my $class = shift;
990    my ($blog_ids) = @_;
991
992    if ( defined($blog_ids) && scalar(@$blog_ids) ) {
993        return
994          {
995            terms => { 'blog_id' => $blog_ids, 'class' => '*' }, 
996            args => undef
997          }
998    }
999    else {
1000        return { terms => { 'class' => '*' }, args => undef };
1001    }
1002}
1003
1004my $assets_seen = {};
1005
1006sub to_xml {
1007    my $obj = shift;
1008    my $xml = q();
1009
1010    return $xml if exists $assets_seen->{$obj->id};
1011
1012    if ($obj->parent) {
1013        my $parent = MT->model('asset')->load($obj->parent)
1014            or return $xml;
1015        $xml .= $parent->to_xml(@_);
1016        $xml .= "\n";
1017    }
1018
1019    $xml .= $obj->SUPER::to_xml(@_);
1020    $assets_seen->{$obj->id} = 1;
1021    $xml;
1022}
1023
1024sub parents {
1025    my $obj = shift;
1026    {
1027        blog_id => MT->model('blog'),
1028        parent  => MT->model('asset')
1029    };
1030}
1031
1032package MT::PluginData;
1033
1034sub backup_terms_args {
1035    my $class = shift;
1036    my ($blog_ids) = @_;
1037
1038    return { terms => undef, args => undef };
1039}
1040
1041sub restore_parent_ids {
1042    my $obj = shift;
1043    my ($data, $objects) = @_;
1044
1045    if ($data->{key} =~ /^configuration:blog:(\d+)$/i) {
1046        my $new_blog = $objects->{'MT::Blog#' . $1};
1047        if ($new_blog) {
1048            $data->{key} = 'configuration:blog:' . $new_blog->id;
1049        }
1050    }
1051    return 1;
1052}
1053
1054package MT::Association;
1055
1056sub restore_parent_ids {
1057    my $obj = shift;
1058    my ($data, $objects) = @_;
1059
1060    my ($u, $b, $g, $r) = (0, 0, 0, 0);
1061
1062    my $processor = sub {
1063        my ($elem) = @_;
1064        my $class = MT->model($elem);
1065        my $old_id = $data->{$elem . '_id'};
1066        my $new_obj = $objects->{"$class#$old_id"};
1067        return 0 unless defined($new_obj) && $new_obj;
1068        $data->{$elem . '_id'} = $new_obj->id;
1069        return 1;
1070    };
1071
1072    $u = $processor->('author');
1073    $g = $processor->('group');
1074    $b = $processor->('blog');
1075    $r = $processor->('role');
1076
1077    # Combination allowed are:
1078    # USER_BLOG_ROLE  => 1;
1079    # GROUP_BLOG_ROLE => 2;
1080    # USER_GROUP      => 3;
1081    # USER_ROLE       => 4;
1082    # GROUP_ROLE      => 5;
1083
1084    ($u && $g) || ($u && $r) || ($g && $r) ? 1 : 0; # || ($u && $b && $r) || ($g && $b && $r)
1085}
1086
1087package MT::Category;
1088
1089my $category_seen = {};
1090
1091sub to_xml {
1092    my $obj = shift;
1093    my $xml = q();
1094
1095    return $xml if exists $category_seen->{$obj->id};
1096   
1097    if ('0' ne $obj->parent) {
1098        $xml .= $obj->parent_category->to_xml(@_);
1099        $xml .= "\n";
1100    }
1101
1102    $xml .= $obj->SUPER::to_xml(@_);
1103    $category_seen->{$obj->id} = 1;
1104    $xml;
1105}
1106
1107sub parents {
1108    my $obj = shift;
1109    {
1110        blog_id => MT->model('blog'),
1111        parent  => [ MT->model('category'), MT->model('folder') ],
1112    };
1113}
1114
1115package MT::Comment;
1116
1117sub parents {
1118    my $obj = shift;
1119    {
1120        entry_id => [ MT->model('entry'), MT->model('page') ],
1121        blog_id => MT->model('blog'),
1122        commenter_id => { class => MT->model('author'), optional => 1 },
1123    };
1124}
1125
1126package MT::Entry;
1127
1128sub parents {
1129    my $obj = shift;
1130    {
1131        blog_id => MT->model('blog'),
1132        author_id => { class => MT->model('author'), optional => 1, orphanize => 1 },
1133    };
1134}
1135
1136package MT::FileInfo;
1137
1138sub parents {
1139    my $obj = shift;
1140    {
1141        entry_id => { class => [ MT->model('entry'), MT->model('page') ], optional => 1 },
1142        blog_id => { class => MT->model('blog'), optional => 1 },
1143        templatemap_id => { class => MT->model('templatemap'), optional => 1 },
1144        template_id => { class => MT->model('template'), optional => 1 },
1145        category_id => { class => [ MT->model('category'), MT->model('folder') ], optional => 1 },
1146        author_id => { class => MT->model('author'), optional => 1 },
1147    };
1148}
1149
1150package MT::Notification;
1151
1152sub parents {
1153    my $obj = shift;
1154    {
1155        blog_id => MT->model('blog'),
1156    };
1157}
1158
1159package MT::ObjectTag;
1160
1161sub parents {
1162    my $obj = shift;
1163    {
1164        blog_id => MT->model('blog'),
1165        tag_id => MT->model('tag'),
1166        object_id => { relations => {
1167            key => 'object_datasource',
1168            entry_id => [ MT->model('entry'), MT->model('page') ],
1169        }}
1170    };
1171}
1172
1173package MT::Permission;
1174
1175sub parents {
1176    my $obj = shift;
1177    {
1178        blog_id => { class => MT->model('blog'), optional => 1 },
1179        author_id => { class => MT->model('author'), optional => 1 },
1180    };
1181}
1182
1183package MT::Placement;
1184
1185sub parents {
1186    my $obj = shift;
1187    {
1188        category_id => [ MT->model('category'), MT->model('folder') ],
1189        blog_id => MT->model('blog'),
1190        entry_id => [ MT->model('entry'), MT->model('page') ],
1191    };
1192}
1193
1194package MT::TBPing;
1195
1196sub parents {
1197    my $obj = shift;
1198    {
1199        blog_id => MT->model('blog'),
1200        tb_id => MT->model('trackback'),
1201    };
1202}
1203
1204package MT::Template;
1205
1206sub parents {
1207    my $obj = shift;
1208    {
1209        blog_id => MT->model('blog'),
1210    };
1211}
1212
1213package MT::TemplateMap;
1214
1215sub parents {
1216    my $obj = shift;
1217    {
1218        blog_id => MT->model('blog'),
1219        template_id  => MT->model('template')
1220    };
1221}
1222
1223package MT::Trackback;
1224
1225sub restore_parent_ids {
1226    my $obj = shift;
1227    my ($data, $objects) = @_;
1228
1229    my $result = 0;
1230    my $blog_class = MT->model('blog');
1231    my $new_blog = $objects->{$blog_class . '#' . $data->{blog_id}};
1232    if ($new_blog) {
1233        $data->{blog_id} = $new_blog->id;
1234    } else {
1235        return 0;
1236    }                           
1237    if (my $cid = $data->{category_id}) {
1238        my $cat_class = MT->model('category');
1239        my $new_obj = $objects->{$cat_class . '#' . $cid};
1240        unless ($new_obj) {
1241            $cat_class = MT->model('folder');
1242            $new_obj = $objects->{$cat_class . '#' . $cid};
1243        }
1244        if ($new_obj) {
1245            $data->{category_id} = $new_obj->id;
1246            $result = 1;
1247        }
1248    } elsif (my $eid = $data->{entry_id}) {
1249        my $entry_class = MT->model('entry');
1250        my $new_obj = $objects->{$entry_class . '#' . $eid};
1251        unless ($new_obj) {
1252            $entry_class = MT->model('page');
1253            $new_obj = $objects->{$entry_class . '#' . $eid};
1254        }
1255        if ($new_obj) {
1256            $data->{entry_id} = $new_obj->id;
1257            $result = 1;
1258        }
1259    }
1260    $result;
1261}
1262
1263package MT::ObjectAsset;
1264
1265sub parents {
1266    my $obj = shift;
1267    {
1268        blog_id => MT->model('blog'),
1269        asset_id => MT->model('asset'),
1270        object_id => { relations => {
1271            key => 'object_ds',
1272            entry_id => [ MT->model('entry'), MT->model('page') ],
1273        }}
1274    };
1275}
1276
1277package MT::ObjectScore;
1278
1279sub backup_terms_args {
1280    my $class = shift;
1281    my ($blog_ids) = @_;
1282
1283    return { terms => undef, args => undef };
1284}
1285
1286sub parents {
1287    my $obj = shift;
1288    {
1289        author_id => MT->model('author'),
1290        object_id => { relations => {
1291            key => 'object_ds',
1292            entry_id => [ MT->model('entry'), MT->model('page') ],
1293        }}
1294    };
1295}
1296
12971;
1298__END__
1299
1300=head1 NAME
1301
1302MT::BackupRestore
1303
1304=head1 METHODS
1305
1306=head2 backup
1307
1308TODO Backup I<MT::Tag>, I<MT::Author>, I<MT::Blog>, I<MT::Role>,
1309I<MT::Category>, I<MT::Asset>, and I<MT::Entry>.  Each object will
1310be back up by MT::Object#to_xml call, which will do the actual
1311Object ==>> XML serialization.
1312
1313=head2 restore_file
1314
1315TODO Restore MT system from an XML file which contains MT backup
1316information (created by backup subroutine).
1317
1318=head2 restore_process_single_file
1319
1320TODO A method which will do the actual heavy lifting of the
1321process to restore objects from an XML file.  Returns array of blog_ids
1322which are restored in the very session, and hash of asset_ids.
1323
1324=head2 restore_directory
1325
1326TODO A method which reads specified directory, find a manifest file,
1327and do the multi-file restore operation directed by the manifest file.
1328
1329=head2 restore_object_asset
1330
1331Accepts an asset object just restored, populate associated entries,
1332and scan text and text_more for each entry.  If association marker
1333(<form> tag) is found, replace asset id and URL to the new ones.
1334
1335=head2 restore_asset
1336
1337TODO A method which restores the assets' actual files to the
1338specified directory.  It also creates subdirectory by request.
1339
1340=head2 process_manifest
1341
1342TODO A method which is called from MT::App::CMS to process an uploaded
1343manifest file which is to be the source of the multi-file restore
1344operation in the MT::App::CMS.
1345
1346=head1 Callbacks
1347
1348For plugins which uses MT::Object-derived types, backup and restore
1349operation call callbacks for plugins to inject XMLs so they are
1350also backup, and read XML to restore objects so they are also restored.
1351
1352Callbacks called by the package are as follows:
1353
1354=over 4
1355
1356=item Backup
1357   
1358Calling convention is:
1359
1360    callback($cb, $blog_ids, $progress)
1361
1362The callback is used for MT::Object-derived types used by plugins
1363to be backup.  The callback must return the object's XML representation
1364in a string, or 1 for nothing.  $blog_ids has an ARRAY reference to
1365blog_ids which indicates what weblog a user chose to backup.  It may
1366be an empty array if a user chose Everything.  $progress is a CODEREF
1367used to report progress to the user.
1368
1369If a plugin has an MT::Object derived type, the plugin will register
1370a callback to Backup callback, and Backup process will call the callbacks
1371to give plugins a chance to add their own data to the backup file.
1372Otherwise, plugin's object classes is likely be ignored in backup operation.
1373
1374=item Restore.<element_name>:<xmlnamespace>
1375
1376Restore callbacks are called in convention like below:
1377
1378    callback($cb, $data, $objects, $deferred, $callback);
1379
1380Where $data is a parameter which was passed to XML::SAX::Base's
1381start_element callback method.
1382
1383$objects is an hash reference which contains all the restored objects
1384in the restore session.  The hash keys are stored in the format
1385MT::ObjectClassName#old_id, and hash values are object reference
1386of the actually restored objects (with new id).  Old ids are ids
1387which are stored in the XML files, while new ids are ids which
1388are restored.
1389
1390$deferred is an hash reference which contains information about
1391restore-deferred objects.  Deferred objects are those objects
1392which appeared in the XMl file but could not be restored because
1393any parent objects are missing.  The hash keys are stored in
1394the format MT::ObjectClassName#old_id and hash values are 1.
1395
1396$callback is a code reference which will print out the passed paramter.
1397Callback method can use this to communicate with users.
1398
1399If a plugin has an MT::Object derived type, the plugin will register
1400a callback to Restore.<element_name>:<xmlnamespace> callback,
1401so later the restore operation will call the callback function with
1402parameters described above.  XML Namespace is required to be registered,
1403so an xml node can be resolved into what plugins to be called back,
1404and can be distinguished the same element name with each other.
1405
1406=item restore
1407   
1408Calling convention is:
1409
1410    callback($cb, $objects, $deferred, $errors, $callback);
1411
1412This callback is called when all of the XML files in the particular
1413restore session are restored, thus, when $objects and $deferred
1414would not have any more objects in them.  This callback is useful
1415for object classes which have relationships with other classes,
1416for the kind of classes may not be able to handle relationship
1417correctly until the associated objects would be successfully
1418restored.
1419
1420NOTE that this callback is called BEFORE blogs' site_path and
1421site_url are updated.  Therefore, blog objects and other objects
1422which contains path information such as assets still have old
1423url and path in I<$objects>.
1424
1425$objects is an hash reference which contains all the restored objects
1426in the restore session.  The hash keys are stored in the format
1427MT::ObjectClassName#old_id, and hash values are object reference
1428of the actually restored objects (with new id).  Old ids are ids
1429which are stored in the XML files, while new ids are ids which
1430are restored.
1431
1432$deferred is an hash reference which contains information about
1433restore-deferred objects.  Deferred objects are those objects
1434which appeared in the XMl file but could not be restored because
1435any parent objects are missing.  The hash keys are stored in
1436the format MT::ObjectClassName#old_id and hash values are 1.
1437
1438$callback is a code reference which will print out the passed paramter.
1439Callback method can use this to communicate with users.
1440
1441=item restore_asset
1442   
1443Calling convention is:
1444
1445    callback($cb, $asset, $callback);
1446
1447This callback is called when asset's actual file is restored.
1448$asset has new url and path.
1449
1450$callback is a code reference which will print out the passed paramter.
1451Callback method can use this to communicate with users.
1452
1453=head1 AUTHOR & COPYRIGHT
1454
1455Please see L<MT/AUTHOR & COPYRIGHT>.
1456
1457=cut
Note: See TracBrowser for help on using the browser.