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

Revision 2693, 43.3 kB (checked in by fumiakiy, 17 months ago)

Lowering the order of the MT::Author to be backed up for Custom Fields to be restored before it. BugId:80479

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