# Copyright 2001-2003 Six Apart. This code cannot be redistributed without
# permission from www.movabletype.org.
#
# $Id: CMS.pm,v 1.281 2003/02/17 06:09:47 btrott Exp $
package MT::App::CMS;
use strict;
use Symbol;
use File::Spec;
use MT::Util qw( encode_html format_ts offset_time_list
remove_html get_entry );
use MT::App;
@MT::App::CMS::ISA = qw( MT::App );
sub init {
my $app = shift;
$app->SUPER::init(@_) or return;
$app->add_methods(
'menu' => \&show_menu,
'save' => \&save_object,
'view' => \&edit_object,
'list' => \&list_objects,
'list_entries' => \&list_entries,
'save_entries' => \&save_entries,
'save_entry' => \&save_entry,
'cfg_archives' => \&cfg_archives,
'cfg_prefs' => \&cfg_prefs,
'cfg_archives_save' => \&cfg_archives_save,
'cfg_archives_add' => \&cfg_archives_add,
'cfg_archives_do_add' => \&cfg_archives_do_add,
'list_blogs' => \&list_blogs,
'list_cat' => \&list_categories,
'save_cat' => \&save_categories,
'edit_placements' => \&edit_placements,
'save_placements' => \&save_placements,
'delete_confirm' => \&delete_confirm,
'delete' => \&delete,
'edit_permissions' => \&edit_permissions,
'save_permissions' => \&save_permissions,
'ping' => \&send_pings,
'rebuild' => \&rebuild_pages,
'start_rebuild' => \&start_rebuild_pages,
'rebuild_confirm' => \&rebuild_confirm,
'send_notify' => \&send_notify,
'start_upload' => \&start_upload,
'upload_file' => \&upload_file,
'start_upload_entry' => \&start_upload_entry,
'show_upload_html' => \&show_upload_html,
'logout' => \&logout,
'start_recover' => \&start_recover,
'recover' => \&recover_password,
'bookmarklets' => \&bookmarklets,
'make_bm_link' => \&make_bm_link,
'view_log' => \&view_log,
'reset_log' => \&reset_log,
'start_import' => \&start_import,
'search_replace' => \&search_replace,
'start_search_replace' => \&start_search_replace,
'export' => \&export,
'import' => \&do_import,
'pinged_urls' => \&pinged_urls,
'tb_cat_pings' => \&tb_cat_pings,
'show_entry_prefs' => \&show_entry_prefs,
'save_entry_prefs' => \&save_entry_prefs,
'reg_file' => \®_file,
'category_add' => \&category_add,
'category_do_add' => \&category_do_add,
'cc_return' => \&cc_return,
);
$app->{default_mode} = 'list_blogs';
$app->{template_dir} = 'cms';
my $mode = $app->{query}->param('__mode');
$app->{requires_login} =
$mode && ($mode eq 'start_recover' || $mode eq 'recover') ?
0 : 1;
$app->{is_admin} = 1;
$app->{user_class} = 'MT::Author';
$app;
}
sub pre_run {
my $app = shift;
my $auth = $app->{author};
$app->set_language($auth->preferred_language)
if $auth && $auth->preferred_language;
1;
}
sub logout {
my $app = shift;
$app->log("User '" . $app->{author}->name . "' logged out");
delete $app->{author};
$app->bake_cookie(-name => 'user', -value => '', -expires => '-1y');
$app->build_page('login.tmpl', { logged_out => 1 });
}
sub start_recover {
my $app = shift;
$app->build_page('recover.tmpl');
}
sub recover_password {
my $app = shift;
my $q = $app->{query};
require MT::Author;
my $name = $q->param('name');
my $author = MT::Author->load({ name => $name });
$app->log("Invalid author name '$name' in password recovery attempt"),
return $app->error($app->translate(
"No such author with name '[_1]'", $name)) unless $author;
return $app->error($app->translate(
"Author has not set birthplace; cannot recover password"))
unless $author->hint;
my $hint = $q->param('hint');
$app->log("Invalid attempt to recover password (used birthplace '$hint')"),
return $app->error($app->translate(
"Birthplace '[_1]' does not match stored birthplace " .
"for this author", $hint)) unless $author->hint eq $hint;
return $app->error($app->translate("Author does not have email address"))
unless $author->email;
my @pool = ('a'..'z', 0..9);
my $pass;
for (1..8) { $pass .= $pool[ rand @pool ] }
$author->set_password($pass);
$author->save;
my %head = ( To => $author->email, From => $author->email,
Subject => "Password Recovery" );
my $body = $app->translate('_USAGE_FORGOT_PASSWORD_1') .
"\n\n $pass\n\n" .
$app->translate('_USAGE_FORGOT_PASSWORD_2') . "\n";
require Text::Wrap;
$Text::Wrap::columns = 72;
$body = Text::Wrap::wrap('', '', $body);
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));
$app->build_page('recover.tmpl', { recovered => 1,
email => $author->email });
}
sub is_authorized {
my $app = shift;
require MT::Permission;
my $blog_id = $app->{query}->param('blog_id');
return 1 unless $blog_id;
return unless my $author = $app->{author};
my $perms = $app->{perms} = MT::Permission->load({
author_id => $author->id,
blog_id => $blog_id });
$perms ? 1 :
$app->error($app->translate(
"You are not authorized to log in to this blog."));
}
sub build_page {
my $app = shift;
my($page, $param) = @_;
if (my $perms = $app->{perms}) {
$param->{can_post} = $perms->can_post;
$param->{can_upload} = $perms->can_upload;
$param->{can_edit_entries} =
$perms->can_post || $perms->can_edit_all_posts;
$param->{can_search_replace} = $perms->can_edit_all_posts;
$param->{can_edit_templates} = $perms->can_edit_templates;
$param->{can_edit_authors} = $perms->can_edit_authors;
$param->{can_edit_config} = $perms->can_edit_config;
$param->{can_rebuild} = $perms->can_rebuild;
$param->{can_edit_categories} = $perms->can_edit_categories;
$param->{can_edit_notifications} = $perms->can_edit_notifications;
$param->{has_manage_label} =
$perms->can_edit_templates || $perms->can_edit_authors ||
$perms->can_edit_categories || $perms->can_edit_config;
}
my $blog_id = $app->{query}->param('blog_id');
if (my $auth = $app->{author}) {
$param->{author_id} = $auth->id;
$param->{author_name} = $auth->name;
my @perms = MT::Permission->load({ author_id => $auth->id });
my @data;
for my $perms (@perms) {
next unless $perms->role_mask;
my $blog = MT::Blog->load($perms->blog_id);
push @data, { top_blog_id => $blog->id,
top_blog_name => $blog->name };
$data[-1]{top_blog_selected} = 1
if $blog_id && $blog->id == $blog_id;
}
@data = sort { $a->{top_blog_name} cmp $b->{top_blog_name} } @data;
$param->{top_blog_loop} = \@data;
}
if ($blog_id) {
my $blog = MT::Blog->load($blog_id);
$param->{blog_name} = $blog->name;
$param->{blog_id} = $blog->id;
$param->{blog_url} = $blog->site_url;
}
if ($app->{query}->param('is_bm')) {
$param->{is_bookmarklet} = 1;
if ($page eq 'login.tmpl') {
## For bookmarklets, we need to pass-thru text, title, and
## href vars
my $q = $app->{query};
$param->{text} = $q->param('text');
$param->{link_title} = $q->param('link_title');
$param->{link_href} = $q->param('link_href');
$param->{bm_show} = $q->param('bm_show');
}
}
$param->{agent_mozilla} = $ENV{HTTP_USER_AGENT} =~ /gecko/i;
$param->{have_tangent} = eval { require MT::Tangent; 1 } ? 1 : 0;
$app->SUPER::build_page($page, $param);
}
## Application methods
sub show_menu {
my $app = shift;
my $perms = $app->{perms}
or return $app->error($app->translate("No permissions"));
require MT::Comment;
require MT::TBPing;
require MT::Trackback;
require MT::Permission;
require MT::Entry;
my $blog_id = $app->{query}->param('blog_id');
my $iter = MT::Entry->load_iter({ blog_id => $blog_id },
{ 'sort' => 'created_on',
direction => 'descend',
limit => 5 });
my @e_data;
my $i = 1;
my $author_id = $app->{author}->id;
while (my $entry = $iter->()) {
my $row = { entry_id => $entry->id,
entry_blog_id => $entry->blog_id, };
unless ($row->{entry_title} = $entry->title) {
my $title = remove_html($entry->text);
$row->{entry_title} = substr($title, 0, 22) . '...';
}
$row->{entry_title} = encode_html($row->{entry_title}, 1);
$row->{entry_created_on} = format_ts("%Y.%m.%d", $entry->created_on);
$row->{is_odd} = $i++ % 2;
$row->{has_edit_access} = $perms->can_edit_all_posts ||
$entry->author_id == $author_id;
push @e_data, $row;
}
$iter = MT::Comment->load_iter({ blog_id => $blog_id },
{ 'sort' => 'created_on',
direction => 'descend',
limit => 5 });
my @c_data;
$i = 1;
while (my $comment = $iter->()) {
my $row = { comment_id => $comment->id,
comment_author => $comment->author || '[No author]',
comment_blog_id => $comment->blog_id, };
$row->{comment_created_on} = format_ts("%Y.%m.%d",
$comment->created_on);
my $entry = MT::Entry->load($comment->entry_id);
$row->{has_edit_access} = $perms->can_edit_all_posts ||
$entry->author_id == $author_id;
$row->{is_odd} = $i++ % 2;
push @c_data, $row;
}
$iter = MT::TBPing->load_iter({ blog_id => $blog_id },
{ 'sort' => 'created_on',
direction => 'descend',
limit => 5 });
my @p_data;
$i = 1;
while (my $ping = $iter->()) {
my $row = { ping_id => $ping->id,
ping_title => $ping->title || '[No title]',
ping_url => $ping->source_url,
ping_blog_id => $ping->blog_id, };
$row->{ping_created_on} = format_ts("%Y.%m.%d", $ping->created_on);
#my $tb = MT::Trackback->load($ping->tb_id);
#my $entry = MT::Entry->load($tb->entry_id);
#$row->{has_edit_access} = $perms->can_edit_all_posts ||
# $entry->author_id == $author_id;
#$row->{ping_entry_id} = $entry->id;
$row->{is_odd} = $i++ % 2;
push @p_data, $row;
}
require MT::Blog;
my $blog = MT::Blog->load($blog_id);
my %param = (entry_loop => \@e_data, comment_loop => \@c_data,
ping_loop => \@p_data);
$param{blog_description} = $blog->description;
$param{welcome} = $blog->welcome_msg;
$param{num_entries} = MT::Entry->count({ blog_id => $blog_id });
$param{num_comments} = MT::Comment->count({ blog_id => $blog_id });
$param{num_authors} = 0;
$iter = MT::Permission->load_iter({ blog_id => $blog_id });
while (my $p = $iter->()) {
$param{num_authors}++ if $p->role_mask > 0;
}
$app->build_page('menu.tmpl', \%param);
}
sub bookmarklets { $_[0]->build_page('bookmarklets.tmpl') }
sub make_bm_link {
my $app = shift;
my %param = ( have_link => 1 );
my @show = $app->{query}->param('show');
my $height = 440;
my %show = map { $_ => 1 } @show;
$height += 50 if $show{trackback};
$height += 40 if $show{allow_comments};
$height += 20 if $show{allow_pings};
$height += 40 if $show{convert_breaks};
$height += 50 if $show{category};
$height += 80 if $show{excerpt};
$height += 80 if $show{keywords};
$height += 80 if $show{text_more};
$param{bm_show} = join ',', @show;
$param{bm_height} = $height;
$param{bm_js} = _bm_js($app->base . $app->uri, $param{bm_show}, $height);
$app->build_page('bookmarklets.tmpl', \%param);
}
sub _bm_js {
my($uri, $show, $height) = @_;
qq!javascript:d=document;t=d.selection?d.selection.createRange().text:d.getSelection();void(window.open('$uri?is_bm=1&bm_show=$show&__mode=view&_type=entry&link_title='+escape(d.title)+'&link_href='+escape(d.location.href)+'&text='+escape(t),'_blank','scrollbars=yes,width=400,height=$height,status=yes,resizable=yes,scrollbars=yes'))!;
}
sub start_import {
my $app = shift;
my $blog_id = $app->{query}->param('blog_id');
my %param;
require MT::Category;
my $iter = MT::Category->load_iter({ blog_id => $blog_id });
my @data;
while (my $cat = $iter->()) {
push @data, { category_id => $cat->id,
category_label => $cat->label };
}
@data = sort { $a->{category_label} cmp $b->{category_label} }
@data;
$param{category_loop} = \@data;
$app->build_page('import.tmpl', \%param);
}
sub view_log {
my $app = shift;
my $author = $app->{author};
return $app->error($app->translate("Permission denied."))
unless $author->can_view_log;
my @log;
require MT::Log;
my $iter = MT::Log->load_iter(undef, { 'sort' => 'created_on' });
my $i = 1;
while (my $log = $iter->()) {
my $row = { log_message => $log->message, log_ip => $log->ip };
$row->{created_on_formatted} = format_ts("%Y.%m.%d %H:%M:%S",
$log->created_on);
$row->{is_odd} = $i++ % 2 ? 1 : 0;
push @log, $row;
}
$log[-1]{is_last} = 1 if @log;
my %param = ( log_entry_loop => \@log );
$param{'reset'} = $app->{query}->param('reset');
$app->build_page('view_log.tmpl', \%param);
}
sub reset_log {
my $app = shift;
my $author = $app->{author};
return $app->error($app->translate("Permission denied."))
unless $author->can_view_log;
require MT::Log;
MT::Log->remove_all;
$app->redirect($app->uri . '?__mode=view_log&reset=1');
}
{
my %API = (
author => 'MT::Author',
comment => 'MT::Comment',
entry => 'MT::Entry',
template => 'MT::Template',
blog => 'MT::Blog',
notification => 'MT::Notification',
templatemap => 'MT::TemplateMap',
category => 'MT::Category',
banlist => 'MT::IPBanList',
ping => 'MT::TBPing',
ping_cat => 'MT::TBPing',
);
sub edit_object {
my $app = shift;
my %param = $_[0] ? %{ $_[0] } : ();
my $q = $app->{query};
my $type = $q->param('_type');
my $blog_id = $q->param('blog_id');
my $id = $q->param('id');
my $perms = $app->{perms};
return $app->error($app->translate("No permissions"))
if !$perms && $id && $type ne 'author';
if ($id &&
($type eq 'blog' && !$perms->can_edit_config) ||
($type eq 'template' && !$perms->can_edit_templates)) {
return $app->error($app->translate("Permission denied."));
}
if ($type eq 'blog' && !$id && !$app->{author}->can_create_blog) {
return $app->error($app->translate("Permission denied."));
}
if ($type eq 'entry' && !$id && !$q->param('is_bm')) {
return $app->error($app->translate("Permission denied."))
unless $perms->can_post;
}
if ($type eq 'author' && $app->{author}->id != $id) {
return $app->error($app->translate("Permission denied."));
}
my $class = $app->_load_driver_for($type) or return;
my $cols = $class->column_names;
my $obj;
if ($id) {
$obj = $class->load($id) or
return $app->error($app->translate("Load failed: [_1]",
$class->errstr));
if ($type eq 'entry') {
return $app->error($app->translate("Permission denied."))
unless $perms->can_edit_entry($obj, $app->{author});
}
for my $col (@$cols) {
$param{$col} = defined $q->param($col) ?
$q->param($col) : $obj->$col();
$param{$col} = encode_html($param{$col}, 1);
}
if ($type eq 'entry') {
## Don't pass in author_id, because it will clash with the
## author_id parameter of the author currently logged in.
delete $param{'author_id'};
delete $param{'category_id'};
if (my $cat = $obj->category) {
$param{category_id} = $cat->id;
}
$blog_id = $obj->blog_id;
my $status = $q->param('status') || $obj->status;
$param{"status_" . MT::Entry::status_text($status)} = 1;
$param{"allow_comments_" . ($q->param('allow_comments') || $obj->allow_comments)} = 1;
my $df = $q->param('created_on_manual') ||
format_ts("%Y-%m-%d %H:%M:%S", $obj->created_on);
$param{created_on_formatted} = $df;
my $comments = $obj->comments;
my @c_data;
my $i = 1;
@$comments = sort { $b->created_on cmp $a->created_on }
@$comments;
for my $c (@$comments) {
my $df = format_ts("%Y-%m-%d %H:%M:%S", $c->created_on);
my $author = $c->author || '[No author]';
push @c_data, { comment_id => $c->id,
comment_author => $author,
comment_created => $df,
comment_odd => ($i++ % 2 ? 1 : 0) };
}
$param{comment_loop} = \@c_data;
$param{num_comment_rows} = @c_data + 3;
$param{can_send_notifications} = $perms->can_send_notifications;
## Load list of trackback pings sent for this entry.
require MT::Trackback;
require MT::TBPing;
my $tb = MT::Trackback->load({ entry_id => $obj->id });
my @tb_data;
if ($tb) {
my $iter = MT::TBPing->load_iter({ tb_id => $tb->id },
{ 'sort' => 'created_on',
direction => 'ascend' });
$i = 1;
while (my $ping = $iter->()) {
my $df = format_ts("%Y-%m-%d %H:%M:%S", $ping->created_on);
push @tb_data, { ping_id => $ping->id,
ping_title => $ping->title,
ping_url => $ping->source_url,
ping_created => $df,
ping_odd => ($i++ % 2 ? 1 : 0) };
}
}
$param{ping_loop} = \@tb_data;
$param{num_ping_rows} = @tb_data + 3;
## Load next and previous entries for next/previous links
if (my $next = $obj->next) {
$param{next_entry_id} = $next->id;
}
if (my $prev = $obj->previous) {
$param{previous_entry_id} = $prev->id;
}
$param{ping_errors} = $q->param('ping_errors');
$param{can_view_log} = $app->{author}->can_view_log;
} elsif ($type eq 'category') {
require MT::Trackback;
my $tb = MT::Trackback->load({ category_id => $obj->id });
if ($tb) {
my $path = $app->{cfg}->CGIPath;
$path .= '/' unless $path =~ m!/$!;
my $script = $app->{cfg}->TrackbackScript;
$param{tb_url} = $path . $script . '/' . $tb->id;
if ($param{tb_passphrase} = $tb->passphrase) {
$param{tb_url} .= '/' .
MT::Util::encode_url($param{tb_passphrase});
}
}
} elsif ($type eq 'template') {
$blog_id = $obj->blog_id;
$param{has_name} = $obj->type eq 'index' ||
$obj->type eq 'custom' ||
$obj->type eq 'archive' ||
$obj->type eq 'category' ||
$obj->type eq 'individual';
$param{has_outfile} = $obj->type eq 'index';
$param{has_rebuild} = $obj->type eq 'index';
$param{rebuild_me} = defined $obj->rebuild_me ?
$obj->rebuild_me : 1;
} elsif ($type eq 'blog') {
$blog_id = $obj->id;
my $at = $obj->archive_type;
if ($at && $at ne 'None') {
my @at = split /,/, $at;
for my $at (@at) {
$param{'archive_type_' . $at} = 1;
}
}
$param{'status_default_' . $obj->status_default} = 1 if
$obj->status_default;
$param{'sanitize_spec_' . ($obj->sanitize_spec ? 1 : 0)} = 1;
$param{sanitize_spec_manual} = $obj->sanitize_spec
if $obj->sanitize_spec;
$param{'archive_type_preferred_' .
$obj->archive_type_preferred} = 1 if
$obj->archive_type_preferred;
$param{words_in_excerpt} = 40
unless defined $param{words_in_excerpt} &&
$param{words_in_excerpt} ne '';
$param{'sort_order_comments_' . $obj->sort_order_comments} = 1;
$param{'sort_order_posts_' . $obj->sort_order_posts} = 1;
my $lang = $obj->language || 'en';
$param{'language_' . $lang} = 1;
(my $offset = $obj->server_offset) =~ s![-\.]!_!g;
$offset =~ s!_00$!!;
$param{'server_offset_' . $offset} = 1;
$param{'allow_comments_default_' . $obj->allow_comments_default} = 1;
$param{cc_license_name} = MT::Util::cc_name($obj->cc_license)
if $obj->cc_license;
## Load text filters.
my $filters = MT->all_text_filters;
my $default_entries = $obj->convert_paras;
my $default_comments = $obj->convert_paras_comments;
if ($default_entries eq '1') {
$default_entries = '__default__';
}
if ($default_comments eq '1') {
$default_comments = '__default__';
}
$param{text_filters} = [];
$param{text_filters_comments} = [];
for my $filter (keys %$filters) {
my $row = {
filter_key => $filter,
filter_label => $filters->{$filter}{label},
};
my $rowc = { %$row };
$row->{filter_selected} = $filter eq $default_entries;
$rowc->{filter_selected} = $filter eq $default_comments;
push @{ $param{text_filters} }, $row;
push @{ $param{text_filters_comments} }, $rowc;
}
$param{text_filters} = [
sort { $a->{filter_key} cmp $b->{filter_key} }
@{ $param{text_filters} } ];
unshift @{ $param{text_filters} }, {
filter_key => '0',
filter_label => 'None',
filter_selected => !$default_entries,
};
unshift @{ $param{text_filters_comments} }, {
filter_key => '0',
filter_label => 'None',
filter_selected => !$default_entries,
};
} elsif ($type eq 'comment') {
require MT::Entry;
if (my $entry = MT::Entry->load($obj->entry_id)) {
$param{entry_title} = $entry->title;
} else {
$param{no_entry} = 1;
}
}
$param{new_object} = 0;
} else {
$param{new_object} = 1;
for my $col (@$cols) {
$param{$col} = $q->param($col);
$param{$col} = encode_html($param{$col}, 1);
}
if ($type eq 'entry') {
delete $param{'author_id'};
delete $param{'pinged_urls'};
require MT::Blog;
if ($blog_id) {
my $blog = MT::Blog->load($blog_id);
my $def_status = $q->param('status') ||
$blog->status_default;
if ($def_status) {
$param{"status_" . MT::Entry::status_text($def_status)}
= 1;
}
$param{'allow_comments_' . (defined $q->param('allow_comments') ? $q->param('allow_comments') : $blog->allow_comments_default)} = 1;
$param{allow_comments} = $blog->allow_comments_default
unless defined $q->param('allow_comments');
$param{allow_pings} = $blog->allow_pings_default
unless defined $q->param('allow_pings');
}
$param{created_on_formatted} = $q->param('created_on_manual');
if ($q->param('is_bm')) {
$param{selected_text} = $param{text};
$param{text} = sprintf qq(%s\n\n%s),
scalar $q->param('link_title'),
scalar $q->param('link_href'),
scalar $q->param('link_title'),
$param{text};
my $show = $q->param('bm_show') || '';
if ($show =~ /trackback/) {
## Now fetch original page and scan it for embedded
## TrackBack RDF tags.
my $url = $q->param('link_href');
if (my $items = MT::Util::discover_tb($url, 1)) {
if (@$items == 1) {
$param{to_ping_urls} = $items->[0]->{ping_url};
} else {
$param{to_ping_url_loop} = $items;
}
}
}
require MT::Permission;
my $iter = MT::Permission->load_iter({ author_id =>
$app->{author}->id });
my @data;
while (my $perms = $iter->()) {
next unless $perms->can_post;
my $blog = MT::Blog->load($perms->blog_id);
next unless $blog;
push @data, { blog_id => $blog->id,
blog_name => $blog->name,
blog_convert_breaks => $blog->convert_paras,
blog_status => $blog->status_default,
blog_allow_comments =>
$blog->allow_comments_default,
blog_allow_pings =>
$blog->allow_pings_default, };
$param{avail_blogs}{$blog->id} = 1;
}
$param{blog_loop} = \@data;
}
} elsif ($type eq 'template') {
$param{has_name} = $q->param('type') eq 'index' ||
$q->param('type') eq 'custom' ||
$q->param('type') eq 'archive' ||
$q->param('type') eq 'category' ||
$q->param('type') eq 'individual';
$param{has_outfile} = $q->param('type') eq 'index';
$param{has_rebuild} = $q->param('type') eq 'index';
$param{rebuild_me} = 1;
} elsif ($type eq 'blog') {
$param{server_offset_0} = 1;
}
}
if ($type eq 'entry') {
## Load categories and process into loop for category pull-down.
require MT::Category;
my $iter = MT::Category->load_iter({ blog_id => $blog_id });
my $cols = MT::Category->column_names;
my @data;
my $cat_id = $param{category_id};
while (my $obj = $iter->()) {
my $row = { };
for my $col (@$cols) {
$row->{'category_' . $col} = $obj->$col();
}
$row->{category_is_selected} = 1
if $cat_id && $cat_id == $obj->id;
push @data, $row;
}
@data = sort { $a->{category_label} cmp $b->{category_label} }
@data;
my $top = { category_id => '',
category_label => 'Select' };
$top->{category_is_selected} = 1 unless $cat_id;
unshift @data, $top;
$param{category_loop} = \@data;
## Now load user's preferences and customization for new/edit
## entry page.
if ($perms) {
my $prefs = $perms->entry_prefs || 'Advanced|Bottom';
($prefs, my($pos)) = split /\|/, $prefs;
if ($prefs eq 'Basic') {
$param{'disp_prefs_' . $prefs} = 1;
} elsif ($prefs eq 'Advanced') {
my @all = qw( category extended excerpt convert_breaks
allow_comments authored_on allow_pings
ping_urls );
for my $p (@all) {
$param{'disp_prefs_show_' . $p} = 1;
}
} else {
my @p = split /,/, $prefs;
for my $p (@p) {
$param{'disp_prefs_show_' . $p} = 1;
}
}
$param{'position_buttons_' . $pos} = 1;
$param{disp_prefs_bar_colspan} = $param{new_object} ? 1 : 2;
}
## Load text filters.
my %entry_filters;
if (defined(my $filter = $q->param('convert_breaks'))) {
$entry_filters{$filter} = 1;
} elsif ($obj) {
%entry_filters = map { $_ => 1 }
@{ $obj->text_filters };
} else {
my $blog = MT::Blog->load($blog_id);
my $cb = $blog->convert_paras;
$cb = '__default__' if $cb eq '1';
$entry_filters{$cb} = 1;
$param{convert_breaks} = $cb;
}
my $filters = MT->all_text_filters;
$param{text_filters} = [];
for my $filter (keys %$filters) {
push @{ $param{text_filters} }, {
filter_key => $filter,
filter_label => $filters->{$filter}{label},
filter_selected => $entry_filters{$filter},
filter_docs => $filters->{$filter}{docs},
};
}
$param{text_filters} = [
sort { $a->{filter_key} cmp $b->{filter_key} }
@{ $param{text_filters} } ];
unshift @{ $param{text_filters} }, {
filter_key => '0',
filter_label => 'None',
filter_selected => (!keys %entry_filters),
};
} elsif ($type eq 'template') {
$param{"type_$param{type}"} = 1;
} elsif ($type eq 'blog') {
my $cwd = '';
if ($ENV{MOD_PERL}) {
## If mod_perl, just use the document root.
$cwd = $app->{apache}->document_root;
} else {
## Try to get the current directory; first try to use
## getcwd(), in case we are running with taint enabled. If
## that fails (inability to open directory, for example),
## try using cwd(). If that fails, use nothing.
require Cwd;
{
my($bad);
local $SIG{__WARN__} = sub { $bad++ };
eval { $cwd = Cwd::getcwd() };
if ($bad || $@) {
eval { $cwd = Cwd::cwd() };
if ($@ && $@ !~ /Insecure \$ENV{PATH}/) {
die $@;
}
}
}
}
if (!$param{site_path}) {
$param{site_path} = $cwd;
}
if (!$param{archive_path}) {
$param{archive_path} = File::Spec->catdir($cwd, 'archives');
}
if (!$param{site_url}) {
$param{site_url} = $app->base . '/';
}
if (!$param{archive_url}) {
$param{archive_url} = $param{site_url} . 'archives/';
}
} elsif ($type eq 'author') {
my $langs = $app->supported_languages;
my @data;
my $preferred = $obj && $obj->preferred_language ?
$obj->preferred_language : 'en-us';
for my $tag (keys %$langs) {
my $row = { l_tag => $tag, l_name => $langs->{$tag} };
$row->{l_selected} = 1 if $preferred eq $tag;
push @data, $row;
}
$param{languages} = [ sort { $a->{l_name} cmp $b->{l_name} }
@data ];
}
for my $p ($q->param) {
$param{$p} = $q->param($p) if $p =~ /^saved/;
}
if ($q->param('is_bm')) {
my $show = $q->param('bm_show') || '';
if ($show) {
my @show = map "show_$_", split /,/, $show;
@param{ @show } = (1) x @show;
}
if ($show =~ /category/) {
my $iter = MT::Category->load_iter({}, { 'sort' => 'label' });
my @i;
my @c_data;
my %avail_blogs = %{ $param{avail_blogs} };
while (my $cat = $iter->()) {
my $blog_id = $cat->blog_id;
next unless $avail_blogs{$blog_id};
my $label = $cat->label;
$label =~ s!'!\\'!g;
$i[$blog_id] = 0 unless defined $i[$blog_id];
push @c_data, {
category_blog_id => $blog_id,
category_index => $i[$blog_id]++,
category_id => $cat->id,
category_label => $label
};
}
$param{category_loop} = \@c_data;
}
return $app->build_page("bm_entry.tmpl", \%param);
} elsif ($param{output}) {
return $app->build_page($param{output}, \%param);
} else {
return $app->build_page("edit_${type}.tmpl", \%param);
}
}
sub save_object {
my $app = shift;
my $q = $app->{query};
my $type = $q->param('_type');
my $id = $q->param('id');
my $perms = $app->{perms};
return $app->error($app->translate("No permissions"))
if !$perms && $id && $type ne 'author';
if ($id &&
($type eq 'blog' && !$perms->can_edit_config) ||
($type eq 'template' && !$perms->can_edit_templates)) {
return $app->error($app->translate("Permission denied."))
}
if ($type eq 'blog' && !$id && !$app->{author}->can_create_blog) {
return $app->error($app->translate("Permission denied."));
}
if ($type eq 'author') {
## If we are saving an author profile, we need to do some
## password maintenance. First make sure that the two
## passwords match...
if ($q->param('pass') ne $q->param('pass_verify')) {
my %param = (error => 'Passwords do not match.');
my $qual = $id ? '' : 'author_state_';
for my $f (qw( name email url )) {
$param{$qual . $f} = $q->param($f);
}
$param{checked_blog_ids} = { map { $_ => 1 }
$q->param('add_to_blog') };
my $meth = $id ? 'edit_object' : 'edit_permissions';
return $app->$meth(\%param);
}
## ... then check to make sure that the author isn't trying
## to change his/her username to one that already exists.
require MT::Author;
my $name = $q->param('name');
my $existing = MT::Author->load({ name => $name });
if ($existing && (!$q->param('id') ||
$existing->id != $q->param('id'))) {
my %param = (error => 'An author by that name already exists.');
my $qual = $id ? '' : 'author_state_';
for my $f (qw( name email url )) {
$param{$qual . $f} = $q->param($f);
}
$param{checked_blog_ids} = { map { $_ => 1 }
$q->param('add_to_blog') };
my $meth = $id ? 'edit_object' : 'edit_permissions';
return $app->$meth(\%param);
}
}
my $class = $app->_load_driver_for($type) or return;
my($obj);
if ($id) {
$obj = $class->load($id);
} else {
$obj = $class->new;
}
my $names = $obj->column_names;
my %values = map { $_ => scalar $q->param($_) } @$names;
$obj->set_values(\%values);
if ($type eq 'author') {
if (my $pass = $q->param('pass')) {
$obj->set_password($pass);
}
## If this is an author editing his/her profile, $id will be
## some defined value; if so we should update the author's
## cookie to reflect any changes made to username and password.
## Otherwise, this is a new user, and we shouldn't update the
## cookie.
if ($id) {
my($remember) = (split /::/, $app->{cookies}->{user}->value)[2];
my %arg = (
-name => 'user',
-value => join('::', $obj->name, $obj->password, $remember),
-path => $app->path,
);
$arg{-expires} = '+10y' if $remember;
$app->bake_cookie(%arg);
}
} elsif ($type eq 'template') {
$obj->rebuild_me(0) unless $q->param('rebuild_me');
## Strip linefeed characters.
(my $text = $values{text}) =~ tr/\r//d;
$obj->text($text);
} elsif ($type eq 'blog') {
if ($q->param('cfg_screen')) {
my @fields = qw( email_new_comments allow_comment_html
autolink_urls allow_comments_default
convert_paras_comments
allow_anon_comments ping_weblogs ping_blogs
allow_pings_default email_new_pings
autodiscover_links );
for my $cb (@fields) {
$obj->$cb(0) if !defined $q->param($cb);
}
} else {
$obj->is_dynamic(0) unless defined $q->param('is_dynamic');
}
if ($obj->sanitize_spec eq '1') {
$obj->sanitize_spec(scalar $q->param('sanitize_spec_manual'));
}
## If this is a new blog, set the preferences and archive
## settings to the defaults.
if (!$id) {
$obj->days_on_index(7);
$obj->words_in_excerpt(40);
$obj->sort_order_posts('descend');
$obj->language('en');
$obj->sort_order_comments('ascend');
$obj->file_extension('html');
$obj->convert_paras('__default__');
$obj->allow_comments_default(1);
$obj->allow_pings_default(0);
$obj->convert_paras_comments(1);
$obj->sanitize_spec(0);
$obj->ping_weblogs(0);
$obj->ping_blogs(0);
$obj->archive_type('Individual,Monthly');
$obj->archive_type_preferred('Individual');
$obj->status_default(1);
}
} elsif ($type eq 'category') {
$obj->allow_pings(0) if !defined $q->param('allow_pings');
if (defined(my $pass = $q->param('tb_passphrase'))) {
$obj->{__tb_passphrase} = $pass;
}
}
$obj->save or
return $app->error($app->translate(
"Saving object failed: [_1]", $obj->errstr));
if ($type eq 'blog' && !$id) {
## If this is a new blog, we need to set up a permissions
## record for the existing user.
require MT::Permission;
my $perms = MT::Permission->new;
$perms->author_id($app->{author}->id);
$perms->blog_id($obj->id);
$perms->set_full_permissions;
$perms->save;
## Load default templates into new blog database.
my $tmpl_list;
eval { $tmpl_list = require 'MT/default-templates.pl' };
return $app->error($app->translate(
"Can't find default template list; where is " .
"'default-templates.pl'?")) if
$@ || !$tmpl_list || ref($tmpl_list) ne 'ARRAY' || !@$tmpl_list;
require MT::Template;
my @arch_tmpl;
for my $val (@$tmpl_list) {
$val->{text} = $app->translate_templatized($val->{text});
my $tmpl = MT::Template->new;
$tmpl->set_values($val);
$tmpl->blog_id($obj->id);
$tmpl->save or
return $app->error($app->translate(
"Populating blog with default templates failed: [_1]",
$tmpl->errstr));
if ($val->{type} eq 'archive' || $val->{type} eq 'category' ||
$val->{type} eq 'individual') {
push @arch_tmpl, $tmpl;
}
}
## Set up mappings from new templates to archive types.
for my $tmpl (@arch_tmpl) {
my(@at);
if ($tmpl->type eq 'archive') {
@at = qw( Daily Weekly Monthly );
} elsif ($tmpl->type eq 'category') {
@at = qw( Category );
} elsif ($tmpl->type eq 'individual') {
@at = qw( Individual );
}
require MT::TemplateMap;
for my $at (@at) {
my $map = MT::TemplateMap->new;
$map->archive_type($at);
$map->is_preferred(1);
$map->template_id($tmpl->id);
$map->blog_id($tmpl->blog_id);
$map->save
or return $app->error($app->translate(
"Setting up mappings failed: [_1]", $map->errstr));
}
}
} elsif ($type eq 'author' && !$id) {
my $author_id = $obj->id;
for my $blog_id ($q->param('add_to_blog')) {
my $pe = MT::Permission->new;
$pe->blog_id($blog_id);
$pe->author_id($author_id);
$pe->can_post(1);
$pe->save;
}
}
my $blog_id = $q->param('blog_id');
if ($type eq 'blog') {
$blog_id = $obj->id;
}
if ($type eq 'author' && !$id) {
return $app->redirect($app->uri .
'?__mode=edit_permissions&author_id=' . $obj->id);
} elsif ($type eq 'notification') {
return $app->redirect($app->uri .
'?__mode=list&_type=notification&blog_id=' . $blog_id .
'&saved=' . $obj->email);
} elsif ($type eq 'comment') {
return $app->redirect($app->uri .
'?__mode=view&_type=entry&id=' . $obj->entry_id .
'&blog_id=' . $blog_id . '&saved_comment=1');
} elsif (my $cfg_screen = $q->param('cfg_screen')) {
return $app->redirect($app->uri .
"?__mode=$cfg_screen&blog_id=" . $obj->id . '&saved=1');
} elsif ($type eq 'banlist') {
return $app->redirect($app->uri .
'?__mode=list&_type=banlist&blog_id=' . $blog_id .
'&saved=' . $obj->ip);
} else {
return $app->redirect($app->uri .
'?__mode=view&_type=' . $type . '&id=' . $obj->id .
'&blog_id=' . $blog_id . '&saved=1');
}
}
sub list_objects {
my $app = shift;
my $q = $app->{query};
my $type = $q->param('_type');
my $perms = $app->{perms};
return $app->error($app->translate("No permissions"))
unless $type eq 'author' || $perms;
if ($perms &&
(($type eq 'blog' && !$perms->can_edit_config) ||
($type eq 'template' && !$perms->can_edit_templates) ||
($type eq 'author' && !$perms->can_edit_authors))) {
return $app->error($app->translate("Permission denied."));
}
my $id = $q->param('id');
my $class = $app->_load_driver_for($type) or return;
my $blog_id = $q->param('blog_id');
my %param;
my %terms;
my $cols = $class->column_names;
for my $name (@$cols) {
$terms{blog_id} = $blog_id, last
if $name eq 'blog_id';
}
my $iter = $class->load_iter(\%terms);
my(@data, @index_data, @custom_data, @archive_data);
my(%authors);
while (my $obj = $iter->()) {
my $row = $obj->column_values;
if (my $ts = $obj->created_on) {
$row->{created_on_formatted} = format_ts("%Y.%m.%d", $ts);
}
if ($type eq 'author') {
$authors{ $obj->id } = $obj->name;
}
if ($type eq 'template') {
if ($obj->type eq 'index') {
push @index_data, $row;
$row->{rebuild_me} = defined $row->{rebuild_me} ?
$row->{rebuild_me} : 1;
} elsif ($obj->type eq 'custom') {
push @custom_data, $row;
} elsif ($obj->type eq 'archive' || $obj->type eq 'category' ||
$obj->type eq 'individual') {
push @archive_data, $row;
} else {
$param{'template_' . $obj->type} = $obj->id;
}
} else {
$row->{is_odd} = @data % 2 ? 0 : 1;
push @data, $row;
}
}
if ($type eq 'author') {
my $this_author_id = $app->{author}->id;
for my $row (@data) {
$row->{added_by} = $authors{$row->{created_by}}
if $row->{created_by};
$row->{can_delete} = 1
if (($row->{created_by} &&
$row->{created_by} == $this_author_id) ||
$row->{id} == $this_author_id);
}
}
if ($type eq 'notification') {
@data = sort { $a->{email} cmp $b->{email} } @data;
for my $i (0..$#data) {
$data[$i]->{is_odd} = $i % 2 ? 0 : 1;
}
}
if ($type eq 'template') {
for my $ref (\@index_data, \@custom_data, \@archive_data) {
@$ref = sort { $a->{name} cmp $b->{name} } @$ref;
for my $i (0..$#$ref) {
$ref->[$i]{is_odd} = $i % 2 ? 0 : 1;
}
}
$param{object_index_loop} = \@index_data;
$param{object_custom_loop} = \@custom_data;
$param{object_archive_loop} = \@archive_data;
} else {
$param{object_loop} = \@data;
}
$param{object_count} = scalar @data;
$param{saved} = $q->param('saved');
$param{saved_deleted} = $q->param('saved_deleted');
if ($type eq 'banlist') {
$app->build_page('cfg_banlist.tmpl', \%param);
} else {
$app->build_page("list_${type}.tmpl", \%param);
}
}
sub delete_confirm {
my $app = shift;
my %param = ( type => scalar $app->{query}->param('_type') );
my @data;
for my $id ($app->{query}->param('id')) {
push @data, { id => $id };
}
$param{id_loop} = \@data;
$param{num} = scalar @data;
$param{'type_' . $param{type}} = 1;
$param{is_zero} = @data == 0;
$param{is_one} = @data == 1;
$param{is_many} = !$param{is_zero} && !$param{is_one};
$param{thisthese} = $param{is_one} ? 'this' : 'these';
$param{is_power_edit} = $app->{query}->param('is_power_edit') ? 1 : 0;
$app->build_page('delete_confirm.tmpl', \%param);
}
sub delete {
my $app = shift;
my $q = $app->{query};
my $type = $q->param('_type');
my $class = $app->_load_driver_for($type) or return;
my $perms = $app->{perms};
my $author = $app->{author};
## Construct an anonymous subroutine $auth_check to check if
## the user has permission to delete each object being deleted.
my $auth_check = sub { };
if ($type eq 'comment') {
$auth_check = sub {
my($obj) = @_;
require MT::Entry;
my $entry = MT::Entry->load($obj->entry_id);
$perms->can_edit_entry($entry, $author);
};
} elsif ($type eq 'ping') {
$auth_check = sub {
my($obj) = @_;
require MT::Trackback;
require MT::Entry;
my $tb = MT::Trackback->load($obj->tb_id);
my $entry = MT::Entry->load($tb->entry_id);
$perms->can_edit_entry($entry, $author);
};
} elsif ($type eq 'ping_cat') {
$auth_check = sub { $perms->can_edit_categories };
} elsif ($type eq 'entry') {
$auth_check = sub { $perms->can_edit_entry($_[0], $author) };
} elsif ($type eq 'notification') {
$auth_check = sub { $perms->can_edit_notifications };
} elsif ($type eq 'template') {
$auth_check = sub { $perms->can_edit_templates };
} elsif ($type eq 'blog') {
$auth_check = sub {
require MT::Permission;
my $perms = MT::Permission->load({
author_id => $author->id,
blog_id => $_[0]->id,
});
$perms->can_edit_config;
};
} elsif ($type eq 'author') {
$auth_check = sub { $_[0]->id == $author->id ||
($_[0]->created_by && $_[0]->created_by == $author->id) };
} elsif ($type eq 'category') {
$auth_check = sub { $perms->can_edit_categories };
} elsif ($type eq 'templatemap') {
$auth_check = sub { $perms->can_edit_config };
} elsif ($type eq 'banlist') {
$auth_check = sub { $perms->can_edit_config };
}
my($entry_id, $cat_id);
for my $id ($q->param('id')) {
my $obj = $class->load($id);
next unless $obj;
return $app->error($app->translate("Permission denied."))
unless $auth_check->($obj);
if (!$entry_id) {
if ($type eq 'comment') {
$entry_id = $obj->entry_id;
} elsif ($type eq 'ping' || $type eq 'ping_cat') {
require MT::Trackback;
my $tb = MT::Trackback->load($obj->tb_id);
$entry_id = $tb->entry_id;
$cat_id = $tb->category_id;
}
}
$obj->remove;
}
my $blog_id = $q->param('blog_id');
my %types = (
comment => "__mode=view&_type=entry&blog_id=$blog_id&id=$entry_id&",
ping => "__mode=view&_type=entry&blog_id=$blog_id&id=$entry_id&",
ping_cat => "__mode=tb_cat_pings&blog_id=$blog_id&category_id=$cat_id&",
entry => "__mode=list_entries&blog_id=$blog_id&",
notification => "__mode=list&_type=notification&blog_id=$blog_id&",
template => "__mode=list&_type=template&blog_id=$blog_id&",
blog => "",
author => "__mode=list&_type=author&",
category => "__mode=list_cat&blog_id=$blog_id&",
templatemap => "__mode=cfg_archives&id=$blog_id&blog_id=$blog_id&",
banlist => "__mode=list&_type=banlist&blog_id=$blog_id&",
);
my $url = $app->uri . '?' . $types{$type} .
($type eq 'ping' ? 'saved_deleted_ping=1' : 'saved_deleted=1');
if ($q->param('is_power_edit')) {
$url .= '&is_power_edit=1';
}
return $app->build_page('reload_opener.tmpl', { url => $url });
}
sub _load_driver_for {
my $app = shift;
my($type) = @_;
my $class = $API{$type} or
return $app->error($app->translate("Unknown object type [_1]",
$type));
eval "use $class;";
return $app->error($app->translate(
"Loading object driver [_1] failed: [_2]", $class, $@)) if $@;
$class;
}
}
sub show_upload_html {
my $app = shift;
defined(my $text = $app->_process_post_upload) or return;
$app->build_page('show_upload_html.tmpl',
{ upload_html => encode_html($text, 1) });
}
sub start_upload_entry {
my $app = shift;
my $q = $app->{query};
$q->param('_type', 'entry');
defined(my $text = $app->_process_post_upload) or return;
$q->param('text', $text);
$app->edit_object;
}
sub _process_post_upload {
my $app = shift;
my $q = $app->{query};
my($url, $width, $height) = map $q->param($_), qw( url width height );
my $blog_id = $q->param('blog_id');
require MT::Blog;
my $blog = MT::Blog->load($blog_id);
my($thumb, $thumb_width, $thumb_height);
if ($thumb = $q->param('thumb')) {
require MT::Image;
my $base_path = $q->param('site_path') ?
$blog->site_path : $blog->archive_path;
my $file = $q->param('fname');
if ($file =~ m!\.\.|\0|\|!) {
return $app->error($app->translate("Invalid filename '[_1]'", $file));
}
my $i_file = File::Spec->catfile($base_path, $file);
## Untaint. We checked $file for security holes above.
($i_file) = $i_file =~ /(.+)/s;
my $fmgr = $blog->file_mgr;
my $data = $fmgr->get_data($i_file, 'upload')
or return $app->error($app->translate(
"Reading '[_1]' failed: [_2]", $i_file, $fmgr->errstr));
my $img = MT::Image->new( Data => $data,
Type => scalar $q->param('image_type') )
or return $app->error($app->translate(
"Thumbnail failed: [_1]", MT::Image->errstr));
my($w, $h) = map $q->param($_), qw( thumb_width thumb_height );
(my($blob), $thumb_width, $thumb_height) =
$img->scale( Width => $w, Height => $h )
or return $app->error($app->translate("Thumbnail failed: [_1]",
$img->errstr));
require File::Basename;
my($base, $path, $ext) = File::Basename::fileparse($i_file, '\.[^.]*');
my $t_file = $path . $base . '-thumb' . $ext;
$fmgr->put_data($blob, $t_file, 'upload')
or return $app->error($app->translate(
"Error writing to '[_1]': [_2]", $t_file, $fmgr->errstr));
my $url = $q->param('site_path') ? $blog->site_url : $blog->archive_url;
$url .= '/' unless $url =~ m!/$!;
$t_file =~ s!^\Q$base_path\E\\?/?!!;
$thumb = $url . $t_file;
}
if ($q->param('popup')) {
require MT::Template;
if (my $tmpl = MT::Template->load({ blog_id => $blog_id,
type => 'popup_image' })) {
(my $base = $q->param('fname')) =~ s!\.[^.]*$!!;
if ($base =~ m!\.\.|\0|\|!) {
return $app->error($app->translate(
"Invalid basename '[_1]'", $base));
}
my $ext = '.' . $blog->file_extension || 'html';
require MT::Template::Context;
my $ctx = MT::Template::Context->new;
$ctx->stash('image_url', $url);
$ctx->stash('image_width', $width);
$ctx->stash('image_height', $height);
my $popup = $tmpl->build($ctx);
my $fmgr = $blog->file_mgr;
my $base_path = $q->param('site_path') ?
$blog->site_path : $blog->archive_path;
my $p_file = File::Spec->catfile($base_path, $base . $ext);
## If the popup filename already exists, we don't want to overwrite
## it, because it could contain valuable data; so we'll just make
## sure to generate the name uniquely.
my($i, $full_base) = (0, $base . $ext);
while ($fmgr->exists($p_file)) {
$full_base = $base . ++$i . $ext;
$p_file = File::Spec->catfile($base_path, $full_base);
}
## Untaint. We have checked for security holes above, so we
## should be safe.
($p_file) = $p_file =~ /(.+)/s;
$fmgr->put_data($popup, $p_file, 'upload')
or return $app->error($app->translate(
"Error writing to '[_1]': [_2]", $p_file, $fmgr->errstr));
$url = $q->param('site_path') ?
$blog->site_url : $blog->archive_url;
$url .= '/' unless $url =~ m!/$!;
$full_base =~ s!^/!!;
$url .= $full_base;
}
my $link = $thumb ? qq() : "View image";
return <$link
HTML
} elsif ($q->param('include')) {
(my $fname = $url) =~ s!^.*/!!;
if ($thumb) {
return <
HTML
} else {
return <
<$MTEntryTitle$>
<$MTEntryBody$> <$MTEntryMore$> HTML my $tokens = $build->compile($ctx, $preview_code) or return $app->error($app->translate( "Parse error: [_1]", $build->errstr)); defined(my $html = $build->build($ctx, $tokens)) or return $app->error($app->translate( "Build error: [_1]", $build->errstr)); my %param = ( preview_body => $html ); if (my $id = $q->param('id')) { $param{id} = $id; } $param{new_object} = $param{id} ? 0 : 1; my $cols = MT::Entry->column_names; my @data = ({ data_name => 'author_id', data_value => $app->{author}->id }); for my $col (@$cols) { next if $col eq 'created_on' || $col eq 'created_by' || $col eq 'modified_on' || $col eq 'modified_by'; push @data, { data_name => $col, data_value => encode_html(scalar $q->param($col), 1) }; } for my $date (qw( created_on_old created_on_manual )) { push @data, { data_name => $date, data_value => scalar $q->param($date) }; } $param{entry_loop} = \@data; $app->build_page('preview_entry.tmpl', \%param); } sub rebuild_confirm { my $app = shift; my $blog_id = $app->{query}->param('blog_id'); require MT::Blog; my $blog = MT::Blog->load($blog_id); my $at = $blog->archive_type || ''; my(@at, @data); if ($at && $at ne 'None') { @at = split /,/, $at; @data = map { { archive_type => $app->translate($_) } } @at; } my $order = join ',', @at, 'index'; require MT::Entry; my $total = MT::Entry->count({ blog_id => $blog_id }); my %param = ( archive_type_loop => \@data, build_order => $order, build_next => 0, total_entries => $total ); if (my $tmpl_id = $app->{query}->param('tmpl_id')) { require MT::Template; my $tmpl = MT::Template->load($tmpl_id); $param{index_tmpl_id} = $tmpl->id; $param{index_tmpl_name} = $tmpl->name; } $app->build_page('rebuild_confirm.tmpl', \%param); } my %Limit_Multipliers = ( Individual => 1, Daily => 2, Weekly => 5, Monthly => 10, ); sub start_rebuild_pages { my $app = shift; my $q = $app->{query}; my $type = $q->param('type'); my $next = $q->param('next'); my @order = split /,/, $type; my $type_name = $order[$next]; my $total_entries = $q->param('total_entries'); my %param = ( build_type => $type, build_next => $next, total_entries => $total_entries, build_type_name => $type_name ); if (my $mult = $Limit_Multipliers{$type_name}) { $param{offset} = 0; $param{limit} = $app->{cfg}->EntriesPerRebuild * $mult; $param{is_individual} = 1; $param{indiv_range} = "1 - " . ($param{limit} > $total_entries ? $total_entries : $param{limit}); } elsif ($type_name =~ /^index-(\d+)$/) { my $tmpl_id = $1; require MT::Template; my $tmpl = MT::Template->load($tmpl_id); $param{build_type_name} = "index template '" . $tmpl->name . "'"; $param{is_one_index} = 1; } elsif ($type_name =~ /^entry-(\d+)$/) { my $entry_id = $1; require MT::Entry; my $entry = MT::Entry->load($entry_id); $param{build_type_name} = "entry '" . $entry->title . "'"; $param{is_entry} = 1; $param{entry_id} = $entry_id; $param{is_bm} = $q->param('is_bm'); $param{is_new} = $q->param('is_new'); $param{old_status} = $q->param('old_status'); } $param{is_full_screen} = $param{is_entry} && !$param{is_bm}; $app->build_page('rebuilding.tmpl', \%param); } sub rebuild_pages { my $app = shift; my $perms = $app->{perms} or return $app->error($app->translate("No permissions")); require MT::Entry; require MT::Blog; my $q = $app->{query}; my $blog_id = $q->param('blog_id'); my $blog = MT::Blog->load($blog_id); my $order = $q->param('type'); my @order = split /,/, $order; my $next = $q->param('next'); my $done = 0; my $type = $order[$next]; $next++; $done++ if $next >= @order; my($offset, $limit); my $total_entries = $q->param('total_entries'); ## Tells MT::_rebuild_entry_archive_type to cache loaded templates so ## that each template is only loaded once. $app->{cache_templates} = 1; my($tmpl_saved); if ($type eq 'all') { return $app->error($app->translate("Permission denied.")) unless $perms->can_rebuild; $app->rebuild( BlogID => $blog_id ) or return; } elsif ($type eq 'index') { return $app->error($app->translate("Permission denied.")) unless $perms->can_rebuild; $app->rebuild_indexes( BlogID => $blog_id ) or return; } elsif ($type =~ /^index-(\d+)$/) { return $app->error($app->translate("Permission denied.")) unless $perms->can_rebuild; my $tmpl_id = $1; require MT::Template; $tmpl_saved = MT::Template->load($tmpl_id); $app->rebuild_indexes( BlogID => $blog_id, Template => $tmpl_saved, Force => 1 ) or return; $order = "index template '" . $tmpl_saved->name . "'"; } elsif ($type =~ /^entry-(\d+)$/) { my $entry_id = $1; require MT::Entry; my $entry = MT::Entry->load($entry_id); return $app->error("Permission denied.") unless $perms->can_edit_entry($entry, $app->{author}); $app->rebuild_entry( Entry => $entry, BuildDependencies => 1 ) or return; $order = "entry '" . $entry->title . "'"; } elsif ($Limit_Multipliers{$type}) { return $app->error($app->translate("Permission denied.")) unless $perms->can_rebuild; $offset = $q->param('offset') || 0; $limit = $q->param('limit'); if ($offset < $total_entries) { $app->rebuild( BlogID => $blog_id, ArchiveType => $type, NoIndexes => 1, Offset => $offset, Limit => $limit ) or return; $offset += $limit; } if ($offset < $total_entries) { $done-- if $done; $next--; } else { $offset = 0; } } elsif ($type) { return $app->error($app->translate("Permission denied.")) unless $perms->can_rebuild; $app->rebuild( BlogID => $blog_id, ArchiveType => $type, NoIndexes => 1 ) or return; } unless ($done) { my $type_name = $order[$next]; ## If we're moving on to the next rebuild step, recalculate the ## limit. if (defined($offset) && $offset == 0) { if (my $mult = $Limit_Multipliers{$type_name}) { $limit = $app->{cfg}->EntriesPerRebuild * $mult; } } my %param = ( build_type => $order, build_next => $next, build_type_name => $type_name, total_entries => $total_entries, offset => $offset, limit => $limit, is_bm => scalar $q->param('is_bm'), entry_id => scalar $q->param('entry_id'), is_new => scalar $q->param('is_new'), old_status => scalar $q->param('old_status') ); if ($Limit_Multipliers{$type_name}) { $param{is_individual} = 1; $param{indiv_range} = sprintf "%d - %d", $offset+1, $offset + $limit > $total_entries ? $total_entries : $offset + $limit; } $app->build_page('rebuilding.tmpl', \%param); } else { if ($q->param('entry_id')) { require MT::Entry; my $entry = MT::Entry->load(scalar $q->param('entry_id')); require MT::Blog; my $blog = MT::Blog->load($entry->blog_id); my $list = $app->needs_ping( Entry => $entry, Blog => $blog, OldStatus => scalar $q->param('old_status') ); if ($entry->status == MT::Entry::RELEASE() && $list) { my @urls = map { { url => $_ } } @$list; $app->build_page('pinging.tmpl', { blog_id => $blog->id, entry_id => $entry->id, old_status => scalar $q->param('old_status'), is_new => scalar $q->param('is_new'), url_list => \@urls, is_bm => scalar $q->param('is_bm') }); } else { $app->_finish_rebuild_ping($entry, scalar $q->param('is_new')); } } else { my $all = $order =~ /,/; my $type = $order; my $is_one_index = $order =~ /index template/; my $is_entry = $order =~ /entry/; my %param = ( all => $all, type => $type, is_one_index => $is_one_index, is_entry => $is_entry ); if ($is_one_index) { $param{tmpl_url} = $blog->site_url; $param{tmpl_url} .= '/' if $param{tmpl_url} !~ m!/$!; $param{tmpl_url} .= $tmpl_saved->outfile; } $app->build_page('rebuilt.tmpl', \%param); } } } sub send_pings { my $app = shift; my $q = $app->{query}; require MT::Entry; require MT::Blog; my $blog = MT::Blog->load(scalar $q->param('blog_id')); my $entry = MT::Entry->load(scalar $q->param('entry_id')); ## MT::ping_and_save pings each of the necessary URLs, then processes ## the return value from MT::ping to update the list of URLs pinged ## and not successfully pinged. It returns the return value from ## MT::ping for further processing. If a fatal error occurs, it returns ## undef. my $results = $app->ping_and_save(Blog => $blog, Entry => $entry, OldStatus => scalar $q->param('old_status')) or return; my $has_errors = 0; for my $res (@$results) { $has_errors++, $app->log("Ping '$res->{url}' failed: $res->{error}") unless $res->{good}; } $app->_finish_rebuild_ping($entry, scalar $q->param('is_new'), $has_errors); } sub edit_permissions { my $app = shift; my %param = $_[0] ? %{ $_[0] } : (); my $q = $app->{query}; my $author = $app->{author}; require MT::Permission; require MT::Blog; require MT::Author; my @auth_perms = MT::Permission->load({ author_id => $author->id }); ## @auth_perms is a list of Permissions for this user. List the ## blogs for which user has can_edit_authors permissions. my $all_perms = MT::Permission->perms; my $author_id = $q->param('author_id'); $param{edit_author_id} = $author_id; my $iter = MT::Author->load_iter; my @a_data; while (my $author = $iter->()) { push @a_data, { author_id => $author->id, author_name => $author->name }; if ($author_id && $author->id == $author_id) { $a_data[-1]{author_selected} = 1; $param{selected_author_name} = $author->name; $param{can_create_blog} = $author->can_create_blog; $param{can_view_log} = $author->can_view_log; } } $param{author_loop} = \@a_data; my(@data, @o_data); my $has_permission = 0; for my $perms (@auth_perms) { next unless $perms->can_edit_authors; $has_permission++; my $blog = MT::Blog->load($perms->blog_id); my $row = { blog_name => $blog->name, blog_id => $blog->id, author_id => $author_id }; if ($param{checked_blog_ids} && $param{checked_blog_ids}{$blog->id}) { $row->{is_checked} = 1; } my $p = MT::Permission->load({ blog_id => $blog->id, author_id => $author_id }); if ($p && $p->role_mask) { my @p_data; for my $ref (@$all_perms) { my $meth = 'can_' . $ref->[1]; push @p_data, { have_access => $p->$meth(), prompt => $ref->[2], blog_id => $blog->id, author_id => $author_id, mask => $ref->[0] }; } my $break = @p_data % 2 ? int(@p_data / 2) + 1 : @p_data / 2; $row->{perm_loop1} = [ @p_data[0..$break-1] ]; $row->{perm_loop2} = [ @p_data[$break..$#p_data] ]; push @data, $row; } else { push @o_data, $row; } } return $app->error($app->translate("Permission denied.")) unless $has_permission; $param{blog_loop} = \@data; $param{blog_no_access_loop} = \@o_data; $param{saved_add_to_blog} = $q->param('saved_add_to_blog'); $param{saved} = $q->param('saved'); $app->build_page('edit_permissions.tmpl', \%param); } sub save_permissions { my $app = shift; my $q = $app->{query}; require MT::Author; my $my_author_id = $app->{author}->id; my $author_id = $q->param('author_id'); my $author = MT::Author->load($author_id); $author->can_create_blog($q->param('can_create_blog') ? 1 : 0); $author->can_view_log($q->param('can_view_log') ? 1 : 0); $author->save; for my $p ($q->param) { if ($p =~ /^role_mask-(\d+)$/) { my($blog_id) = ($1); my $perms = MT::Permission->load({ author_id => $my_author_id, blog_id => $blog_id }); return $app->error($app->translate("Permission denied.")) unless $perms && $perms->can_edit_authors; my $pe = $my_author_id == $author_id ? $perms : MT::Permission->load({ author_id => $author_id, blog_id => $blog_id }); if (!$pe) { $pe = MT::Permission->new; $pe->blog_id($blog_id); $pe->author_id($author_id); } my $mask = 0; for my $val ($q->param($p)) { $mask += $val; } $pe->role_mask($mask); $pe->save; } } my $url = $app->uri . '?__mode=edit_permissions&author_id=' . $author_id . '&saved=1'; if (my $blog_id = $q->param('add_role_mask')) { my $pe = MT::Permission->load({ author_id => $author_id, blog_id => $blog_id }); if (!$pe) { $pe = MT::Permission->new; $pe->blog_id($blog_id); $pe->author_id($author_id); } $pe->can_post(1); $pe->save; require MT::Blog; my $blog = MT::Blog->load($blog_id); $url .= '&saved_add_to_blog=' . $blog->name; } $app->redirect($url); } sub send_notify { my $app = shift; my $q = $app->{query}; my $entry_id = $q->param('entry_id') or return $app->error($app->translate("No entry ID provided")); require MT::Entry; require MT::Blog; require MT::Util; my $entry = MT::Entry->load($entry_id) or return $app->error($app->translate("No such entry '[_1]'", $entry_id)); my $blog = MT::Blog->load($entry->blog_id); my $author = $entry->author; return $app->error($app->translate( "No email address for author '[_1]'", $author->name)) unless $author->email; my $body = ''; my $cols = 72; my $name = $app->translate("[_1] Update: [_2]", $blog->name, $entry->title); my $fill_len = $cols - length($name) - 2; $fill_len++ if $fill_len % 2; $name = ('(' x ($fill_len/2)) . ' ' . $name . ' ' . (')' x ($fill_len/2)); $body .= $name . "\n\n"; my @ts = offset_time_list(time, $blog); my $ts = sprintf "%04d%02d%02d%02d%02d%02d", $ts[5]+1900, $ts[4]+1, @ts[3,2,1,0]; my $date = format_ts('%B %e, %Y', $ts, $blog); my $fill_left = ' ' x int(($cols - length($date)) / 2); $body .= "$fill_left$date\n\n\n"; $body .= ('-' x $cols) . "\n\n"; require Text::Wrap; $Text::Wrap::columns = $cols; if ($q->param('send_excerpt')) { my $excerpt = $entry->excerpt || MT::Util::first_n_words($entry->text, $blog->words_in_excerpt || 40); local $Text::Wrap::columns = $cols - 4; $body .= Text::Wrap::wrap(" ", " ", $excerpt) . "\n\n"; $body .= ('-' x $cols) . "\n\n"; } $body .= $entry->permalink . "\n\n"; $body .= Text::Wrap::wrap('', '', $q->param('message')) . "\n\n"; if ($q->param('send_body')) { $body .= ('-' x $cols) . "\n\n"; $body .= Text::Wrap::wrap('', '', $entry->text) . "\n"; } my $subj = $app->translate("[_1] Update: [_2]", $blog->name, $entry->title); $subj =~ s![\x80-\xFF]!!g; my %head = ( To => $author->email, From => $author->email, Subject => $subj, 'Content-Transfer-Encoding' => '8bit' ); my $charset = $app->{cfg}->PublishCharset || 'iso-8859-1'; $head{'Content-Type'} = qq(text/plain; charset="$charset"); require MT::Notification; my $iter = MT::Notification->load_iter({ blog_id => $blog->id }); my $i = 1; require MT::Mail; MT::Mail->send(\%head, $body) or return $app->error($app->translate( "Error sending mail ([_1]); try another MailTransfer setting?", MT::Mail->errstr)); delete $head{To}; while (my $note = $iter->()) { next unless $note->email; push @{ $head{Bcc} }, $note->email; if ($i++ % 20 == 0) { MT::Mail->send(\%head, $body) or return $app->error($app->translate( "Error sending mail ([_1]); try another MailTransfer setting?", MT::Mail->errstr)); @{ $head{Bcc} } = (); } } if ($head{Bcc} && @{ $head{Bcc} }) { MT::Mail->send(\%head, $body) or return $app->error($app->translate( "Error sending mail ([_1]); try another MailTransfer setting?", MT::Mail->errstr)); } $app->redirect($app->uri . '?__mode=view&_type=entry&blog_id=' . $entry->blog_id . '&id=' . $entry->id . '&saved_notify=1'); } sub start_upload { my $app = shift; my $perms = $app->{perms} or return $app->error($app->translate("No permissions")); return $app->error($app->translate("Permission denied.")) unless $perms->can_upload; my $blog_id = $app->{query}->param('blog_id'); require MT::Blog; my $blog = MT::Blog->load($blog_id); $app->build_page('upload.tmpl', { local_archive_path => $blog->archive_path, local_site_path => $blog->site_path, }); } sub upload_file { my $app = shift; my $perms = $app->{perms} or return $app->error($app->translate("No permissions")); return $app->error($app->translate("Permission denied.")) unless $perms->can_upload; my $q = $app->{query}; my($fh, $no_upload); if ($ENV{MOD_PERL}) { my $up = $q->upload('file'); $no_upload = !$up || !$up->size; $fh = $up->fh if $up; } else { ## Older versions of CGI.pm didn't have an 'upload' method. eval { $fh = $q->upload('file') }; if ($@ && $@ =~ /^Undefined subroutine/) { $fh = $q->param('file'); } $no_upload = !$fh; } my $has_overwrite = $q->param('overwrite_yes') || $q->param('overwrite_no'); return $app->error($app->translate("You did not choose a file to upload.")) if $no_upload && !$has_overwrite; my $fname = $q->param('file') || $q->param('fname'); $fname =~ s!\\!/!g; ## Change backslashes to forward slashes $fname =~ s!^.*/!!; ## Get rid of full directory paths if ($fname =~ m!\.\.|\0|\|!) { return $app->error($app->translate("Invalid filename '[_1]'", $fname)); } my $blog_id = $q->param('blog_id'); require MT::Blog; my $blog = MT::Blog->load($blog_id); my $fmgr = $blog->file_mgr; ## Set up the full path to the local file; this path could start ## at either the Local Site Path or Local Archive Path, and could ## include an extra directory or two in the middle. my($base, $extra_path); if ($q->param('site_path')) { $base = $blog->site_path; $extra_path = $q->param('extra_path_site'); } else { $base = $blog->archive_path; $extra_path = $q->param('extra_path_archive'); } my $extra_path_save = $extra_path; my $path = $base; if ($extra_path) { if ($extra_path =~ m!\.\.|\0|\|!) { return $app->error($app->translate( "Invalid extra path '[_1]'", $extra_path)); } $path = File::Spec->catdir($path, $extra_path); ## Untaint. We already checked for security holes in $extra_path. ($path) = $path =~ /(.+)/s; ## Build out the directory structure if it doesn't exist. DirUmask ## determines the permissions of the new directories. unless ($fmgr->exists($path)) { $fmgr->mkpath($path) or return $app->error($app->translate( "Can't make path '[_1]': [_2]", $path, $fmgr->errstr)); } } $extra_path = File::Spec->catfile($extra_path, $fname); my $local_file = File::Spec->catfile($path, $fname); ## Untaint. We have already tested $fname and $extra_path for security ## issues above, and we have to assume that we can trust the user's ## Local Archive Path setting. So we should be safe. ($local_file) = $local_file =~ /(.+)/s; ## If $local_file already exists, we try to write the upload to a ## tempfile, then ask for confirmation of the upload. if ($fmgr->exists($local_file)) { if ($has_overwrite) { my $tmp = $q->param('temp'); if ($tmp =~ m!([^/]+)$!) { $tmp = $1; } else { return $app->error($app->translate( "Invalid temp file name '[_1]'", $tmp)); } my $tmp_file = File::Spec->catfile($app->{cfg}->TempDir, $tmp); if ($q->param('overwrite_yes')) { $fh = gensym(); open $fh, $tmp_file or return $app->error($app->translate( "Error opening '[_1]': [_2]", $tmp_file, "$!")); } else { if (-e $tmp_file) { unlink($tmp_file) or return $app->error($app->translate( "Error deleting '[_1]': [_2]", $tmp_file, "$!")); } return $app->start_upload; } } else { eval { require File::Temp }; if ($@) { return $app->error($app->translate( "File with name '[_1]' already exists. (Install " . "File::Temp if you'd like to be able to overwrite " . "existing uploaded files.)", $fname)); } my($tmp_fh, $tmp_file) = File::Temp::tempfile(DIR => $app->{cfg}->TempDir); defined(_write_upload($fh, $tmp_fh)) or return $app->error($app->translate( "File with name '[_1]' already exists; Tried to write " . "to tempfile, but open failed: [_2]", $fname, "$!")); my($vol, $path, $tmp) = File::Spec->splitpath($tmp_file); return $app->build_page('upload_confirm.tmpl', { temp => $tmp, extra_path => $extra_path_save, site_path => scalar $q->param('site_path'), fname => $fname }); } } ## File does not exist, or else we have confirmed that we can overwrite. my $umask = oct $app->{cfg}->UploadUmask; my $old = umask($umask); defined(my $bytes = $fmgr->put($fh, $local_file, 'upload')) or return $app->error($app->translate( "Error writing upload to '[_1]': [_2]", $local_file, $fmgr->errstr)); umask($old); ## If we are overwriting the file, that means we still have a temp file ## lying around. Delete it. if ($q->param('overwrite_yes')) { my $tmp = $q->param('temp'); if ($tmp =~ m!([^/]+)$!) { $tmp = $1; } else { return $app->error($app->translate( "Invalid temp file name '[_1]'", $tmp)); } my $tmp_file = File::Spec->catfile($app->{cfg}->TempDir, $tmp); unlink($tmp_file) or return $app->error($app->translate( "Error deleting '[_1]': [_2]", $tmp_file, "$!")); } ## Use Image::Size to check if the uploaded file is an image, and if so, ## record additional image info (width, height). We first rewind the ## filehandle $fh, then pass it in to imgsize. seek $fh, 0, 0; eval { require Image::Size; }; return $app->error($app->translate( "Perl module Image::Size is required to determine " . "width and height of uploaded images.")) if $@; my($w, $h, $id) = Image::Size::imgsize($fh); ## We are going to use $extra_path as the filename and as the url passed ## in to the templates. So, we want to replace all of the '\' characters ## with '/' characters so that it won't look like backslashed characters. ## Also, get rid of a slash at the front, if present. $extra_path =~ s!\\!/!g; $extra_path =~ s!^/!!; my %param = ( width => $w, height => $h, bytes => $bytes, image_type => $id, fname => $extra_path, site_path => scalar $q->param('site_path') ); my $url = $q->param('site_path') ? $blog->site_url : $blog->archive_url; $url .= '/' unless $url =~ m!/$!; $extra_path =~ s!^/!!; $url .= $extra_path; $param{url} = $url; $param{is_image} = defined($w) && defined($h); if ($param{is_image}) { eval { require MT::Image; MT::Image->new or die; }; $param{do_thumb} = !$@ ? 1 : 0; } $app->build_page('upload_complete.tmpl', \%param); } sub _write_upload { my($upload_fh, $dest_fh) = @_; my $fh = gensym(); if (ref($dest_fh) eq 'GLOB') { $fh = $dest_fh; } else { open $fh, ">$dest_fh" or return; } binmode $fh; binmode $upload_fh; my($bytes, $data) = (0); while (my $len = read $upload_fh, $data, 8192) { print $fh $data; $bytes += $len; } close $fh; $bytes; } sub start_search_replace { my $app = shift; my $perms = $app->{perms} or return $app->error($app->translate("No permissions")); return $app->error($app->translate("Permission denied.")) unless $perms->can_post; $app->build_page('search_replace.tmpl', { search_cols_title => 1, search_cols_text => 1, search_cols_text_more => 1, search_cols_keywords => 1, }); } sub search_replace { my $app = shift; my $q = $app->{query}; my $blog_id = $q->param('blog_id'); my $perms = $app->{perms} or return $app->error($app->translate("No permissions")); my($search, $replace, $do_replace, $case, $is_regex) = map scalar $q->param($_), qw( search replace do_replace case is_regex ); my @cols = $q->param('search_cols'); ## Sometimes we need to pass in the search columns like 'title,text', so ## we look for a comma (not a valid character in a column name) and split ## on it if it's there. if ($cols[0] =~ /,/) { @cols = split /,/, $cols[0]; } unless ($is_regex) { $search = quotemeta($search); $search = '(?i)' . $search unless $case; } require MT::Entry; my $iter = MT::Entry->load_iter({ blog_id => $blog_id }, { 'sort' => 'created_on', direction => 'descend' }); my @data; my $i = 1; my @to_save; my $author_id = $app->{author}->id; while (my $entry = $iter->()) { next unless $perms->can_edit_entry($entry, $app->{author}); my $match = 0; for my $col (@cols) { my $text = $entry->$col(); $text = '' unless defined $text; if ($do_replace) { $match++ if $text =~ s!$search!$replace!go; $entry->$col($text); } else { $match = $text =~ m!$search!o; last if $match; } } if ($match && $do_replace) { push @to_save, $entry; } if ($match) { my $title = $entry->title; unless ($title) { $title = remove_html($entry->text); $title = substr($title, 0, 22) . '...'; } push @data, { entry_title => encode_html($title, 1), entry_id => $entry->id, entry_created_on => format_ts("%Y.%m.%d", $entry->created_on), is_odd => $i++ % 2 ? 1 : 0, }; } } for my $entry (@to_save) { $entry->save or return $app->error($app->translate( "Saving entry failed: [_2]", $entry->errstr)); } my %res = ( have_results => 1, results => \@data, search => scalar $q->param('search'), replace => $replace, do_replace => $do_replace, case => $case, is_regex => $is_regex ); for my $col (@cols) { $res{'search_cols_' . $col} = 1; } $app->build_page('search_replace.tmpl', \%res); } sub export { my $app = shift; $app->{no_print_body} = 1; local $| = 1; $app->send_http_header('text/plain'); require MT::Entry; require MT::Permission; my $q = $app->{query}; my $SEP = ('-' x 8); my $SUB_SEP = ('-' x 5); require MT::Blog; my $blog_id = $q->param('blog_id') or return $app->error($app->translate("No blog ID")); my $blog = MT::Blog->load($blog_id) or return $app->error($app->translate( "Load of blog '[_1]' failed: [_2]", $blog_id, MT::Blog->errstr)); $blog->language('en'); my $author_id = $app->{author}->id; my $perms = MT::Permission->load({ author_id => $author_id, blog_id => $blog_id }); return $app->error($app->translate("You do not have export permissions")) unless $perms && $perms->can_edit_config; ## Create template for exporting a single entry require MT::Template; require MT::Template::Context; my $tmpl = MT::Template->new; $tmpl->blog_id($blog_id); $tmpl->name('Export Template'); $tmpl->text(<<'TEXT'); AUTHOR: <$MTEntryAuthor$> TITLE: <$MTEntryTitle$> STATUS: <$MTEntryStatus$> ALLOW COMMENTS: <$MTEntryFlag flag="allow_comments"$> CONVERT BREAKS: <$MTEntryFlag flag="convert_breaks"$> ALLOW PINGS: <$MTEntryFlag flag="allow_pings"$> PRIMARY CATEGORY: <$MTEntryCategory$>\n"); require MT::Entry; require MT::Placement; require MT::Category; require MT::Author; require MT::Permission; my $q = $app->{query}; my $SEP = ('-' x 8); my $SUB_SEP = ('-' x 5); my(%authors, %categories); require MT::Blog; my $blog_id = $q->param('blog_id') or return $app->error($app->translate("No blog ID")); my $blog = MT::Blog->load($blog_id) or return $app->error($app->translate( "Load of blog '[_1]' failed: [_2]", $blog_id, MT::Blog->errstr)); ## Determine the author as whom we will import the entries. my $author = $app->{author}; my $author_id = $author->id; my $perms = MT::Permission->load({ author_id => $author_id, blog_id => $blog_id }); return $app->error($app->translate("You do not have import permissions")) unless $perms && $perms->can_edit_config; $app->print("Importing entries into blog '", $blog->name, "'\n"); my($pass); my $import_as_me = $q->param('import_as_me'); if ($import_as_me) { $app->print("Importing entries as author '", $author->name, "'\n"); } else { $pass = $q->param('password') or return $app->error($app->translate( "You need to provide a password if you are going to\n" . "create new authors for each author listed in your blog.\n")); $app->print("Creating new authors for each author found in the blog\n"); } $app->print("\n"); ## Default category for imported entries. my $def_cat_id = $q->param('default_cat_id'); my $t_start = $q->param('title_start'); my $t_end = $q->param('title_end'); my $allow_comments = $blog->allow_comments_default; my $allow_pings = $blog->allow_pings_default ? 1 : 0; my $convert_breaks = $blog->convert_paras; my $def_status = $q->param('default_status') || $blog->status_default; my $dir = $app->{cfg}->ImportPath; opendir DH, $dir or return $app->error($app->translate( "Can't open directory '[_1]': [_2]", $dir, "$!")); for my $f (readdir DH) { next if $f =~ /^\./; $app->print("Importing entries from file '$f'\n"); my $file = File::Spec->catfile($dir, $f); open FH, $file or return $app->error($app->translate( "Can't open file '[_1]': [_2]", $file, "$!")); local $/ = $SEP; ENTRY_BLOCK: while () { my($meta, @pieces) = split /$SUB_SEP/; next unless $meta && @pieces; ## Create entry object and assign some defaults. my $entry = MT::Entry->new; $entry->blog_id($blog_id); $entry->status($def_status); $entry->allow_comments($allow_comments); $entry->allow_pings($allow_pings); $entry->convert_breaks($convert_breaks); $entry->author_id($author_id) if $import_as_me; ## Some users may want to import just their GM comments, having ## already imported their GM entries. We try to match up the ## entries using the created on timestamp, and the import file ## tells us not to import an entry with the meta-tag "NO ENTRY". my $no_save = 0; ## Handle all meta-data: author, category, title, date. my $i = -1; my($primary_cat_id, @placements); my @lines = split /\r?\n/, $meta; META: for my $line (@lines) { $i++; next unless $line; $line =~ s!^\s*!!; $line =~ s!\s*$!!; my($key, $val) = split /\s*:\s*/, $line, 2; if ($key eq 'AUTHOR' && !$import_as_me) { my $author; unless ($author = $authors{$val}) { $author = MT::Author->load({ name => $val }); } unless ($author) { $author = MT::Author->new; $author->created_by($author_id); $author->name($val); $author->email(''); $author->set_password($pass); $app->print("Creating new author ('$val')..."); if ($author->save) { $app->print("ok\n"); } else { $app->print("failed\n"); return $app->error($app->translate( "Saving author failed: [_1]", $author->errstr)); } $authors{$val} = $author; $app->print("Assigning permissions for new author..."); my $perms = MT::Permission->new; $perms->blog_id($blog_id); $perms->author_id($author->id); $perms->can_post(1); if ($perms->save) { $app->print("ok\n"); } else { $app->print("failed\n"); return $app->error($app->translate( "Saving permission failed: [_1]", $perms->errstr)); } } $entry->author_id($author->id); } elsif ($key eq 'CATEGORY' || $key eq 'PRIMARY CATEGORY') { if ($val) { my $cat; unless ($cat = $categories{$val}) { $cat = MT::Category->load({ label => $val, blog_id => $blog_id }); } unless ($cat) { $cat = MT::Category->new; $cat->blog_id($blog_id); $cat->label($val); $cat->author_id($entry->author_id); $app->print("Creating new category ('$val')..."); if ($cat->save) { $app->print("ok\n"); } else { $app->print("failed\n"); return $app->error($app->translate( "Saving category failed: [_1]", $cat->errstr)); } $categories{$val} = $cat; } if ($key eq 'CATEGORY') { push @placements, $cat->id; } else { $primary_cat_id = $cat->id; } } } elsif ($key eq 'TITLE') { $entry->title($val); } elsif ($key eq 'DATE') { my $date = $app->_convert_date($val) or return; $entry->created_on($date); } elsif ($key eq 'STATUS') { my $status = MT::Entry::status_int($val) or return $app->error($app->translate( "Invalid status value '[_1]'", $val)); $entry->status($status); } elsif ($key eq 'ALLOW COMMENTS') { $val = 0 unless $val; $entry->allow_comments($val); } elsif ($key eq 'CONVERT BREAKS') { $val = 0 unless $val; $entry->convert_breaks($val); } elsif ($key eq 'ALLOW PINGS') { $val = 0 unless $val; return $app->error("Invalid allow pings value '$val'") unless $val eq 0 || $val eq 1; $entry->allow_pings($val); } elsif ($key eq 'NO ENTRY') { $no_save++; } elsif ($key eq 'START BODY') { ## Special case for backwards-compatibility with old ## export files: if we see START BODY: on a line, we ## gather up the rest of the lines in meta and package ## them for handling below in the non-meta area. @pieces = ("BODY:\n" . join "\n", @lines[$i+1..$#lines]); last META; } } ## If we're not saving this entry (but rather just using it to ## import comments, for example), we need to load the relevant ## entry using the timestamp. if ($no_save) { my $ts = $entry->created_on; $entry = MT::Entry->load({ created_on => $ts, blog_id => $blog_id }); if (!$entry) { $app->print("Can't find existing entry with timestamp " . "'$ts'... skipping comments, and moving on to " . "next entry.\n"); next ENTRY_BLOCK; } else { $app->print(sprintf "Importing into existing " . "entry %d ('%s')\n", $entry->id, $entry->title); } } ## Deal with non-meta pieces: entry body, extended entry body, ## comments. We need to hold the list of comments until after ## we have saved the entry, then assign the new entry ID of ## the entry to each comment. my(@comments, @pings); for my $piece (@pieces) { $piece =~ s!^\s*!!; $piece =~ s!\s*$!!; if ($piece =~ s/^BODY:\r?\n//) { $entry->text($piece); } elsif ($piece =~ s/^EXTENDED BODY:\r?\n//) { $entry->text_more($piece); } elsif ($piece =~ s/^EXCERPT:\r?\n//) { $entry->excerpt($piece) if $piece =~ /\S/; } elsif ($piece =~ s/^KEYWORDS:\r?\n//) { $entry->keywords($piece) if $piece =~ /\S/; } elsif ($piece =~ s/^COMMENT:\r?\n//) { ## Comments are: AUTHOR, EMAIL, URL, IP, DATE (in any order), ## then body my $comment = MT::Comment->new; $comment->blog_id($blog_id); my @lines = split /\r?\n/, $piece; my($i, $body_idx) = (0) x 2; COMMENT: for my $line (@lines) { $line =~ s!^\s*!!; my($key, $val) = split /\s*:\s*/, $line, 2; if ($key eq 'AUTHOR') { $comment->author($val); } elsif ($key eq 'EMAIL') { $comment->email($val); } elsif ($key eq 'URL') { $comment->url($val); } elsif ($key eq 'IP') { $comment->ip($val); } elsif ($key eq 'DATE') { my $date = $app->_convert_date($val) or return; $comment->created_on($date); } else { ## Now we have reached the body of the comment; ## everything from here until the end of the ## array is body. $body_idx = $i; last COMMENT; } $i++; } $comment->text( join "\n", @lines[$body_idx..$#lines] ); push @comments, $comment; } elsif ($piece =~ s/^PING:\r?\n//) { ## Pings are: TITLE, URL, IP, DATE, BLOG NAME, ## then excerpt require MT::TBPing; my $ping = MT::TBPing->new; $ping->blog_id($blog_id); my @lines = split /\r?\n/, $piece; my($i, $body_idx) = (0) x 2; PING: for my $line (@lines) { $line =~ s!^\s*!!; my($key, $val) = split /\s*:\s*/, $line, 2; if ($key eq 'TITLE') { $ping->title($val); } elsif ($key eq 'URL') { $ping->source_url($val); } elsif ($key eq 'IP') { $ping->ip($val); } elsif ($key eq 'DATE') { my $date = $app->_convert_date($val) or return; $ping->created_on($date); } elsif ($key eq 'BLOG NAME') { $ping->blog_name($val); } else { ## Now we have reached the ping excerpt; ## everything from here until the end of the ## array is body. $body_idx = $i; last PING; } $i++; } $ping->excerpt( join "\n", @lines[$body_idx..$#lines] ); push @pings, $ping; } } ## Assign a title if one is not already assigned. unless ($entry->title) { my $body = $entry->text; if ($t_start && $t_end && $body =~ s!\Q$t_start\E(.*?)\Q$t_end\E\s*!!s) { (my $title = $1) =~ s/[\r\n]/ /g; $entry->title($title); $entry->text($body); } else { $entry->title( MT::Util::first_n_words($body, 5) ); } } ## If an entry has comments listed along with it, set ## allow_comments to 1 no matter what the default is. if (@comments && !$entry->allow_comments) { $entry->allow_comments(1); } ## If an entry has TrackBack pings listed along with it, ## set allow_pings to 1 no matter what the default is. if (@pings) { $entry->allow_pings(1); ## If the entry has TrackBack pings, we need to make sure ## that an MT::Trackback object is created. To do that, we ## need to make sure that $entry->save is called. $no_save = 0; } ## Save entry. unless ($no_save) { $app->print("Saving entry ('", $entry->title, "')... "); if ($entry->save) { $app->print("ok (ID ", $entry->id, ")\n"); } else { $app->print("failed\n"); return $app->error($app->translate( "Saving entry failed: [_1]", $entry->errstr)); } } ## Save placement. ## If we have no primary category ID (from a PRIMARY CATEGORY ## key), we first look to see if we have any placements from ## CATEGORY tags. If so, we grab the first one and use it as the ## primary placement. If not, we try to use the default category ## ID specified. if (!$primary_cat_id) { if (@placements) { $primary_cat_id = shift @placements; } elsif ($def_cat_id) { $primary_cat_id = $def_cat_id; } } else { ## If a PRIMARY CATEGORY is also specified as a CATEGORY, we ## don't want to add it twice; so we filter it out. @placements = grep { $_ != $primary_cat_id } @placements; } ## So if we have a primary placement from any of the means ## specified above, we add the placement. if ($primary_cat_id) { my $place = MT::Placement->new; $place->is_primary(1); $place->entry_id($entry->id); $place->blog_id($blog_id); $place->category_id($primary_cat_id); $place->save or return $app->error($app->translate( "Saving placement failed: [_1]", $place->errstr)); } ## Now add all of the other, non-primary placements. for my $cat_id (@placements) { my $place = MT::Placement->new; $place->is_primary(0); $place->entry_id($entry->id); $place->blog_id($blog_id); $place->category_id($cat_id); $place->save or return $app->error($app->translate( "Saving placement failed: [_1]", $place->errstr)); } ## Save comments. for my $comment (@comments) { $comment->entry_id($entry->id); $app->print("Creating new comment ('", $comment->author, "')... "); if ($comment->save) { $app->print("ok (ID ", $comment->id, ")\n"); } else { $app->print("failed\n"); return $app->error($app->translate( "Saving comment failed: [_1]", $comment->errstr)); } } ## Save pings. if (@pings) { my $tb = MT::Trackback->load({ entry_id => $entry->id }) or return $app->error($app->translate( "Entry has no MT::Trackback object!")); for my $ping (@pings) { $ping->tb_id($tb->id); $app->print("Creating new ping ('", $ping->title, "')... "); if ($ping->save) { $app->print("ok (ID ", $ping->id, ")\n"); } else { $app->print("failed\n"); return $app->error($app->translate( "Saving ping failed: [_1]", $ping->errstr)); } } } } } closedir DH; $app->print(< TEXT 1; } sub _convert_date { my $app = shift; my($date) = @_; my($mo, $d, $y, $h, $m, $s, $ampm) = $date =~ m!^(\d{1,2})/(\d{1,2})/(\d{2,4}) (\d{1,2}):(\d{1,2}):(\d{1,2})(?:\s(\w{2}))?$! or return $app->error($app->translate( "Invalid date format '[_1]'; must be " . "'MM/DD/YYYY HH:MM:SS AM|PM' (AM|PM is optional)", $date)); if ($ampm) { if ($ampm eq 'PM' && $h < 12) { $h += 12; } elsif ($ampm eq 'AM' && $h == 12) { $h = 0; } } if (length($y) == 2) { $y += 1900; } sprintf "%04d%02d%02d%02d%02d%02d", $y, $mo, $d, $h, $m, $s; } sub show_entry_prefs { my $app = shift; my $perms = $app->{perms} or return $app->error($app->translate("No permissions")); my %param; my $prefs = $perms->entry_prefs || 'Advanced|Bottom'; ($prefs, my($pos)) = split /\|/, $prefs; if ($prefs eq 'Basic' || $prefs eq 'Advanced') { $param{'disp_prefs_' . $prefs} = 1; } else { $param{disp_prefs_custom} = 1; my @p = split /,/, $prefs; for my $p (@p) { $param{'disp_prefs_show_' . $p} = 1; } } $param{'position_' . $pos} = 1; $param{entry_id} = $app->{query}->param('entry_id'); $app->build_page('entry_prefs.tmpl', \%param); } sub save_entry_prefs { my $app = shift; my $perms = $app->{perms} or return $app->error($app->translate("No permissions")); my $q = $app->{query}; my $type = $q->param('entry_prefs'); my $prefs; if ($type eq 'Custom') { $prefs = join ',', $q->param('custom_prefs'); } else { $prefs = $type; } $prefs .= '|' . $q->param('bar_position'); $perms->entry_prefs($prefs); $perms->save or return $app->error($app->translate( "Saving permissions failed: [_1]", $perms->errstr)); my $url = $app->uri . '?__mode=view&_type=entry'; if (my $id = $q->param('entry_id')) { $url .= '&id=' . $id; } $url .= '&blog_id=' . $perms->blog_id . '&saved_prefs=1'; $app->build_page('reload_opener.tmpl', { url => $url }); } sub pinged_urls { my $app = shift; my $perms = $app->{perms} or return $app->error($app->translate("No permissions")); my %param; my $entry_id = $app->{query}->param('entry_id'); require MT::Entry; my $entry = MT::Entry->load($entry_id); $param{url_loop} = [ map { { url => $_ } } @{ $entry->pinged_url_list } ]; $app->build_page('pinged_urls.tmpl', \%param); } sub tb_cat_pings { my $app = shift; my $perms = $app->{perms} or return $app->error($app->translate("No permissions")); return $app->error($app->translate("Permission denied.")) unless $perms->can_edit_categories; require MT::Category; my $q = $app->{query}; my $cat = MT::Category->load(scalar $q->param('category_id')); require MT::Trackback; require MT::TBPing; my $tb = MT::Trackback->load({ category_id => $cat->id }); my @tb_data; if ($tb) { my $iter = MT::TBPing->load_iter({ tb_id => $tb->id }, { 'sort' => 'created_on', direction => 'ascend' }); my $i = 1; while (my $ping = $iter->()) { my $df = format_ts("%Y-%m-%d %H:%M:%S", $ping->created_on); push @tb_data, { ping_id => $ping->id, ping_title => $ping->title, ping_url => $ping->source_url, ping_ip => $ping->ip, ping_created => $df, ping_odd => ($i++ % 2 ? 1 : 0) }; } } my %param; $param{ping_loop} = \@tb_data; $param{saved_deleted} = $q->param('saved_deleted'); $app->build_page('tb_cat_pings.tmpl', \%param); } sub reg_file { my $app = shift; my $q = $app->{query}; my $js = _bm_js($app->base . $app->uri, scalar $q->param('bm_show'), scalar $q->param('bm_height')); $js =~ s!d=document!d=external.menuArguments.document!; $app->{no_print_body} = 1; $app->set_header('Content-Disposition' => 'attachment; filename=mt.reg'); $app->send_http_header('text/plain; name=mt.reg'); $app->print( qq(REGEDIT4\r\n) . qq([HKEY_CURRENT_USER\\Software\\Microsoft\\Internet Explorer\\MenuExt\\MT It!]\r\n) . qq(@="$js"\r\n) . qq("contexts"=hex:31)); 1; } sub category_add { $_[0]->build_page('category_add.tmpl') } sub category_do_add { my $app = shift; my $q = $app->{query}; require MT::Category; my $name = $q->param('label') or return $app->error("No label"); my $cat = MT::Category->new; $cat->blog_id(scalar $q->param('blog_id')); $cat->author_id($app->{user}->id); $cat->label($name); $cat->save or return $app->error($cat->errstr); my $id = $cat->id; $name = MT::Util::encode_js($name); my %param = (javascript => <