root/branches/release-35/lib/MT/BackupRestore.pm @ 1948

Revision 1948, 39.4 kB (checked in by fumiakiy, 20 months ago)

New metadata structure is now backup- and restore-able. BugId:79317

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