root/branches/release-39/lib/MT/BackupRestore.pm @ 2548

Revision 2548, 43.3 kB (checked in by bchoate, 18 months ago)

Updates to iterator handling and use of 'window_size' argument for load_iter method of MT::Object. BugId:79247

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