root/branches/release-34/lib/MT/BackupRestore.pm @ 1823

Revision 1823, 37.9 kB (checked in by takayama, 20 months ago)

Fixed BugId:67959
* Added check for result of object loading

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