package MT::CMS::Tools; use strict; use Symbol; use MT::I18N qw( encode_text wrap_text ); use MT::Util qw( encode_url encode_html decode_html encode_js trim ); sub system_check { my $app = shift; if ( my $blog_id = $app->param('blog_id') ) { return $app->redirect( $app->uri( 'mode' => 'view_log', args => { blog_id => $blog_id } ) ); } my %param; # licensed user count: someone who has logged in within 90 days my $sess_class = $app->model('session'); my $from = time - ( 60 * 60 * 24 * 90 + 60 * 60 * 24 ); $sess_class->remove( { kind => 'UA', start => [ undef, $from ] }, { range => { start => 1 } } ); $param{licensed_user_count} = $sess_class->count( { kind => 'UA' } ); my $author_class = $app->model('author'); $param{user_count} = $author_class->count( { type => MT::Author::AUTHOR() } ); $param{commenter_count} = q[N/A]; $param{screen_id} = "system-check"; require MT::Memcached; if (MT::Memcached->is_available) { $param{memcached_enabled} = 1; my $inst = MT::Memcached->instance; my $key = 'syscheck-' . $$; $inst->add($key, $$); if ($inst->get($key) == $$) { $inst->delete($key); $param{memcached_active} = 1; } } $param{server_modperl} = 1 if $ENV{MOD_PERL}; $param{server_fastcgi} = 1 if $ENV{FAST_CGI}; $param{syscheck_html} = get_syscheck_content($app) || ''; $app->load_tmpl( 'system_check.tmpl', \%param ); } sub get_syscheck_content { my $app = shift; my $syscheck_url = $app->base . $app->mt_path . $app->config('CheckScript') . '?view=tools&version=' . MT->version_id; if ( $syscheck_url && $syscheck_url ne 'disable' ) { my $SYSCHECKCACHE_TIMEOUT = 60 * 60 * 24; my $sess_class = $app->model('session'); my ($syscheck_object) = (""); my $retries = 0; $syscheck_object = $sess_class->load( { id => 'SC' } ); if ( $syscheck_object && ( $syscheck_object->start() < ( time - $SYSCHECKCACHE_TIMEOUT ) ) ) { $syscheck_object->remove; $syscheck_object = undef; } return encode_text( $syscheck_object->data(), 'utf-8', undef ) if ($syscheck_object); my $ua = $app->new_ua({ timeout => 20 }); return unless $ua; $ua->max_size(undef) if $ua->can('max_size'); my $req = new HTTP::Request( GET => $syscheck_url ); my $resp = $ua->request($req); return unless $resp->is_success(); my $result = $resp->content(); if ($result) { require MT::Sanitize; # allowed html my $spec = '* style class id,ul,li,div,span,br,h2,h3,strong,code,blockquote,p'; $result = MT::Sanitize->sanitize( $result, $spec ); $syscheck_object = MT::Session->new(); $syscheck_object->set_values( { id => 'SC', kind => 'SC', start => time(), data => $result } ); $syscheck_object->save(); $result = encode_text( $result, 'utf-8', undef ); } return $result; } } sub start_recover { my $app = shift; my ($param) = @_; my $cfg = $app->config; $param ||= {}; $param->{'email'} = $app->param('email'); $param->{'return_to'} = $app->param('return_to') || $cfg->ReturnToURL || ''; $param->{'can_signin'} = (ref $app eq 'MT::App::CMS') ? 1 : 0; $app->add_breadcrumb( $app->translate('Password Recovery') ); my $blog_id = $app->param('blog_id'); $param->{'blog_id'} = $blog_id; my $tmpl = $app->load_global_tmpl( { identifier => 'new_password_reset_form', $blog_id ? ( blog_id => $app->param('blog_id') ) : () } ); if (!$tmpl) { $tmpl = $app->load_tmpl( 'cms/dialog/recover.tmpl' ); } $tmpl->param($param); return $tmpl; } sub recover_password { my $app = shift; my $email = $app->param('email') || ''; my $username = $app->param('name'); $email = trim($email); $username = trim($username) if $username; if ( !$email ) { return $app->start_recover( { error => $app->translate('Email Address is required for password recovery.'), } ); } # Searching user by email (and username) my $class = ref $app eq 'MT::App::Upgrader' ? 'MT::BasicAuthor' : $app->model('author'); eval "use $class;"; my @all_authors = $class->load( { email => $email, ( $username ? ( name => $username ) : () ) } ); my @authors; my $user; foreach (@all_authors) { next unless $_->password && ( $_->password ne '(none)' ); push( @authors, $_ ); } if ( !@authors ) { return $app->start_recover( { error => $app->translate('User not found'), ( $username ? ( not_unique_email => 1 ) : () ), } ); } elsif ( @authors > 1 ) { return $app->start_recover( { not_unique_email => 1, } ); } $user = pop @authors; # Generate Token require MT::Util::Captcha; my $salt = MT::Util::Captcha->_generate_code(8); my $expires = time + ( 60 * 60 ); my $token = MT::Util::perl_sha1_digest_hex( $salt . $expires . $app->config->SecretToken ); $user->password_reset($salt); $user->password_reset_expires($expires); $user->password_reset_return_to($app->param('return_to')) if $app->param('return_to'); $user->save; # Send mail to user my %head = ( id => 'recover_password', To => $email, From => $app->config('EmailAddressMain') || $email, Subject => $app->translate("Password Recovery") ); my $charset = $app->charset; my $mail_enc = uc( $app->config('MailEncoding') || $charset ); $head{'Content-Type'} = qq(text/plain; charset="$mail_enc"); my $blog_id = $app->param('blog_id'); my $body = $app->build_email( 'recover-password', { link_to_login => $app->base . $app->uri . "?__mode=new_pw&token=$token&email=" . encode_url($email) . ($blog_id ? "&blog_id=$blog_id" : ''), } ); require MT::Mail; MT::Mail->send( \%head, $body ) or return $app->error( $app->translate( "Error sending mail ([_1]); please fix the problem, then " . "try again to recover your password.", MT::Mail->errstr ) ); return $app->start_recover( { recovered => 1, } ); } sub new_password { my $app = shift; my ($param) = @_; $param ||= {}; my $token = $app->param('token'); if ( !$token ) { return $app->start_recover( { error => $app->translate('Password reset token not found'), } ); } my $email = $app->param('email'); if ( !$email ) { return $app->start_recover( { error => $app->translate('Email address not found'), } ); } my $class = $app->model('author'); my @users = $class->load( { email => $email } ); if ( !@users ) { return $app->start_recover( { error => $app->translate('User not found'), } ); } # comparing token require MT::Util::Captcha; my $user; for my $u (@users) { my $salt = $u->password_reset; my $expires = $u->password_reset_expires; my $compare = MT::Util::perl_sha1_digest_hex( $salt . $expires . $app->config->SecretToken ); if ( $compare eq $token ) { if ( time > $u->password_reset_expires ) { return $app->start_recover( { error => $app->translate( 'Your request to change your password has expired.' ), } ); } $user = $u; last; } } if ( !$user ) { return $app->start_recover( { error => $app->translate('Invalid password reset request'), } ); } # Password reset my $new_password = $app->param('password'); if ($new_password) { my $again = $app->param('password_again'); if ( !$again ) { $param->{'error'} = $app->translate('Please confirm your new password'); } elsif ( $new_password ne $again ) { $param->{'error'} = $app->translate('Passwords do not match'); } else { my $redirect = $user->password_reset_return_to || ''; $user->set_password($new_password); $user->password_reset(undef); $user->password_reset_expires(undef); $user->password_reset_return_to(undef); $user->save; $app->param( 'username', $user->name ) if $user->type == MT::Author::AUTHOR(); if (ref $app eq 'MT::App::CMS' && !$redirect) { $app->login; return $app->return_to_dashboard( redirect => 1 ); } else { if (!$redirect) { my $cfg = $app->config; $redirect = $cfg->ReturnToURL || ''; } $app->make_commenter_session($user); if ($redirect) { return $app->redirect($redirect); } else { return $app->redirect_to_edit_profile(); } } } } $param->{'email'} = $email; $param->{'token'} = $token; $param->{'password'} = $app->param('password'); $param->{'password_again'} = $app->param('password_again'); $app->add_breadcrumb( $app->translate('Password Recovery') ); my $blog_id = $app->param('blog_id'); $param->{'blog_id'} = $blog_id if $blog_id; my $tmpl = $app->load_global_tmpl( { identifier => 'new_password', $blog_id ? ( blog_id => $app->param('blog_id') ) : () } ); if (!$tmpl) { $tmpl = $app->load_tmpl( 'cms/dialog/new_password.tmpl' ); } $tmpl->param($param); return $tmpl; } sub do_list_action { my $app = shift; $app->validate_magic or return; # plugin_action_selector should always (?) be in the query; use it? my $action_name = $app->param('action_name'); my $type = $app->param('_type'); my ($the_action) = ( grep { $_->{key} eq $action_name } @{ $app->list_actions($type) } ); return $app->errtrans( "That action ([_1]) is apparently not implemented!", $action_name ) unless $the_action; unless ( ref( $the_action->{code} ) ) { if ( my $plugin = $the_action->{plugin} ) { $the_action->{code} = $app->handler_to_coderef( $the_action->{handler} || $the_action->{code} ); } } $the_action->{code}->($app); } sub do_page_action { my $app = shift; # plugin_action_selector should always (?) be in the query; use it? my $action_name = $app->param('action_name'); my $type = $app->param('_type'); my ($the_action) = ( grep { $_->{key} eq $action_name } @{ $app->page_actions($type) } ); return $app->errtrans( "That action ([_1]) is apparently not implemented!", $action_name ) unless $the_action; unless ( ref( $the_action->{code} ) ) { if ( my $plugin = $the_action->{plugin} ) { $the_action->{code} = $app->handler_to_coderef( $the_action->{handler} || $the_action->{code} ); } } $the_action->{code}->($app); } sub cfg_system_general { my $app = shift; my %param; if ( $app->param('blog_id') ) { return $app->return_to_dashboard( redirect => 1 ); } return $app->errtrans("Permission denied.") unless $app->user->is_superuser(); my $cfg = $app->config; $app->add_breadcrumb( $app->translate('General Settings') ); $param{nav_config} = 1; $param{nav_settings} = 1; $param{languages} = MT::I18N::languages_list( $app, $app->config('DefaultUserLanguage') ); my $tag_delim = $app->config('DefaultUserTagDelimiter') || 'comma'; $param{"tag_delim_$tag_delim"} = 1; ( my $tz = $app->config('DefaultTimezone') ) =~ s![-\.]!_!g; $tz =~ s!_00$!!; $param{ 'server_offset_' . $tz } = 1; $param{default_site_root} = $app->config('DefaultSiteRoot'); $param{default_site_url} = $app->config('DefaultSiteURL'); $param{personal_weblog_readonly} = $app->config->is_readonly('NewUserAutoProvisioning'); $param{personal_weblog} = $app->config->NewUserAutoProvisioning ? 1 : 0; if ( my $id = $param{new_user_template_blog_id} = $app->config('NewUserTemplateBlogId') || '' ) { my $blog = MT::Blog->load($id); if ($blog) { $param{new_user_template_blog_name} = $blog->name; } else { $app->config( 'NewUserTemplateBlogId', undef, 1 ); $cfg->save_config(); delete $param{new_user_template_blog_id}; } } $param{system_email_address} = $cfg->EmailAddressMain; $param{saved} = $app->param('saved'); $param{error} = $app->param('error'); $param{screen_class} = "settings-screen system-general-settings"; $app->load_tmpl( 'cfg_system_general.tmpl', \%param ); } sub save_cfg_system_general { my $app = shift; $app->validate_magic or return; return $app->errtrans("Permission denied.") unless $app->user->is_superuser(); my $cfg = $app->config; $app->config( 'EmailAddressMain', $app->param('system_email_address') || undef, 1 ); $cfg->save_config(); my $args = (); $args->{saved} = 1; $app->redirect( $app->uri( 'mode' => 'cfg_system', args => $args ) ); } sub upgrade { my $app = shift; # check for an empty database... no author table would do it... my $driver = MT::Object->driver; my $upgrade_script = $app->config('UpgradeScript'); my $user_class = MT->model('author'); if ( !$driver || !$driver->table_exists($user_class) ) { return $app->redirect( $app->path . $upgrade_script . $app->uri_params( mode => 'install' ) ); } return $app->redirect( $app->path . $upgrade_script ); } sub recover_profile_password { my $app = shift; $app->validate_magic or return; return $app->errtrans("Permission denied.") unless $app->user->is_superuser(); my $q = $app->param; require MT::Auth; require MT::Log; if ( !MT::Auth->can_recover_password ) { $app->log( { message => $app->translate( "Invalid password recovery attempt; can't recover password in this configuration" ), level => MT::Log::SECURITY(), class => 'system', category => 'recover_profile_password', } ); return $app->error("Can't recover password in this configuration"); } my $author_id = $q->param('author_id'); my $author = MT::Author->load($author_id); return $app->error( $app->translate("Invalid author_id") ) if !$author || $author->type != MT::Author::AUTHOR() || !$author_id; my ( $rc, $res ) = reset_password( $app, $author, $author->hint, $author->name ); if ($rc) { my $url = $app->uri( 'mode' => 'view', args => { _type => 'author', recovered => 1, id => $author_id } ); $app->redirect($url); } else { $app->error($res); } } sub start_backup { my $app = shift; my $user = $app->user; my $blog_id = $app->param('blog_id'); my $perms = $app->permissions; unless ( $user->is_superuser ) { return $app->errtrans("Permission denied.") unless defined($blog_id) && $perms->can_administer_blog; } my %param = (); if ( defined($blog_id) ) { $param{blog_id} = $blog_id; $app->add_breadcrumb( $app->translate('Backup') ); } else { $app->add_breadcrumb( $app->translate('Backup & Restore') ); } $param{system_overview_nav} = 1 unless $blog_id; $param{nav_backup} = 1; require MT::Util::Archive; my @formats = MT::Util::Archive->available_formats(); $param{archive_formats} = \@formats; my $limit = $app->config('CGIMaxUpload') || 2048; $param{over_300} = 1 if $limit >= 300 * 1024; $param{over_500} = 1 if $limit >= 500 * 1024; $param{over_1024} = 1 if $limit >= 1024 * 1024; $param{over_2048} = 1 if $limit >= 2048 * 1024; my $tmp = $app->config('TempDir'); unless ( ( -d $tmp ) && ( -w $tmp ) ) { $param{error} = $app->translate( 'Temporary directory needs to be writable for backup to work correctly. Please check TempDir configuration directive.' ); } $app->load_tmpl( 'backup.tmpl', \%param ); } sub start_restore { my $app = shift; my $user = $app->user; my $blog_id = $app->param('blog_id'); my $perms = $app->permissions; unless ( $user->is_superuser ) { return $app->errtrans("Permission denied.") unless defined($blog_id) && $perms->can_administer_blog; } my %param = (); if ( defined($blog_id) ) { $param{blog_id} = $blog_id; $app->add_breadcrumb( $app->translate('Backup') ); } else { $app->add_breadcrumb( $app->translate('Backup & Restore') ); } $param{system_overview_nav} = 1 unless $blog_id; $param{nav_backup} = 1; eval "require XML::SAX"; $param{missing_sax} = 1 if $@; my $tmp = $app->config('TempDir'); unless ( ( -d $tmp ) && ( -w $tmp ) ) { $param{error} = $app->translate( 'Temporary directory needs to be writable for restore to work correctly. Please check TempDir configuration directive.' ); } $app->load_tmpl( 'restore.tmpl', \%param ); } sub backup { my $app = shift; my $user = $app->user; my $q = $app->param; my $blog_id = $q->param('blog_id'); my $perms = $app->permissions; unless ( $user->is_superuser ) { return $app->errtrans("Permission denied.") unless defined($blog_id) && $perms->can_administer_blog; } $app->validate_magic() or return; my $blog_ids = $q->param('backup_what'); my $size = $q->param('size_limit') || 0; return $app->errtrans( '[_1] is not a number.', encode_html($size) ) if $size !~ /^\d+$/; my @blog_ids = split ',', $blog_ids; my $archive = $q->param('backup_archive_format'); my $enc = $app->charset || 'utf-8'; my @ts = gmtime(time); my $ts = sprintf "%04d-%02d-%02d-%02d-%02d-%02d", $ts[5] + 1900, $ts[4] + 1, @ts[ 3, 2, 1, 0 ]; my $file = "Movable_Type-$ts" . '-Backup'; my $param = { return_args => '__mode=start_backup' }; $app->{no_print_body} = 1; $app->add_breadcrumb( $app->translate('Backup & Restore'), $app->uri( mode => 'start_backup' ) ); $app->add_breadcrumb( $app->translate('Backup') ); $param->{system_overview_nav} = 1 if defined($blog_ids) && $blog_ids; $param->{blog_id} = $blog_id if $blog_id; $param->{blog_ids} = $blog_ids if $blog_ids; $param->{nav_backup} = 1; local $| = 1; $app->send_http_header('text/html'); $app->print( $app->build_page( 'include/backup_start.tmpl', $param ) ); require File::Temp; require File::Spec; use File::Copy; my $temp_dir = $app->config('TempDir'); require MT::BackupRestore; my $count_term = $blog_id ? { class => '*', blog_id => [ 0, $blog_id ] } : { class => '*' }; my $num_assets = $app->model('asset')->count($count_term); my $printer; my $splitter; my $finisher; my $progress = sub { _progress($app, @_); }; my $fh; my $fname; my $arc_buf; if ( !( $size || $num_assets ) ) { $splitter = sub { }; if ( '0' eq $archive ) { ( $fh, my $filepath ) = File::Temp::tempfile( 'xml.XXXXXXXX', DIR => $temp_dir ); ( my $vol, my $dir, $fname ) = File::Spec->splitpath($filepath); $printer = sub { my ($data) = @_; print $fh $data; return length($data); }; $finisher = sub { my ($asset_files) = @_; close $fh; _backup_finisher( $app, $fname, $param ); }; } else { # archive/compress files $printer = sub { my ($data) = @_; $arc_buf .= $data; return length($data); }; $finisher = sub { require MT::Util::Archive; my ($asset_files) = @_; ( my $fh, my $filepath ) = File::Temp::tempfile( $archive . '.XXXXXXXX', DIR => $temp_dir ); ( my $vol, my $dir, $fname ) = File::Spec->splitpath($filepath); close $fh; unlink $filepath; my $arc = MT::Util::Archive->new($archive, $filepath); $arc->add_string( $arc_buf, "$file.xml" ); $arc->add_string( "", "$file.manifest"); $arc->close; _backup_finisher( $app, $fname, $param ); }; } } else { my @files; my $filename = File::Spec->catfile( $temp_dir, $file . "-1.xml" ); $fh = gensym(); open $fh, ">$filename"; my $url = $app->uri . "?__mode=backup_download&name=$file-1.xml&magic_token=" . $app->current_magic; $url .= "&blog_id=$blog_id" if defined($blog_id); push @files, { url => $url, filename => $file . "-1.xml" }; $printer = sub { my ($data) = @_; print $fh $data; return length($data); }; $splitter = sub { my ($findex) = @_; print $fh ''; close $fh; my $filename = File::Spec->catfile( $temp_dir, $file . "-$findex.xml" ); $fh = gensym(); open $fh, ">$filename"; my $url = $app->uri . "?__mode=backup_download&name=$file-$findex.xml&magic_token=" . $app->current_magic; $url .= "&blog_id=$blog_id" if defined($blog_id); push @files, { url => $url, filename => $file . "-$findex.xml" }; my $header .= "\n"; $header = "\n$header" if $enc !~ m/utf-?8/i; print $fh $header; }; $finisher = sub { my ($asset_files) = @_; close $fh; my $filename = File::Spec->catfile( $temp_dir, "$file.manifest" ); $fh = gensym(); open $fh, ">$filename"; print $fh "\n"; for my $file (@files) { my $name = $file->{filename}; print $fh "\n"; } for my $id ( keys %$asset_files ) { my $name = $id . '-' . $asset_files->{$id}->[2]; my $tmp = File::Spec->catfile( $temp_dir, $name ); unless ( copy( $asset_files->{$id}->[1], $tmp ) ) { $app->log( { message => $app->translate( 'Copying file [_1] to [_2] failed: [_3]', $asset_files->{$id}->[1], $tmp, $! ), level => MT::Log::INFO(), class => 'system', category => 'backup' } ); next; } print $fh "\n"; my $url = $app->uri . "?__mode=backup_download&assetname=$name&magic_token=" . $app->current_magic; $url .= "&blog_id=$blog_id" if defined($blog_id); push @files, { url => $url, filename => $name, }; } print $fh "\n"; close $fh; my $url = $app->uri . "?__mode=backup_download&name=$file.manifest&magic_token=" . $app->current_magic; $url .= "&blog_id=$blog_id" if defined($blog_id); push @files, { url => $url, filename => "$file.manifest" }; if ( '0' eq $archive ) { $param->{files_loop} = \@files; $param->{tempdir} = $temp_dir; my @fnames = map { $_->{filename} } @files; _backup_finisher( $app, \@fnames, $param ); } else { my ( $fh_arc, $filepath ) = File::Temp::tempfile( $archive . '.XXXXXXXX', DIR => $temp_dir ); ( my $vol, my $dir, $fname ) = File::Spec->splitpath($filepath); require MT::Util::Archive; close $fh_arc; unlink $filepath; my $arc = MT::Util::Archive->new($archive, $filepath); for my $f (@files) { $arc->add_file( $temp_dir, $f->{filename} ); } $arc->close; # for safery, don't unlink before closing $arc here. for my $f (@files) { unlink File::Spec->catfile( $temp_dir, $f->{filename} ); } _backup_finisher( $app, $fname, $param ); } }; } my @tsnow = gmtime(time); my $metadata = { backup_by => MT::Util::encode_xml($app->user->name, 1) . '(ID: ' . $app->user->id . ')', backup_on => sprintf( "%04d-%02d-%02dT%02d:%02d:%02d", $tsnow[5] + 1900, $tsnow[4] + 1, @tsnow[ 3, 2, 1, 0 ] ), backup_what => join( ',', @blog_ids ), schema_version => $app->config('SchemaVersion'), }; MT::BackupRestore->backup( \@blog_ids, $printer, $splitter, $finisher, $progress, $size * 1024, $enc, $metadata ); } sub backup_download { my $app = shift; my $user = $app->user; my $blog_id = $app->param('blog_id'); unless ( $user->is_superuser ) { my $perms = $app->permissions; return $app->errtrans("Permission denied.") unless defined($blog_id) && $perms->can_administer_blog; } $app->validate_magic() or return; my $filename = $app->param('filename'); my $assetname = $app->param('assetname'); my $temp_dir = $app->config('TempDir'); my $newfilename; if ( defined($assetname) ) { my $sess = MT::Session->load( { kind => 'BU', name => $assetname } ); if ( !defined($sess) || !$sess ) { return $app->errtrans("Specified file was not found."); } $newfilename = $filename = $assetname; $sess->remove; } elsif ( defined($filename) ) { my $sess = MT::Session->load( { kind => 'BU', name => $filename } ); if ( !defined($sess) || !$sess ) { return $app->errtrans("Specified file was not found."); } my @ts = gmtime( $sess->start ); my $ts = sprintf "%04d-%02d-%02d-%02d-%02d-%02d", $ts[5] + 1900, $ts[4] + 1, @ts[ 3, 2, 1, 0 ]; $newfilename = "Movable_Type-$ts" . '-Backup'; $sess->remove; } else { $newfilename = $app->param('name'); return if $newfilename !~ /Movable_Type-\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}-Backup(?:-\d+)?\.\w+/; $filename = $newfilename; } require File::Spec; my $fname = File::Spec->catfile( $temp_dir, $filename ); my $contenttype; if ( !defined($assetname) && ( $filename =~ m/^xml\..+$/i ) ) { my $enc = $app->charset || 'utf-8'; $contenttype = "text/xml; charset=$enc"; $newfilename .= '.xml'; } elsif ( $filename =~ m/^tgz\..+$/i ) { $contenttype = 'application/x-tar-gz'; $newfilename .= '.tar.gz'; } elsif ( $filename =~ m/^zip\..+$/i ) { $contenttype = 'application/zip'; $newfilename .= '.zip'; } else { $contenttype = 'application/octet-stream'; } if ( open( my $fh, "<", $fname ) ) { binmode $fh; $app->{no_print_body} = 1; $app->set_header( "Content-Disposition" => "attachment; filename=$newfilename" ); $app->send_http_header($contenttype); my $data; while ( read $fh, my ($chunk), 8192 ) { $data .= $chunk; } close $fh; $app->print($data); $app->log( { message => $app->translate( '[_1] successfully downloaded backup file ([_2])', $app->user->name, $fname ), level => MT::Log::INFO(), class => 'system', category => 'restore' } ); unlink $fname; } else { $app->errtrans('Specified file was not found.'); } } sub restore { my $app = shift; my $user = $app->user; return $app->errtrans("Permission denied.") if !$user->is_superuser; $app->validate_magic() or return; my $q = $app->param; my ($fh) = $app->upload_info('file'); my $uploaded = $q->param('file'); my ( $volume, $directories, $uploaded_filename ) = File::Spec->splitpath($uploaded) if defined($uploaded); if ( defined($uploaded_filename) && ( $uploaded_filename =~ /^.+\.manifest$/i ) ) { return restore_upload_manifest( $app, $fh ); } my $param = { return_args => '__mode=dashboard' }; $app->add_breadcrumb( $app->translate('Backup & Restore'), $app->uri( mode => 'start_restore' ) ); $app->add_breadcrumb( $app->translate('Restore') ); $param->{system_overview_nav} = 1; $param->{nav_backup} = 1; $app->{no_print_body} = 1; local $| = 1; $app->send_http_header('text/html'); $app->print( $app->build_page( 'restore_start.tmpl', $param ) ); require File::Path; my $error = ''; my $result; if (!$fh) { $param->{restore_upload} = 0; my $dir = $app->config('ImportPath'); my ( $blog_ids, $asset_ids ) = restore_directory( $app, $dir, \$error ); if ( defined $blog_ids ) { $param->{open_dialog} = 1; $param->{blog_ids} = join( ',', @$blog_ids ); $param->{asset_ids} = join( ',', @$asset_ids ) if defined $asset_ids; $param->{tmp_dir} = $dir; } elsif ( defined $asset_ids ) { my %asset_ids = @$asset_ids; my %error_assets; _restore_non_blog_asset( $app, $dir, $asset_ids, \%error_assets ); if (%error_assets) { my $data; while ( my ( $key, $value ) = each %error_assets ) { $data .= $app->translate( 'MT::Asset#[_1]: ', $key ) . $value . "\n"; } my $message = $app->translate( 'Some of the actual files for assets could not be restored.'); $app->log( { message => $message, level => MT::Log::WARNING(), class => 'system', category => 'restore', metadata => $data, } ); $error .= $message; } } } else { $param->{restore_upload} = 1; if ( $uploaded_filename =~ /^.+\.xml$/i ) { my $blog_ids = restore_file( $app, $fh, \$error ); if ( defined $blog_ids ) { $param->{open_dialog} = 1; $param->{blog_ids} = join( ',', @$blog_ids ); } } else { require MT::Util::Archive; my $arc; if ( $uploaded_filename =~ /^.+\.tar(\.gz)?$/i ) { $arc = MT::Util::Archive->new('tgz', $fh); } elsif ( $uploaded_filename =~ /^.+\.zip$/i ) { $arc = MT::Util::Archive->new('zip', $fh); } else { $error = $app->translate( 'Please use xml, tar.gz, zip, or manifest as a file extension.' ); } unless ($arc) { $result = 0; $param->{restore_success} = 0; if ($error) { $param->{error} = $error; } else { $error = MT->translate('Unknown file format'); $app->log( { message => $error . ":$uploaded_filename", level => MT::Log::WARNING(), class => 'system', category => 'restore', metadata => MT::Util::Archive->errstr, } ); } $app->print( $error ); $app->print( $app->build_page( "restore_end.tmpl", $param ) ); close $fh if $fh; return 1; } my $temp_dir = $app->config('TempDir'); require File::Temp; my $tmp = File::Temp::tempdir( $uploaded_filename . 'XXXX', DIR => $temp_dir ); $arc->extract($tmp); $arc->close; my ( $blog_ids, $asset_ids ) = restore_directory( $app, $tmp, \$error ); if (defined $blog_ids) { $param->{open_dialog} = 1; $param->{blog_ids} = join( ',', @$blog_ids ) if defined $blog_ids; $param->{asset_ids} = join( ',', @$asset_ids ) if defined $asset_ids; $param->{tmp_dir} = $tmp; } elsif ( defined $asset_ids ) { my %asset_ids = @$asset_ids; my %error_assets; _restore_non_blog_asset( $app, $tmp, \%asset_ids, \%error_assets ); if (%error_assets) { my $data; while ( my ( $key, $value ) = each %error_assets ) { $data .= $app->translate( 'MT::Asset#[_1]: ', $key ) . $value . "\n"; } my $message = $app->translate( 'Some of the actual files for assets could not be restored.'); $app->log( { message => $message, level => MT::Log::WARNING(), class => 'system', category => 'restore', metadata => $data, } ); $error .= $message; } } } } $param->{restore_success} = !$error; $param->{error} = $error if $error; if ( ( exists $param->{open_dialog} ) && ( $param->{open_dialog} ) ) { $param->{dialog_mode} = 'dialog_adjust_sitepath'; $param->{dialog_params} = 'magic_token=' . $app->current_magic . '&blog_ids=' . $param->{blog_ids} . '&asset_ids=' . $param->{asset_ids} . '&tmp_dir=' . encode_url( $param->{tmp_dir} ); if ( ( $param->{restore_upload} ) && ( $param->{restore_upload} ) ) { $param->{dialog_params} .= '&restore_upload=1'; } if ( ( $param->{error} ) && ( $param->{error} ) ) { $param->{dialog_params} .= '&error=' . encode_url( $param->{error} ); } } $app->print( $app->build_page( "restore_end.tmpl", $param ) ); close $fh if $fh; 1; } sub restore_premature_cancel { my $app = shift; my $user = $app->user; return $app->errtrans("Permission denied.") if !$user->is_superuser; $app->validate_magic() or return; require JSON; my $deferred = JSON::from_json( $app->param('deferred_json') ) if $app->param('deferred_json'); my $param = { restore_success => 1 }; if ( defined $deferred && ( scalar( keys %$deferred ) ) ) { _log_dirty_restore( $app, $deferred ); my $log_url = $app->uri( mode => 'view_log', args => {} ); $param->{restore_success} = 0; my $message = $app->translate( 'Some objects were not restored because their parent objects were not restored.' ); $param->{error} = $message . ' ' . $app->translate( "Detailed information is in the activity log.", $log_url ); } else { $app->log( { message => $app->translate( '[_1] has canceled the multiple files restore operation prematurely.', $app->user->name ), level => MT::Log::WARNING(), class => 'system', category => 'restore', } ); } $app->redirect( $app->uri( mode => 'view_log', args => {} ) ); } sub _restore_non_blog_asset { my ( $app, $tmp_dir, $asset_ids, $error_assets ) = @_; require MT::FileMgr; my $fmgr = MT::FileMgr->new('Local'); foreach my $new_id ( keys %$asset_ids ) { my $asset = $app->model('asset')->load($new_id); next unless $asset; my $old_id = $asset_ids->{$new_id}; my $filename = $old_id . '-' . $asset->file_name; my $file = File::Spec->catfile( $tmp_dir, $filename ); MT::BackupRestore->restore_asset( $file, $asset, $old_id, $fmgr, $error_assets, sub { $app->print(@_); } ); } } sub adjust_sitepath { my $app = shift; my $user = $app->user; return $app->errtrans("Permission denied.") if !$user->is_superuser; $app->validate_magic() or return; require MT::BackupRestore; my $q = $app->param; my $tmp_dir = $q->param('tmp_dir'); my $error = $q->param('error') || q(); my %asset_ids = split ',', $q->param('asset_ids'); $app->{no_print_body} = 1; local $| = 1; $app->send_http_header('text/html'); $app->print( $app->build_page( 'dialog/restore_start.tmpl', {} ) ); my $asset_class = $app->model('asset'); my %error_assets; my %blogs_meta; my @p = $q->param; foreach my $p (@p) { next unless $p =~ /^site_path_(\d+)/; my $id = $1; my $blog = $app->model('blog')->load($id) or return $app->error($app->translate('Can\'t load blog #[_1].', $id)); my $old_site_path = scalar $q->param("old_site_path_$id"); my $old_site_url = scalar $q->param("old_site_url_$id"); my $site_path = scalar $q->param("site_path_$id") || q(); my $site_url = scalar $q->param("site_url_$id") || q(); $blog->site_path($site_path); $blog->site_url($site_url); if ( $site_url || $site_path ) { $app->print( $app->translate( "Changing Site Path for the blog '[_1]' (ID:[_2])...", encode_html( $blog->name ), $blog->id ) ); } else { $app->print( $app->translate( "Removing Site Path for the blog '[_1]' (ID:[_2])...", encode_html( $blog->name ), $blog->id ) ); } my $old_archive_path = scalar $q->param("old_archive_path_$id"); my $old_archive_url = scalar $q->param("old_archive_url_$id"); my $archive_path = scalar $q->param("archive_path_$id") || q(); my $archive_url = scalar $q->param("archive_url_$id") || q(); $blog->archive_path($archive_path); $blog->archive_url($archive_url); if ( ( $old_archive_url && $archive_url ) || ( $old_archive_path && $archive_path ) ) { $app->print( "\n" . $app->translate( "Changing Archive Path for the blog '[_1]' (ID:[_2])...", encode_html( $blog->name ), $blog->id ) ); } elsif ( $old_archive_url || $old_archive_path ) { $app->print( "\n" . $app->translate( "Removing Archive Path for the blog '[_1]' (ID:[_2])...", encode_html( $blog->name ), $blog->id ) ); } $blog->save or $app->print( $app->translate("failed") . "\n" ), next; $app->print( $app->translate("ok") . "\n" ); $blogs_meta{$id} = { 'old_archive_path' => $old_archive_path, 'old_archive_url' => $old_archive_url, 'archive_path' => $archive_path, 'archive_url' => $archive_url, 'old_site_path' => $old_site_path, 'old_site_url' => $old_site_url, 'site_path' => $site_path, 'site_url' => $site_url, }; next unless %asset_ids; my $fmgr = ( $site_path || $archive_path ) ? $blog->file_mgr : undef; next unless defined $fmgr; my @assets = $asset_class->load( { blog_id => $id, class => '*' } ); foreach my $asset (@assets) { my $path = $asset->column('file_path'); my $url = $asset->column('url'); if ($archive_path) { $path =~ s/^\Q$old_archive_path\E/$archive_path/i; $asset->file_path($path); } if ($archive_url) { $url =~ s/^\Q$old_archive_url\E/$archive_url/i; $asset->url($url); } if ($site_path) { $path =~ s/^\Q$old_site_path\E/$site_path/i; $asset->file_path($path); } if ($site_url) { $url =~ s/^\Q$old_site_url\E/$site_url/i; $asset->url($url); } $app->print( $app->translate( "Changing file path for the asset '[_1]' (ID:[_2])...", encode_html( $asset->label ), $asset->id ) ); $asset->save or $app->print( $app->translate("failed") . "\n" ), next; $app->print( $app->translate("ok") . "\n" ); unless ( $q->param('redirect') ) { my $old_id = delete $asset_ids{ $asset->id }; my $filename = "$old_id-" . $asset->file_name; my $file = File::Spec->catfile( $tmp_dir, $filename ); MT::BackupRestore->restore_asset( $file, $asset, $old_id, $fmgr, \%error_assets, sub { $app->print(@_); } ); } } } unless ( $q->param('redirect') ) { _restore_non_blog_asset( $app, $tmp_dir, \%asset_ids, \%error_assets ); } if (%error_assets) { my $data; while ( my ( $key, $value ) = each %error_assets ) { $data .= $app->translate( 'MT::Asset#[_1]: ', $key ) . $value . "\n"; } my $message = $app->translate( 'Some of the actual files for assets could not be restored.'); $app->log( { message => $message, level => MT::Log::WARNING(), class => 'system', category => 'restore', metadata => $data, } ); $error .= $message; } if ($tmp_dir) { require File::Path; File::Path::rmtree($tmp_dir); } my $param = {}; if ( scalar $q->param('redirect') ) { $param->{restore_end} = 0; # redirect=1 means we are from multi-uploads $param->{blogs_meta} = MT::Util::to_json( \%blogs_meta ); $param->{next_mode} = 'dialog_restore_upload'; } else { $param->{restore_end} = 1; } if ($error) { $param->{error} = $error; $param->{error_url} = $app->uri( mode => 'view_log', args => {} ); } for my $key ( qw(files last redirect is_dirty is_asset objects_json deferred_json)) { $param->{$key} = scalar $q->param($key); } $param->{name} = $q->param('current_file'); $param->{assets} = encode_html( $q->param('assets') ); $app->print( $app->build_page( 'dialog/restore_end.tmpl', $param ) ); } sub dialog_restore_upload { my $app = shift; my $user = $app->user; return $app->errtrans("Permission denied.") if !$user->is_superuser; $app->validate_magic() or return; my $q = $app->param; my $current = $q->param('current_file'); my $last = $q->param('last'); my $files = $q->param('files'); my $assets_json = $q->param('assets'); my $is_asset = $q->param('is_asset') || 0; my $schema_version = $q->param('schema_version') || $app->config('SchemaVersion'); my $overwrite_template = $q->param('overwrite_templates') ? 1 : 0; my $objects = {}; my $deferred = {}; require JSON; my $objects_json = $q->param('objects_json') if $q->param('objects_json'); $deferred = JSON::from_json( $q->param('deferred_json') ) if $q->param('deferred_json'); my ($fh) = $app->upload_info('file'); my $param = {}; $param->{start} = $q->param('start') || 0; $param->{is_asset} = $is_asset; $param->{name} = $current; $param->{files} = $files; $param->{assets} = $assets_json; $param->{last} = $last; $param->{redirect} = 1; $param->{is_dirty} = $q->param('is_dirty'); $param->{objects_json} = $objects_json if defined($objects_json); $param->{deferred_json} = MT::Util::to_json($deferred) if defined($deferred); $param->{blogs_meta} = $q->param('blogs_meta'); $param->{schema_version} = $schema_version; $param->{overwrite_templates} = $overwrite_template; my $uploaded = $q->param('file'); if ( defined($uploaded) ) { $uploaded =~ s!\\!/!g; ## Change backslashes to forward slashes my ( $volume, $directories, $uploaded_filename ) = File::Spec->splitpath($uploaded); if ( $current ne $uploaded_filename ) { close $fh if $uploaded_filename; $param->{error} = $app->translate( 'Please upload [_1] in this page.', $current ); return $app->load_tmpl( 'dialog/restore_upload.tmpl', $param ); } } if (!$fh) { $param->{error} = $app->translate('File was not uploaded.') if !( $q->param('redirect') ); return $app->load_tmpl( 'dialog/restore_upload.tmpl', $param ); } $app->{no_print_body} = 1; local $| = 1; $app->send_http_header('text/html'); $app->print( $app->build_page( 'dialog/restore_start.tmpl', $param ) ); if ( defined $objects_json ) { my $objects_tmp = JSON::from_json($objects_json); my %class2ids; # { MT::CLASS#OLD_ID => NEW_ID } for my $key ( keys %$objects_tmp ) { my ( $class, $old_id ) = split '#', $key; if ( exists $class2ids{$class} ) { my $newids = $class2ids{$class}->{newids}; push @$newids, $objects_tmp->{$key}; my $keymaps = $class2ids{$class}->{keymaps}; push @$keymaps, { newid => $objects_tmp->{$key}, oldid => $old_id }; } else { $class2ids{$class} = { newids => [ $objects_tmp->{$key} ], keymaps => [ { newid => $objects_tmp->{$key}, oldid => $old_id } ] }; } } for my $class ( keys %class2ids ) { eval "require $class;"; next if $@; my $newids = $class2ids{$class}->{newids}; my $keymaps = $class2ids{$class}->{keymaps}; my @objs = $class->load( { id => $newids } ); for my $obj (@objs) { my @old_ids = grep { $_->{newid} eq $obj->id } @$keymaps; my $old_id = $old_ids[0]->{oldid}; $objects->{"$class#$old_id"} = $obj; } } } my $assets = JSON::from_json( decode_html($assets_json) ) if ( defined($assets_json) && $assets_json ); $assets = [] if !defined($assets); my $asset; my @errors; my $error_assets = {}; require MT::BackupRestore; my $blog_ids; my $asset_ids; if ($is_asset) { $asset = shift @$assets; $asset->{fh} = $fh; my $blogs_meta = JSON::from_json( $q->param('blogs_meta') || '{}' ); MT::BackupRestore->_restore_asset_multi( $asset, $objects, $error_assets, sub { $app->print(@_); }, $blogs_meta ); if ( defined( $error_assets->{ $asset->{asset_id} } ) ) { $app->log( { message => $app->translate('Restoring a file failed: ') . $error_assets->{ $asset->{asset_id} }, level => MT::Log::WARNING(), class => 'system', category => 'restore', } ); } } else { ( $blog_ids, $asset_ids ) = eval { MT::BackupRestore->restore_process_single_file( $fh, $objects, $deferred, \@errors, $schema_version, $overwrite_template, sub { _progress($app, @_) } ); }; if ($@) { $last = 1; } } my @files = split( ',', $files ); my $file_next = shift @files if scalar(@files); if ( !defined($file_next) ) { if ( scalar(@$assets) ) { $asset = $assets->[0]; $file_next = $asset->{asset_id} . '-' . $asset->{name}; $param->{is_asset} = 1; } } $param->{files} = join( ',', @files ); $param->{assets} = encode_html( MT::Util::to_json($assets) ); $param->{name} = $file_next; if ( 0 < scalar(@files) ) { $param->{last} = 0; } elsif ( 0 >= scalar(@$assets) - 1 ) { $param->{last} = 1; } else { $param->{last} = 0; } $param->{is_dirty} = scalar( keys %$deferred ); if ($last) { $param->{restore_end} = 1; if ( $param->{is_dirty} ) { _log_dirty_restore( $app, $deferred ); my $log_url = $app->uri( mode => 'view_log', args => {} ); $param->{error} = $app->translate( 'Some objects were not restored because their parent objects were not restored.' ); $param->{error_url} = $log_url; } elsif ( scalar( keys %$error_assets ) ) { $param->{error} = $app->translate('Some of the files were not restored correctly.'); my $log_url = $app->uri( mode => 'view_log', args => {} ); $param->{error_url} = $log_url; } elsif ( scalar @errors ) { $param->{error} = join '; ', @errors; my $log_url = $app->uri( mode => 'view_log', args => {} ); $param->{error_url} = $log_url; } else { $app->log( { message => $app->translate( "Successfully restored objects to Movable Type system by user '[_1]'", $app->user->name ), level => MT::Log::INFO(), class => 'system', category => 'restore' } ); $param->{ok_url} = $app->uri( mode => 'start_restore', args => {} ); } } else { my %objects_json; %objects_json = map { $_ => $objects->{$_}->id } keys %$objects; $param->{objects_json} = MT::Util::to_json( \%objects_json ); $param->{deferred_json} = MT::Util::to_json($deferred); $param->{error} = join( '; ', @errors ); if ( defined($blog_ids) && scalar(@$blog_ids) ) { $param->{next_mode} = 'dialog_adjust_sitepath'; $param->{blog_ids} = join( ',', @$blog_ids ); $param->{asset_ids} = join( ',', @$asset_ids ) if defined($asset_ids); } else { $param->{next_mode} = 'dialog_restore_upload'; } } MT->run_callbacks('restore', $objects, $deferred, \@errors, sub { _progress( $app, @_ ) }); $app->print( $app->build_page( 'dialog/restore_end.tmpl', $param ) ); close $fh if $fh; } sub dialog_adjust_sitepath { my $app = shift; my $user = $app->user; return $app->errtrans("Permission denied.") if !$user->is_superuser; $app->validate_magic() or return; my $q = $app->param; my $tmp_dir = $q->param('tmp_dir'); my $error = $q->param('error') || q(); my $uploaded = $q->param('restore_upload') || 0; my @blog_ids = split ',', $q->param('blog_ids'); my $asset_ids = $q->param('asset_ids'); my @blogs = $app->model('blog')->load( { id => \@blog_ids } ); my @blogs_loop; foreach my $blog (@blogs) { push @blogs_loop, { name => $blog->name, id => $blog->id, old_site_path => $blog->site_path, old_site_url => $blog->site_url, $blog->column('archive_path') ? ( old_archive_path => $blog->archive_path ) : (), $blog->column('archive_url') ? ( old_archive_url => $blog->archive_url ) : (), }; } my $param = { blogs_loop => \@blogs_loop, tmp_dir => $tmp_dir }; $param->{error} = $error if $error; $param->{restore_upload} = $uploaded if $uploaded; $param->{asset_ids} = $asset_ids if $asset_ids; for my $key ( qw(files assets last redirect is_dirty is_asset objects_json deferred_json) ) { $param->{$key} = $q->param($key) if $q->param($key); } $param->{name} = $q->param('current_file'); $param->{screen_id} = "adjust-sitepath"; $app->load_tmpl( 'dialog/adjust_sitepath.tmpl', $param ); } sub convert_to_html { my $app = shift; my $format = $app->param('format') || ''; my @formats = split /\s*,\s*/, $format; my $text = $app->param('text') || ''; my $text_more = $app->param('text_more') || ''; my $result = { text => $app->apply_text_filters( $text, \@formats ), text_more => $app->apply_text_filters( $text_more, \@formats ), format => $formats[0], }; return $app->json_result($result); } sub update_list_prefs { my $app = shift; my $prefs = $app->list_pref( $app->param('_type') ); $app->call_return; } sub recover_passwords { my $app = shift; my @id = $app->param('id'); return $app->errtrans("Permission denied.") unless $app->user->is_superuser(); my $class = ref $app eq 'MT::App::Upgrader' ? 'MT::BasicAuthor' : $app->model('author'); eval "use $class;"; my @msg_loop; foreach (@id) { my $author = $class->load($_) or next; my ( $rc, $res ) = reset_password( $app, $author, $author->hint ); push @msg_loop, { message => $res }; } $app->load_tmpl( 'recover_password_result.tmpl', { message_loop => \@msg_loop, return_url => $app->return_uri } ); } sub reset_password { my $app = shift; my ($author) = $_[0]; my $hint = $_[1]; my $name = $_[2]; require MT::Auth; require MT::Log; if ( !MT::Auth->can_recover_password ) { $app->log( { message => $app->translate( "Invalid password recovery attempt; can't recover password in this configuration" ), level => MT::Log::SECURITY(), class => 'system', category => 'recover_password', } ); return ( 0, $app->translate("Can't recover password in this configuration") ); } $app->log( { message => $app->translate( "Invalid user name '[_1]' in password recovery attempt", $name ), level => MT::Log::SECURITY(), class => 'system', category => 'recover_password', } ), return ( 0, $app->translate("User name or password hint is incorrect.") ) unless $author; return ( 0, $app->translate("User has not set pasword hint; cannot recover password") ) if ( $hint && !$author->hint ); $app->log( { message => $app->translate( "Invalid attempt to recover password (used hint '[_1]')", $hint ), level => MT::Log::SECURITY(), class => 'system', category => 'recover_password' } ), return ( 0, $app->translate("User name or password hint is incorrect.") ) unless $author->hint eq $hint; return ( 0, $app->translate("User does not have email address") ) unless $author->email; # Generate Token require MT::Util::Captcha; my $salt = MT::Util::Captcha->_generate_code(8); my $expires = time + ( 60 * 60 ); my $token = MT::Util::perl_sha1_digest_hex( $salt . $expires . $app->config->SecretToken ); $author->password_reset($salt); $author->password_reset_expires($expires); $author->password_reset_return_to(undef); $author->save; my $message = $app->translate( "A password reset link has been sent to [_3] for user '[_1]' (user #[_2]).", $author->name, $author->id, $author->email ); $app->log( { message => $message, level => MT::Log::SECURITY(), class => 'system', category => 'recover_password' } ); # Send mail to user my $email = $author->email; my %head = ( id => 'recover_password', To => $email, From => $app->config('EmailAddressMain') || $email, Subject => $app->translate("Password Recovery") ); my $charset = $app->charset; my $mail_enc = uc( $app->config('MailEncoding') || $charset ); $head{'Content-Type'} = qq(text/plain; charset="$mail_enc"); my $body = $app->build_email( 'recover-password', { link_to_login => $app->base . $app->uri . "?__mode=new_pw&token=$token&email=" . encode_url($email), } ); require MT::Mail; MT::Mail->send( \%head, $body ) or return $app->error( $app->translate( "Error sending mail ([_1]); please fix the problem, then " . "try again to recover your password.", MT::Mail->errstr ) ); ( 1, $message ); } sub restore_file { my $app = shift; my ( $fh, $errormsg ) = @_; my $q = $app->param; my $schema_version = $app->config->SchemaVersion; #my $schema_version = # $q->param('ignore_schema_conflict') # ? 'ignore' # : $app->config('SchemaVersion'); my $overwrite_template = $q->param('overwrite_global_templates') ? 1 : 0; require MT::BackupRestore; my ( $deferred, $blogs ) = MT::BackupRestore->restore_file( $fh, $errormsg, $schema_version, $overwrite_template, sub { _progress( $app, @_ ); } ); if ( !defined($deferred) || scalar( keys %$deferred ) ) { _log_dirty_restore( $app, $deferred ); my $log_url = $app->uri( mode => 'view_log', args => {} ); $$errormsg .= '; ' if $$errormsg; $$errormsg .= $app->translate( 'Some objects were not restored because their parent objects were not restored. Detailed information is in the activity log.', $log_url ); return $blogs; } if ($$errormsg) { $app->log( { message => $$errormsg, level => MT::Log::ERROR(), class => 'system', category => 'restore', } ); return $blogs; } $app->log( { message => $app->translate( "Successfully restored objects to Movable Type system by user '[_1]'", $app->user->name ), level => MT::Log::INFO(), class => 'system', category => 'restore' } ); $blogs; } sub restore_directory { my $app = shift; my ( $dir, $error ) = @_; if ( !-d $dir ) { $$error = $app->translate( '[_1] is not a directory.', $dir ); return ( undef, undef ); } my $q = $app->param; my $schema_version = $app->config->SchemaVersion; #my $schema_version = # $q->param('ignore_schema_conflict') # ? 'ignore' # : $app->config('SchemaVersion'); my $overwrite_template = $q->param('overwrite_global_templates') ? 1 : 0; my @errors; my %error_assets; require MT::BackupRestore; my ( $deferred, $blogs, $assets ) = MT::BackupRestore->restore_directory( $dir, \@errors, \%error_assets, $schema_version, $overwrite_template, sub { _progress( $app, @_ ); } ); if ( scalar @errors ) { $$error = $app->translate('Error occured during restore process.'); $app->log( { message => $$error, level => MT::Log::WARNING(), class => 'system', category => 'restore', metadata => join( '; ', @errors ), } ); } return ( $blogs, $assets ) unless ( defined($deferred) && %$deferred ); if ( scalar( keys %error_assets ) ) { my $data; while ( my ( $key, $value ) = each %error_assets ) { $data .= $app->translate( 'MT::Asset#[_1]: ', $key ) . $value . "\n"; } my $message = $app->translate('Some of files could not be restored.'); $app->log( { message => $message, level => MT::Log::WARNING(), class => 'system', category => 'restore', metadata => $data, } ); $$error .= $message; } if ( scalar( keys %$deferred ) ) { _log_dirty_restore( $app, $deferred ); my $log_url = $app->uri( mode => 'view_log', args => {} ); $$error = $app->translate( 'Some objects were not restored because their parent objects were not restored. Detailed information is in the activity log.', $log_url ); return ( $blogs, $assets ); } return ( $blogs, $assets ) if $$error; $app->log( { message => $app->translate( "Successfully restored objects to Movable Type system by user '[_1]'", $app->user->name ), level => MT::Log::INFO(), class => 'system', category => 'restore' } ); return ( $blogs, $assets ); } sub restore_upload_manifest { my $app = shift; my ($fh) = @_; my $user = $app->user; return $app->errtrans("Permission denied.") if !$user->is_superuser; $app->validate_magic() or return; my $q = $app->param; require MT::BackupRestore; my $backups = MT::BackupRestore->process_manifest($fh); return $app->errtrans( "Uploaded file was not a valid Movable Type backup manifest file.") if !defined($backups); my $files = $backups->{files}; my $assets = $backups->{assets}; my $file_next = shift @$files if defined($files) && scalar(@$files); my $assets_json; my $param = {}; if ( !defined($file_next) ) { if ( scalar(@$assets) > 0 ) { my $asset = shift @$assets; $file_next = $asset->{name}; $param->{is_asset} = 1; } } $assets_json = encode_url( MT::Util::to_json($assets) ) if scalar(@$assets) > 0; $param->{files} = join( ',', @$files ); $param->{assets} = $assets_json; $param->{filename} = $file_next; $param->{last} = scalar(@$files) ? 0 : ( scalar(@$assets) ? 0 : 1 ); $param->{open_dialog} = 1; $param->{schema_version} = $app->config->SchemaVersion; #$param->{schema_version} = # $q->param('ignore_schema_conflict') # ? 'ignore' # : $app->config('SchemaVersion'); $param->{overwrite_templates} = $q->param('overwrite_global_templates') ? 1 : 0; $param->{dialog_mode} = 'dialog_restore_upload'; $param->{dialog_params} = 'start=1' . '&magic_token=' . $app->current_magic . '&files=' . $param->{files} . '&assets=' . $param->{assets} . '&current_file=' . $param->{filename} . '&last=' . $param->{'last'} . '&schema_version=' . $param->{schema_version} . '&overwrite_templates=' . $param->{overwrite_templates} . '&redirect=1'; $app->load_tmpl( 'restore.tmpl', $param ); #close $fh if $fh; } sub _backup_finisher { my $app = shift; my ( $fnames, $param ) = @_; unless ( ref $fnames ) { $fnames = [$fnames]; } $param->{filename} = $fnames->[0]; $param->{backup_success} = 1; require MT::Session; MT::Session->remove( { kind => 'BU' } ); foreach my $fname (@$fnames) { my $sess = MT::Session->new; $sess->id( $app->make_magic_token() ); $sess->kind('BU'); # BU == Backup $sess->name($fname); $sess->start(time); $sess->save; } my $message; if ( my $blog_id = $param->{blog_id} || $param->{blog_ids} ) { $message = $app->translate( "Blog(s) (ID:[_1]) was/were successfully backed up by user '[_2]'", $blog_id, $app->user->name ); } else { $message = $app->translate( "Movable Type system was successfully backed up by user '[_1]'", $app->user->name ); } $app->log( { message => $message, level => MT::Log::INFO(), class => 'system', category => 'restore' } ); $app->print( $app->build_page( 'include/backup_end.tmpl', $param ) ); } sub _progress { my $app = shift; my $ids = $app->request('progress_ids') || {}; my ( $str, $id ) = @_; if ( $id && $ids->{$id} ) { my $str_js = encode_js($str); $app->print( qq{} ); } elsif ($id) { $ids->{$id} = 1; $app->print(qq{\n$str}); } else { $app->print("$str"); } $app->request( 'progress_ids', $ids ); } sub _log_dirty_restore { my $app = shift; my ($deferred) = @_; my %deferred_by_class; for my $key ( keys %$deferred ) { my ( $class, $id ) = split( '#', $key ); if ( exists $deferred_by_class{$class} ) { push @{ $deferred_by_class{$class} }, $id; } else { $deferred_by_class{$class} = [$id]; } } while ( my ( $class_name, $ids ) = each %deferred_by_class ) { my $message = $app->translate( 'Some [_1] were not restored because their parent objects were not restored.', $class_name ); $app->log( { message => $message, level => MT::Log::WARNING(), class => 'system', category => 'restore', metadata => join( ', ', @$ids ), } ); } 1; } 1;