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

Revision 1902, 38.4 kB (checked in by fumiakiy, 20 months ago)

Restore userpic association. BugId:75025

  • 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        } elsif ( $key =~ /^MT::Author#(\d+)$/ ) {
472            # restore userpic association now
473            my $new_author = $all_objects->{$key};
474            if ( my $userpic_id = $new_author->userpic_asset_id ) {
475                if ( my $new_asset = $all_objects->{'MT::Asset#' . $userpic_id} ) {
476                    $new_author->userpic_asset_id( $new_asset->id );
477                    $new_author->update;
478                }
479            }
480        }
481    }
482
483    my $i = 0;
484    $callback->(
485        MT->translate("Restoring asset associations ... ( [_1] )", $i++),
486        'cb-restore-entry-asset'
487    );
488    for my $obj_id ( keys %entries ) {
489        my $entry = $entries{$obj_id};
490
491        my @placements = MT->model('objectasset')->load( {
492            object_id => $obj_id, 
493            object_ds => 'entry', 
494            blog_id => $entry->blog_id
495        });
496        next unless @placements;
497
498        my %related;
499        for my $placement ( @placements ) {
500            my $asset_hash = $assets{$placement->asset_id};
501            next unless $asset_hash;
502            $related{ $asset_hash->{old_id} } = $asset_hash->{object};
503        }
504
505        if ($entry->class == 'entry') {
506            $callback->(
507                MT->translate("Restoring asset associations in entry ... ( [_1] )", $i++),
508                'cb-restore-entry-asset'
509            );
510        } else {
511            $callback->(
512                MT->translate("Restoring asset associations in page ... ( [_1] )", $i++),
513                'cb-restore-entry-asset'
514            );
515        }
516       
517        for my $col ( qw( text text_more ) ) {
518            my $text = $entry->$col;
519            next unless $text;
520            $text = _sync_asset_id( $text, \%related ); 
521            $entry->$col($text);
522        }
523        $entry->update();  # directly call update to bypass processing in save()
524    }
525    $callback->( MT->translate("Done.") . "\n" );
526    1;
527}
528
529sub _sync_asset_url {
530    my ($text, $asset) = @_;
531
532    my $filename = quotemeta(encode_url($asset->file_name));
533    my $url = $asset->url;
534    my $id = $asset->id;
535    my @children = MT->model('asset')->load(
536        { parent => $asset->id, blog_id => $asset->blog_id, class => '*' }
537    );
538    my %children = map {
539        $_->id => {
540            'filename' => quotemeta(encode_url($_->file_name)),
541            'url' => $_->url
542        }
543    } @children;
544
545    $text =~ s!<form([^>]*?\s)mt:asset-id=(["'])$id(["'])([^>]*?)>(.+?)</form>!
546        my $result = '<form' . $1 . 'mt:asset-id=' . $2 . $id . $3 . $4 . '>';
547        my $html = $5;
548        $html =~ s#<a([^>]*? )href=(["'])[^>]+?/$filename(["'])([^>]*?)>#<a$1href=$2$url$3$4>#gim;
549        $html =~ s#<img([^>]*? )src=(["'])[^>]+?/$filename(["'])([^>]*?)(/? *)>#<img$1src=$2$url$3$4$5>#gim;
550        if ( %children ) {
551            for my $child (values %children) {
552                my $child_filename = $child->{filename};
553                my $child_url = $child->{url};
554                $html =~ s#<img([^>]*? )src=(["'])[^>]+?/$child_filename(["'])([^>]*?)>#<img$1src=$2$child_url$3$4>#gim;
555                $html =~ s#<a([^>]*? )href=(["'])[^>]+?/$child_filename(["'])([^>]*?)>#<a$1href=$2$child_url$3$4>#gim;
556                $html =~ s#<a([^>]*? )onclick=(["'])[^>]+?/$child_filename(["'])([^>]*?)>#<a$1onclick=$2$child_url$3$4>#gim;
557            }
558        }
559        $result .= $html . '</form>';
560        $result;
561    !igem;
562    $text;
563}
564
565sub cb_restore_asset {
566    my $pkg = shift;
567    my ($asset, $callback) = @_;
568   
569    my @placements = MT->model('objectasset')->load( {
570        asset_id => $asset->id, 
571        blog_id => $asset->blog_id
572    });
573
574    my $i = 0;
575    $callback->(
576        MT->translate('Restoring url of the assets ( [_1] )...', $i++),
577        'cb-restore-asset-url'
578    );
579    for my $placement (@placements) { 
580        next unless 'entry' eq $placement->object_ds; 
581        my $entry = MT->model('entry')->load( $placement->object_id ); 
582        next unless $entry; 
583       
584        if ($entry->class == 'entry') {
585            $callback->(
586                MT->translate('Restoring url of the assets in entry ( [_1] )...', $i++),
587                'cb-restore-asset-url'
588            );
589        } else {
590            $callback->(
591                MT->translate('Restoring url of the assets in page ( [_1] )...', $i++),
592                'cb-restore-asset-url'
593            );
594        }
595        for my $col ( qw( text text_more ) ) {
596            my $text = $entry->$col;
597            next unless $text;
598            $text = _sync_asset_url( $text, $asset );
599            $entry->$col($text);
600        }
601        $entry->update();  # directly call update to bypass processing in save()
602    }
603    $callback->( MT->translate("Done.") . "\n" );
604    1;
605}
606
607sub _restore_asset_multi {
608    my $class = shift;
609    my ($asset_element, $objects, $errors, $callback, $blogs_meta) = @_;
610
611    my $old_id = $asset_element->{asset_id};
612    if (!defined($old_id)) {
613        $callback->(MT->translate('ID for the file was not set.'));
614        return 0;
615    }
616    my $asset_class = MT->model('asset');
617    my $asset = $objects->{"$asset_class#$old_id"};
618    unless (defined($asset)) {
619        $callback->(MT->translate('The file ([_1]) was not restored.', $old_id));
620        return 0;
621    }
622
623    my $fmgr;
624    if (exists $blogs_meta->{$asset->blog_id}) {
625        my $blog = MT->model('blog')->load($asset->blog_id)
626            or return 0;
627
628        my $meta = $blogs_meta->{$asset->blog_id};
629        my $path = $asset->file_path;
630        my $url = $asset->url;
631        if (my $archive_path = $meta->{'archive_path'}) {
632            my $old_archive_path = $meta->{'old_archive_path'};
633            $path =~ s/\Q$old_archive_path\E/$archive_path/i;
634            $asset->file_path($path);
635        }
636        if (my $archive_url = $meta->{'archive_url'}) {
637            my $old_archive_url = $meta->{'old_archive_url'};
638            $url =~ s/\Q$old_archive_url\E/$archive_url/i;
639            $asset->url($url);
640        }
641        if (my $site_path = $meta->{'site_path'}) {
642            my $old_site_path = $meta->{'old_site_path'};
643            $path =~ s/\Q$old_site_path\E/$site_path/i;
644            $asset->file_path($path);
645        }
646        if (my $site_url = $meta->{'site_url'}) {
647            my $old_site_url = $meta->{'old_site_url'};
648            $url =~ s/\Q$old_site_url\E/$site_url/i;
649            $asset->url($url);
650        }
651        $callback->(MT->translate("Changing path for the file '[_1]' (ID:[_2])...", $asset->label, $asset->id));
652        $asset->save or $callback->(MT->translate("failed") . "\n");
653        $callback->(MT->translate("ok") . "\n");
654
655        $fmgr = $blog->file_mgr;
656    }
657    my $file = $asset_element->{fh};
658    $class->restore_asset($file, $asset, $old_id, $fmgr, $errors, $callback);
659}
660
661package MT::Object;
662
663sub _is_element {
664    my $obj = shift;
665    my ($def) = @_;
666    return (('text' eq $def->{type}) || (('string' eq $def->{type}) && (255 < $def->{size}))) ? 1 : 0;
667}
668
669sub to_xml {
670    my $obj = shift;
671    my ($namespace, $args) = @_;
672
673    my $coldefs = $obj->column_defs;
674    my $colnames = $obj->column_names;
675    my $xml;
676
677    my $elem = $obj->datasource;
678    $xml = '<' . $elem;
679    $xml .= " xmlns='$namespace'" if defined($namespace) && $namespace;
680
681    my (@elements, @blobs, @meta);
682    for my $name (@$colnames) {
683        if ($obj->column($name) || (defined($obj->column($name)) && ('0' eq $obj->column($name)))) {
684            if ( ( $obj->properties->{meta_column} || '' ) eq $name ) {
685                push @meta, $name;
686                next;
687            }
688            elsif ($obj->_is_element($coldefs->{$name})) {
689                push @elements, $name;
690                next;
691            } elsif ('blob' eq $coldefs->{$name}->{type}) {
692                push @blobs, $name;
693                next;
694            }
695            $xml .= " $name='" . MT::Util::encode_xml($obj->column($name), 1) . "'";
696        }
697    }
698    $xml .= '>';
699    $xml .= "<$_>" . MT::Util::encode_xml($obj->column($_), 1) . "</$_>" foreach @elements;
700    require MIME::Base64;
701    $xml .= "<$_>" . MIME::Base64::encode_base64($obj->column($_), '') . "</$_>" foreach @blobs;
702    foreach my $meta_col (@meta) {
703        my $hashref = $obj->$meta_col;
704        $xml .= "<$meta_col>" . 
705                MIME::Base64::encode_base64(MT::Serialize->serialize(\$hashref), '') .
706                "</$meta_col>";
707    }
708    $xml .= '</' . $elem . '>';
709    $xml;
710}
711
712sub parents {
713    my $obj = shift;
714    {};
715}
716
717sub _restore_id {
718    my $obj = shift;
719    my ($key, $val, $data, $objects) = @_;
720
721    return 0 unless 'ARRAY' eq ref($val);
722    return 1 if 0 == $data->{$key}; 
723
724    my $new_obj;
725    my $old_id = $data->{$key};
726    foreach (@$val) {
727        $new_obj = $objects->{"$_#$old_id"};
728        last if $new_obj;
729    }
730    return 0 unless $new_obj;
731    $data->{$key} = $new_obj->id;
732    return 1;
733}
734
735sub restore_parent_ids {
736    my $obj = shift;
737    my ($data, $objects) = @_;
738
739    my $parents = $obj->parents;
740    my $count = scalar(keys %$parents);
741
742    my $done = 0;
743    while (my ($key, $val) = each(%$parents)) {
744        $val = [ $val ] unless (ref $val);
745        if ('ARRAY' eq ref($val)) {
746            $done += $obj->_restore_id($key, $val, $data, $objects);
747        }
748        elsif ('HASH' eq ref($val)) {
749            my $v = $val->{class};
750            $v = [ $v ] unless (ref $v);
751            my $result = 0;
752            if (my $relations = $val->{relations}) {
753                my $col = $relations->{key};
754                my $ds = $data->{$col};
755                my $ev = $relations->{$ds . '_id'};
756                $ev = MT->model($ds) unless $ev;
757                return 0 unless $ev;
758                $ev = [ $ev ] unless (ref $ev);
759                $done += $obj->_restore_id($key, $ev, $data, $objects);
760            }
761            else {
762                $result = $obj->_restore_id($key, $v, $data, $objects);
763                $result = 1 if exists($val->{optional}) && $val->{optional};
764                $data->{$key} = -1 
765                  if !$result && (exists($val->{orphanize}) && $val->{orphanize});
766                $done += $result;
767            }
768        }
769    }
770    ($count == $done) ? 1 : 0;   
771}
772
773package MT::Blog;
774
775sub backup_terms_args {
776    my $class = shift;
777    my ($blog_ids) = @_;
778
779    if ( defined($blog_ids) && scalar(@$blog_ids) ) {
780        return
781          {
782            terms => { id => $blog_ids }, 
783            args => undef,
784          };
785    }
786    else {
787        return { terms => undef, args => undef };
788    }
789}
790
791package MT::Tag;
792
793sub backup_terms_args {
794    my $class = shift;
795    my ($blog_ids) = @_;
796
797    if ( defined($blog_ids) && scalar(@$blog_ids) ) {
798        return
799          {
800            terms => undef, 
801            args =>
802              {
803                'join' =>
804                  [
805                    'MT::ObjectTag', 
806                    'tag_id',
807                    { blog_id => $blog_ids }, 
808                    { unique => 1 }
809                  ]
810              }
811          };
812    }
813    else {
814        return { terms => undef, args => undef };
815    }
816}
817
818package MT::Role;
819
820sub backup_terms_args {
821    my $class = shift;
822    my ($blog_ids) = @_;
823
824    if ( defined($blog_ids) && scalar(@$blog_ids) ) {
825        return
826          {
827            terms => undef, 
828            args =>
829              {
830                'join' =>
831                  [
832                    'MT::Association',
833                    'role_id',
834                    { blog_id => $blog_ids },
835                    { unique => 1 }
836                  ]
837              }
838          };
839    }
840    else {
841        return { terms => undef, args => undef };
842    }
843}
844
845package MT::Asset;
846
847sub backup_terms_args {
848    my $class = shift;
849    my ($blog_ids) = @_;
850
851    if ( defined($blog_ids) && scalar(@$blog_ids) ) {
852        return
853          {
854            terms => { 'blog_id' => $blog_ids, 'class' => '*' }, 
855            args => undef
856          }
857    }
858    else {
859        return { terms => { 'class' => '*' }, args => undef };
860    }
861}
862
863my $assets_seen = {};
864
865sub to_xml {
866    my $obj = shift;
867    my $xml = q();
868
869    return $xml if exists $assets_seen->{$obj->id};
870
871    if ($obj->parent) {
872        my $parent = MT->model('asset')->load($obj->parent)
873            or return $xml;
874        $xml .= $parent->to_xml(@_);
875        $xml .= "\n";
876    }
877
878    $xml .= $obj->SUPER::to_xml(@_);
879    $assets_seen->{$obj->id} = 1;
880    $xml;
881}
882
883sub parents {
884    my $obj = shift;
885    {
886        blog_id => MT->model('blog'),
887        parent  => MT->model('asset')
888    };
889}
890
891package MT::PluginData;
892
893sub backup_terms_args {
894    my $class = shift;
895    my ($blog_ids) = @_;
896
897    return { terms => undef, args => undef };
898}
899
900sub restore_parent_ids {
901    my $obj = shift;
902    my ($data, $objects) = @_;
903
904    if ($data->{key} =~ /^configuration:blog:(\d+)$/i) {
905        my $new_blog = $objects->{'MT::Blog#' . $1};
906        if ($new_blog) {
907            $data->{key} = 'configuration:blog:' . $new_blog->id;
908        }
909    }
910    return 1;
911}
912
913package MT::Association;
914
915sub restore_parent_ids {
916    my $obj = shift;
917    my ($data, $objects) = @_;
918
919    my ($u, $b, $g, $r) = (0, 0, 0, 0);
920
921    my $processor = sub {
922        my ($elem) = @_;
923        my $class = MT->model($elem);
924        my $old_id = $data->{$elem . '_id'};
925        my $new_obj = $objects->{"$class#$old_id"};
926        return 0 unless defined($new_obj) && $new_obj;
927        $data->{$elem . '_id'} = $new_obj->id;
928        return 1;
929    };
930
931    $u = $processor->('author');
932    $g = $processor->('group');
933    $b = $processor->('blog');
934    $r = $processor->('role');
935
936    # Combination allowed are:
937    # USER_BLOG_ROLE  => 1;
938    # GROUP_BLOG_ROLE => 2;
939    # USER_GROUP      => 3;
940    # USER_ROLE       => 4;
941    # GROUP_ROLE      => 5;
942
943    ($u && $g) || ($u && $r) || ($g && $r) ? 1 : 0; # || ($u && $b && $r) || ($g && $b && $r)
944}
945
946package MT::Category;
947
948my $category_seen = {};
949
950sub to_xml {
951    my $obj = shift;
952    my $xml = q();
953
954    return $xml if exists $category_seen->{$obj->id};
955   
956    if ('0' ne $obj->parent) {
957        $xml .= $obj->parent_category->to_xml(@_);
958        $xml .= "\n";
959    }
960
961    $xml .= $obj->SUPER::to_xml(@_);
962    $category_seen->{$obj->id} = 1;
963    $xml;
964}
965
966sub parents {
967    my $obj = shift;
968    {
969        blog_id => MT->model('blog'),
970        parent  => [ MT->model('category'), MT->model('folder') ],
971    };
972}
973
974package MT::Comment;
975
976sub parents {
977    my $obj = shift;
978    {
979        entry_id => [ MT->model('entry'), MT->model('page') ],
980        blog_id => MT->model('blog'),
981        commenter_id => { class => MT->model('author'), optional => 1 },
982    };
983}
984
985package MT::Entry;
986
987sub parents {
988    my $obj = shift;
989    {
990        blog_id => MT->model('blog'),
991        author_id => { class => MT->model('author'), optional => 1, orphanize => 1 },
992    };
993}
994
995package MT::Notification;
996
997sub parents {
998    my $obj = shift;
999    {
1000        blog_id => MT->model('blog'),
1001    };
1002}
1003
1004package MT::ObjectTag;
1005
1006sub parents {
1007    my $obj = shift;
1008    {
1009        blog_id => MT->model('blog'),
1010        tag_id => MT->model('tag'),
1011        object_id => { relations => {
1012            key => 'object_datasource',
1013            entry_id => [ MT->model('entry'), MT->model('page') ],
1014        }}
1015    };
1016}
1017
1018package MT::Permission;
1019
1020sub parents {
1021    my $obj = shift;
1022    {
1023        blog_id => { class => MT->model('blog'), optional => 1 },
1024        author_id => { class => MT->model('author'), optional => 1 },
1025    };
1026}
1027
1028package MT::Placement;
1029
1030sub parents {
1031    my $obj = shift;
1032    {
1033        category_id => [ MT->model('category'), MT->model('folder') ],
1034        blog_id => MT->model('blog'),
1035        entry_id => [ MT->model('entry'), MT->model('page') ],
1036    };
1037}
1038
1039package MT::TBPing;
1040
1041sub parents {
1042    my $obj = shift;
1043    {
1044        blog_id => MT->model('blog'),
1045        tb_id => MT->model('trackback'),
1046    };
1047}
1048
1049package MT::Template;
1050
1051sub parents {
1052    my $obj = shift;
1053    {
1054        blog_id => MT->model('blog'),
1055    };
1056}
1057
1058package MT::TemplateMap;
1059
1060sub parents {
1061    my $obj = shift;
1062    {
1063        blog_id => MT->model('blog'),
1064        template_id  => MT->model('template')
1065    };
1066}
1067
1068package MT::Trackback;
1069
1070sub restore_parent_ids {
1071    my $obj = shift;
1072    my ($data, $objects) = @_;
1073
1074    my $result = 0;
1075    my $blog_class = MT->model('blog');
1076    my $new_blog = $objects->{$blog_class . '#' . $data->{blog_id}};
1077    if ($new_blog) {
1078        $data->{blog_id} = $new_blog->id;
1079    } else {
1080        return 0;
1081    }                           
1082    if (my $cid = $data->{category_id}) {
1083        my $cat_class = MT->model('category');
1084        my $new_obj = $objects->{$cat_class . '#' . $cid};
1085        unless ($new_obj) {
1086            $cat_class = MT->model('folder');
1087            $new_obj = $objects->{$cat_class . '#' . $cid};
1088        }
1089        if ($new_obj) {
1090            $data->{category_id} = $new_obj->id;
1091            $result = 1;
1092        }
1093    } elsif (my $eid = $data->{entry_id}) {
1094        my $entry_class = MT->model('entry');
1095        my $new_obj = $objects->{$entry_class . '#' . $eid};
1096        unless ($new_obj) {
1097            $entry_class = MT->model('page');
1098            $new_obj = $objects->{$entry_class . '#' . $eid};
1099        }
1100        if ($new_obj) {
1101            $data->{entry_id} = $new_obj->id;
1102            $result = 1;
1103        }
1104    }
1105    $result;
1106}
1107
1108package MT::ObjectAsset;
1109
1110sub parents {
1111    my $obj = shift;
1112    {
1113        blog_id => MT->model('blog'),
1114        asset_id => MT->model('asset'),
1115        object_id => { relations => {
1116            key => 'object_ds',
1117            entry_id => [ MT->model('entry'), MT->model('page') ],
1118        }}
1119    };
1120}
1121
1122package MT::ObjectScore;
1123
1124sub backup_terms_args {
1125    my $class = shift;
1126    my ($blog_ids) = @_;
1127
1128    return { terms => undef, args => undef };
1129}
1130
1131sub parents {
1132    my $obj = shift;
1133    {
1134        author_id => MT->model('author'),
1135        object_id => { relations => {
1136            key => 'object_ds',
1137            entry_id => [ MT->model('entry'), MT->model('page') ],
1138        }}
1139    };
1140}
1141
11421;
1143__END__
1144
1145=head1 NAME
1146
1147MT::BackupRestore
1148
1149=head1 METHODS
1150
1151=head2 backup
1152
1153TODO Backup I<MT::Tag>, I<MT::Author>, I<MT::Blog>, I<MT::Role>,
1154I<MT::Category>, I<MT::Asset>, and I<MT::Entry>.  Each object will
1155be back up by MT::Object#to_xml call, which will do the actual
1156Object ==>> XML serialization.
1157
1158=head2 restore_file
1159
1160TODO Restore MT system from an XML file which contains MT backup
1161information (created by backup subroutine).
1162
1163=head2 restore_process_single_file
1164
1165TODO A method which will do the actual heavy lifting of the
1166process to restore objects from an XML file.  Returns array of blog_ids
1167which are restored in the very session, and hash of asset_ids.
1168
1169=head2 restore_directory
1170
1171TODO A method which reads specified directory, find a manifest file,
1172and do the multi-file restore operation directed by the manifest file.
1173
1174=head2 restore_object_asset
1175
1176Accepts an asset object just restored, populate associated entries,
1177and scan text and text_more for each entry.  If association marker
1178(<form> tag) is found, replace asset id and URL to the new ones.
1179
1180=head2 restore_asset
1181
1182TODO A method which restores the assets' actual files to the
1183specified directory.  It also creates subdirectory by request.
1184
1185=head2 process_manifest
1186
1187TODO A method which is called from MT::App::CMS to process an uploaded
1188manifest file which is to be the source of the multi-file restore
1189operation in the MT::App::CMS.
1190
1191=head1 Callbacks
1192
1193For plugins which uses MT::Object-derived types, backup and restore
1194operation call callbacks for plugins to inject XMLs so they are
1195also backup, and read XML to restore objects so they are also restored.
1196
1197Callbacks called by the package are as follows:
1198
1199=over 4
1200
1201=item Backup
1202   
1203Calling convention is:
1204
1205    callback($cb, $blog_ids, $progress)
1206
1207The callback is used for MT::Object-derived types used by plugins
1208to be backup.  The callback must return the object's XML representation
1209in a string, or 1 for nothing.  $blog_ids has an ARRAY reference to
1210blog_ids which indicates what weblog a user chose to backup.  It may
1211be an empty array if a user chose Everything.  $progress is a CODEREF
1212used to report progress to the user.
1213
1214If a plugin has an MT::Object derived type, the plugin will register
1215a callback to Backup callback, and Backup process will call the callbacks
1216to give plugins a chance to add their own data to the backup file.
1217Otherwise, plugin's object classes is likely be ignored in backup operation.
1218
1219=item Restore.<element_name>:<xmlnamespace>
1220
1221Restore callbacks are called in convention like below:
1222
1223    callback($cb, $data, $objects, $deferred, $callback);
1224
1225Where $data is a parameter which was passed to XML::SAX::Base's
1226start_element callback method.
1227
1228$objects is an hash reference which contains all the restored objects
1229in the restore session.  The hash keys are stored in the format
1230MT::ObjectClassName#old_id, and hash values are object reference
1231of the actually restored objects (with new id).  Old ids are ids
1232which are stored in the XML files, while new ids are ids which
1233are restored.
1234
1235$deferred is an hash reference which contains information about
1236restore-deferred objects.  Deferred objects are those objects
1237which appeared in the XMl file but could not be restored because
1238any parent objects are missing.  The hash keys are stored in
1239the format MT::ObjectClassName#old_id and hash values are 1.
1240
1241$callback is a code reference which will print out the passed paramter.
1242Callback method can use this to communicate with users.
1243
1244If a plugin has an MT::Object derived type, the plugin will register
1245a callback to Restore.<element_name>:<xmlnamespace> callback,
1246so later the restore operation will call the callback function with
1247parameters described above.  XML Namespace is required to be registered,
1248so an xml node can be resolved into what plugins to be called back,
1249and can be distinguished the same element name with each other.
1250
1251=item restore
1252   
1253Calling convention is:
1254
1255    callback($cb, $objects, $deferred, $errors, $callback);
1256
1257This callback is called when all of the XML files in the particular
1258restore session are restored, thus, when $objects and $deferred
1259would not have any more objects in them.  This callback is useful
1260for object classes which have relationships with other classes,
1261for the kind of classes may not be able to handle relationship
1262correctly until the associated objects would be successfully
1263restored.
1264
1265NOTE that this callback is called BEFORE blogs' site_path and
1266site_url are updated.  Therefore, blog objects and other objects
1267which contains path information such as assets still have old
1268url and path in I<$objects>.
1269
1270$objects is an hash reference which contains all the restored objects
1271in the restore session.  The hash keys are stored in the format
1272MT::ObjectClassName#old_id, and hash values are object reference
1273of the actually restored objects (with new id).  Old ids are ids
1274which are stored in the XML files, while new ids are ids which
1275are restored.
1276
1277$deferred is an hash reference which contains information about
1278restore-deferred objects.  Deferred objects are those objects
1279which appeared in the XMl file but could not be restored because
1280any parent objects are missing.  The hash keys are stored in
1281the format MT::ObjectClassName#old_id and hash values are 1.
1282
1283$callback is a code reference which will print out the passed paramter.
1284Callback method can use this to communicate with users.
1285
1286=item restore_asset
1287   
1288Calling convention is:
1289
1290    callback($cb, $asset, $callback);
1291
1292This callback is called when asset's actual file is restored.
1293$asset has new url and path.
1294
1295$callback is a code reference which will print out the passed paramter.
1296Callback method can use this to communicate with users.
1297
1298=head1 AUTHOR & COPYRIGHT
1299
1300Please see L<MT/AUTHOR & COPYRIGHT>.
1301
1302=cut
Note: See TracBrowser for help on using the browser.