package BSE::Edit::Article;
use strict;
use base qw(BSE::Edit::Base);
-use BSE::Util::Tags qw(tag_error_img);
+use BSE::Util::Tags qw(tag_error_img tag_article tag_object);
use BSE::Util::SQL qw(now_sqldate now_sqldatetime);
-use BSE::Util::Valid qw/valid_date/;
use BSE::Permissions;
-use DevHelp::HTML qw(:default popup_menu);
+use BSE::Util::HTML qw(:default popup_menu);
use BSE::Arrows;
-use BSE::CfgInfo qw(custom_class admin_base_url cfg_image_dir);
+use BSE::CfgInfo qw(custom_class admin_base_url cfg_image_dir cfg_dist_image_uri cfg_image_uri);
use BSE::Util::Iterate;
use BSE::Template;
use BSE::Util::ContentType qw(content_type);
+use BSE::Regen 'generate_article';
+use DevHelp::Date qw(dh_parse_date dh_parse_sql_date);
+use List::Util qw(first);
use constant MAX_FILE_DISPLAYNAME_LENGTH => 255;
+use constant ARTICLE_CUSTOM_FIELDS_CFG => "article custom fields";
+
+our $VERSION = "1.056";
=head1 NAME
return $req->json_content
(
{
+ success => 0,
message => "Access forbidden: user not logged on",
errors => {},
+ error_code => "LOGON",
}
);
}
my $action;
my %actions = $self->article_actions;
for my $check (keys %actions) {
- if ($cgi->param($check) || $cgi->param("$check.x")) {
+ if ($cgi->param($check) || $cgi->param("$check.x")
+ || $cgi->param("a_$check") || $cgi->param("a_$check.x")) {
$action = $check;
last;
}
BSE::Permissions->check_logon($req)
or return BSE::Template->get_refresh($req->url('logon'), $req->cfg);
+ my $mymsg;
+ my $article = $self->_dummy_article($req, $articles, \$mymsg);
+ unless ($article) {
+ require BSE::Edit::Site;
+ my $site = BSE::Edit::Site->new(cfg=>$req->cfg, db=> BSE::DB->single);
+ return $site->edit_sections($req, $articles, $mymsg);
+ }
+
my $cgi = $req->cgi;
my $action = 'add';
my %actions = $self->noarticle_actions;
}
}
my $method = $actions{$action};
- return $self->$method($req, $articles);
+ return $self->$method($req, $article, $articles);
}
sub article_actions {
addimg => 'add_image',
a_edit_image => 'req_edit_image',
a_save_image => 'req_save_image',
+ a_order_images => 'req_order_images',
remove => 'remove',
showimages => 'show_images',
process => 'save_image_changes',
a_ajax_get => 'req_ajax_get',
a_ajax_save_body => 'req_ajax_save_body',
a_ajax_set => 'req_ajax_set',
+ a_filemeta => 'req_filemeta',
+ a_csrfp => 'req_csrfp',
+ a_tree => 'req_tree',
+ a_article => 'req_article',
+ a_config => 'req_config',
+ a_restepkid => 'req_restepkid',
);
}
(
add => 'add_form',
save => 'save_new',
+ a_csrfp => 'req_csrfp',
+ a_config => 'req_config',
);
}
$parent = $articles->getByPkey($article->{id});
}
- my $shopid = $self->{cfg}->entryErr('articles', 'shop');
+ my $shopid = $self->cfg->entryErr('articles', 'shop');
return $article->{parentid} && $parent &&
($article->{parentid} == $shopid ||
- $parent->{generator} eq 'Generate::Catalog');
+ $parent->{generator} eq 'BSE::Generate::Catalog');
}
sub possible_parents {
my %labels;
my @values;
- my $shopid = $self->{cfg}->entryErr('articles', 'shop');
+ my $shopid = $self->cfg->entryErr('articles', 'shop');
my @parents = $articles->getBy('level', $article->{level}-1);
- @parents = grep { $_->{generator} eq 'Generate::Article'
+ @parents = grep { $_->{generator} eq 'BSE::Generate::Article'
&& $_->{id} != $shopid } @parents;
# user can only select parent they can add to
else {
if ($req->user_can('edit_add_child')) {
push @values, -1;
- $labels{-1} = "-- move up a level -- become a section";
+ $labels{-1} = $req->catmsg("msg:bse/admin/edit/uplabelsect");
}
}
}
my @children;
$article->{id} or return;
- if (UNIVERSAL::isa($article, 'Article')) {
+ if (UNIVERSAL::isa($article, 'BSE::TB::Article')) {
@children = $article->children;
}
elsif ($article->{id}) {
sub tag_templates {
my ($self, $article, $cfg, $cgi) = @_;
- my @templates = sort $self->templates($article);
+ my @templates = sort { $a->{name} cmp $b->{name} } $self->templates_long($article);
my $default;
- if ($article->{template} && grep $_ eq $article->{template}, @templates) {
+ if ($article->{template} && grep $_->{name} eq $article->{template}, @templates) {
$default = $article->{template};
}
else {
- my @options;
- $default = $self->default_template($article, $cfg, \@templates);
+ my @template_names = map $_->{name}, @templates;
+ $default = $self->default_template($article, $cfg, \@template_names);
}
- return popup_menu(-name=>'template',
- -values=>\@templates,
- -default=>$default,
- -override=>1);
+ my %labels =
+ (
+ map
+ { ;
+ $_->{name} =>
+ $_->{name} eq $_->{description}
+ ? $_->{name}
+ : "$_->{description} ($_->{name})"
+ } @templates
+ );
+ return popup_menu(-name => 'template',
+ -values => [ map $_->{name}, @templates ],
+ -labels => \%labels,
+ -default => $default,
+ -override => 1);
}
sub title_images {
my ($self, $article) = @_;
my @title_images;
- my $imagedir = cfg_image_dir($self->{cfg});
+ my $imagedir = cfg_image_dir($self->cfg);
if (opendir TITLE_IMAGES, "$imagedir/titles") {
@title_images = sort
grep -f "$imagedir/titles/$_" && /\.(gif|jpeg|jpg|png)$/i,
my @dirs = $self->base_template_dirs;
if (my $parentid = $article->{parentid}) {
my $section = "children of $parentid";
- if (my $dirs = $self->{cfg}->entry($section, 'template_dirs')) {
+ if (my $dirs = $self->cfg->entry($section, 'template_dirs')) {
push @dirs, split /,/, $dirs;
}
}
@templates;
}
+sub categories {
+ my ($self, $articles) = @_;
+
+ return $articles->categories;
+}
+
sub edit_parent {
my ($article) = @_;
sub _load_step_kids {
my ($article, $step_kids) = @_;
- my @stepkids = OtherParents->getBy(parentId=>$article->{id}) if $article->{id};
+ require BSE::TB::OtherParents;
+ my @stepkids = BSE::TB::OtherParents->getBy(parentId=>$article->{id}) if $article->{id};
%$step_kids = map { $_->{childId} => $_ } @stepkids;
$step_kids->{loaded} = 1;
}
return unless $article->{id} && $article->{id} > 0;
- OtherParents->getBy(childId=>$article->{id});
+ require BSE::TB::OtherParents;
+ BSE::TB::OtherParents->getBy(childId=>$article->{id});
}
sub tag_ifStepParents {
$urladd = '' unless defined $urladd;
my $cgi_uri = $self->{cfg}->entry('uri', 'cgi', '/cgi-bin');
- my $images_uri = $self->{cfg}->entry('uri', 'images', '/images');
+ my $images_uri = cfg_dist_image_uri();
my $html = '';
my $url = $ENV{SCRIPT_NAME} . "?id=$article->{id}";
if ($cgi->param('_t')) {
$urladd = '' unless defined $urladd;
my $cgi_uri = $self->{cfg}->entry('uri', 'cgi', '/cgi-bin');
- my $images_uri = $self->{cfg}->entry('uri', 'images', '/images');
+ my $images_uri = cfg_dist_image_uri();
my $urlbase = admin_base_url($req->cfg);
my $refresh_url = "$urlbase$ENV{SCRIPT_NAME}?id=$article->{id}";
my $t = $req->cgi->param('_t');
return make_arrows($req->cfg, $down_url, $up_url, $refresh_url, $img_prefix);
}
+sub tag_category {
+ my ($self, $articles, $article) = @_;
+
+ my @cats = $self->categories($articles);
+
+ my %labels = map { $_->{id}, $_->{name} } @cats;
+
+ return popup_menu(-name => 'category',
+ -values => [ map $_->{id}, @cats ],
+ -labels => \%labels,
+ -default => $article->{category});
+}
+
sub tag_edit_link {
my ($cfg, $article, $args, $acts, $funcname, $templater) = @_;
my ($which, $name) = split / /, $args, 2;
$url .= $urladd;
my $image = $images->[$$rindex];
- my $down_url;
+ my $csrfp = $req->get_csrf_token("admin_move_image");
+ my $baseurl = "$ENV{SCRIPT_NAME}?id=$article->{id}&imageid=$image->{id}&";
+ $baseurl .= "_csrfp=$csrfp&";
+ my $down_url = "";
if ($$rindex < $#$images) {
- $down_url = "$ENV{SCRIPT_NAME}?id=$article->{id}&moveimgdown=1&imageid=$image->{id}";
+ $down_url = $baseurl . "moveimgdown=1";
}
- my $up_url = '';
+ my $up_url = "";
if ($$rindex > 0) {
- $up_url = "$ENV{SCRIPT_NAME}?id=$article->{id}&moveimgup=1&imageid=$image->{id}";
+ $up_url = $baseurl . "moveimgup=1";
}
return make_arrows($req->cfg, $down_url, $up_url, $url, $img_prefix);
}
}
my $down_url = "";
+ my $csrfp = $req->get_csrf_token("admin_move_file");
+ my $baseurl = "$ENV{SCRIPT_NAME}?fileswap=1&id=$article->{id}&";
+ $baseurl .= "_csrfp=$csrfp&";
if ($$rindex < $#$files) {
- $down_url = "$ENV{SCRIPT_NAME}?fileswap=1&id=$article->{id}&file1=$files->[$$rindex]{id}&file2=$files->[$$rindex+1]{id}";
+ $down_url = $baseurl . "file1=$files->[$$rindex]{id}&file2=$files->[$$rindex+1]{id}";
}
my $up_url = "";
if ($$rindex > 0) {
- $up_url = "$ENV{SCRIPT_NAME}?fileswap=1&id=$article->{id}&file1=$files->[$$rindex]{id}&file2=$files->[$$rindex-1]{id}";
+ $up_url = $baseurl . "file1=$files->[$$rindex]{id}&file2=$files->[$$rindex-1]{id}";
}
return make_arrows($req->cfg, $down_url, $up_url, $url, $img_prefix);
my $filename = "$imagedir/$$current_image->{image}";
-e $filename or return "** image file missing **";
- my $geometry = $cfg->entry('thumb geometries', $args, 'scale(200x200)');
+ defined $args && $args =~ /\S/
+ or $args = "editor";
my $image = $$current_image;
- my ($width, $height) = $thumbs_obj->thumb_dimensions_sized
- ($geometry, @$image{qw/width height/});
+ return $image->thumb
+ (
+ geo => $args,
+ cfg => $cfg,
+ nolink => 1,
+ );
+}
+
+sub tag_file_display {
+ my ($self, $files, $file_index) = @_;
+
+ $$file_index >= 0 && $$file_index < @$files
+ or return "* file_display only usable inside a files iterator *";
+ my $file = $files->[$$file_index];
+
+ my $disp_type = $self->cfg->entry("editor", "file_display", "");
+
+ return $file->inline
+ (
+ cfg => $self->cfg,
+ field => $disp_type,
+ );
+}
+
+sub tag_image {
+ my ($self, $cfg, $rcurrent, $args) = @_;
+
+ my $im = $$rcurrent
+ or return '';
+
+ my ($align, $rest) = split ' ', $args, 2;
+
+ if ($align && exists $im->{$align}) {
+ if ($align eq 'src') {
+ return escape_html($im->image_url($self->{cfg}));
+ }
+ else {
+ return escape_html($im->{$align});
+ }
+ }
+ else {
+ return $im->formatted
+ (
+ cfg => $cfg,
+ align => $align,
+ extras => $rest,
+ );
+ }
+}
+
+sub iter_tags {
+ my ($self, $article) = @_;
+
+ $article->{id}
+ or return;
+
+ return $article->tag_objects;
+}
+
+my %base_custom_validation =
+ (
+ customDate1 =>
+ {
+ rules => "date",
+ htmltype => "text",
+ width => 10,
+ default => "",
+ type => "date",
+ },
+ customDate2 =>
+ {
+ rules => "date",
+ htmltype => "text",
+ width => 10,
+ default => "",
+ type => "date",
+ },
+ customStr1 =>
+ {
+ htmltype => "text",
+ default => "",
+ },
+ customStr2 =>
+ {
+ htmltype => "text",
+ default => "",
+ },
+ customInt1 =>
+ {
+ rules => "integer",
+ htmltype => "text",
+ width => 10,
+ default => "",
+ },
+ customInt2 =>
+ {
+ rules => "integer",
+ htmltype => "text",
+ width => 10,
+ default => "",
+ },
+ customInt3 =>
+ {
+ rules => "integer",
+ htmltype => "text",
+ width => 10,
+ default => "",
+ },
+ customInt4 =>
+ {
+ rules => "integer",
+ htmltype => "text",
+ width => 10,
+ default => "",
+ },
+ );
+
+sub custom_fields {
+ my $self = shift;
+
+ require DevHelp::Validate;
+ DevHelp::Validate->import;
+ return DevHelp::Validate::dh_configure_fields
+ (
+ \%base_custom_validation,
+ $self->cfg,
+ ARTICLE_CUSTOM_FIELDS_CFG,
+ BSE::DB->single->dbh,
+ );
+}
+sub _custom_fields {
+ my $self = shift;
- my ($uri, $alt);
- $alt = "thumbnail of ".$$current_image->{alt};
- $uri = "$ENV{SCRIPT_NAME}?a_thumb=1&id=$$current_image->{articleId}&im=$$current_image->{id}&w=$width&h=$height";
+ my $fields = $self->custom_fields;
+ my %active;
+ for my $key (keys %$fields) {
+ $fields->{$key}{description}
+ and $active{$key} = $fields->{$key};
+ }
- $alt = escape_html($alt);
- $uri = escape_html($uri);
- return qq!<img src="$uri" width="$width" height="$height" alt="$alt" border="0" />!;
+ return \%active;
}
+=back
+
+=head1 Common Edit Page Tags
+
+Variables:
+
+=over
+
+=item *
+
+C<article> - the article being edited. This is a dummy article when a
+new article is being created.
+
+=item *
+
+C<isnew> - true if a new article is being created.
+
+=item *
+
+C<custom> - describes custom tags.
+
+=item *
+
+C<errors> - errors from the last submission of the page.
+
+=item *
+
+C<image_stores> - a function returning an array of possible image
+storages.
+
+=item *
+
+C<thumbs> - for the image list, whether thumbs should be displayed
+instead of full size images.
+
+=item *
+
+C<can_thumbs> - true if thumbnails are available.
+
+=back
+
+=cut
+
sub low_edit_tags {
my ($self, $acts, $request, $article, $articles, $msg, $errors) = @_;
my $cgi = $request->cgi;
my $show_full = $cgi->param('f_showfull');
- $msg ||= join "\n", map escape_html($_), $cgi->param('message'), $cgi->param('m');
- $msg ||= $request->message($errors);
+ my $if_error = $msg || ($errors && keys %$errors) || $request->cgi->param("_e");
+ #$msg ||= join "\n", map escape_html($_), $cgi->param('message'), $cgi->param('m');
+ $msg .= $request->message($errors);
my $parent;
if ($article->{id}) {
if ($article->{parentid} > 0) {
else {
$parent = { title=>"How did we get here?", id=>0 };
}
+ $request->set_article(article => $article);
+ $request->set_variable(ifnew => !$article->{id});
my $cfg = $self->{cfg};
my $mbcs = $cfg->entry('html', 'mbcs', 0);
my $tag_hash = $mbcs ? \&tag_hash_mbcs : \&tag_hash;
my @groups;
my $current_group;
my $it = BSE::Util::Iterate->new;
+ my $ito = BSE::Util::Iterate::Objects->new;
+ my $ita = BSE::Util::Iterate::Article->new(req => $request);
+
+ my $custom = $self->_custom_fields;
+ # only return the fields that are defined
+ $request->set_variable(custom => $custom);
+ $request->set_variable(errors => $errors || {});
+ my $article_type = $cfg->entry('level names', $article->{level}, 'Article');
+ $request->set_variable(article_type => $article_type);
+ $request->set_variable(thumbs => defined $thumbs_obj);
+ $request->set_variable(can_thumbs => defined $thumbs_obj_real);
+ $request->set_variable(image_stores =>
+ sub {
+ $self->iter_image_stores;
+ });
+
return
(
$request->admin_tags,
- article => [ $tag_hash, $article ],
+ article => sub { tag_article($article, $cfg, $_[0]) },
old => [ \&tag_old, $article, $cgi ],
default => [ \&tag_default, $self, $request, $article ],
- articleType => [ \&tag_art_type, $article->{level}, $cfg ],
+ articleType => escape_html($article_type),
parentType => [ \&tag_art_type, $article->{level}-1, $cfg ],
ifNew => [ \&tag_if_new, $article ],
list => [ \&tag_list, $self, $article, $articles, $cgi, $request ],
$it->make_iterator
([ \&iter_get_images, $self, $article ], 'image', 'images', \@images,
\$image_index, undef, \$current_image),
+ image => [ tag_image => $self, $cfg, \$current_image ],
thumbimage => [ \&tag_thumbimage, $cfg, $thumbs_obj, \$current_image ],
ifThumbs => defined($thumbs_obj),
ifCanThumbs => defined($thumbs_obj_real),
imgmove => [ \&tag_imgmove, $request, $article, \$image_index, \@images ],
message => $msg,
- DevHelp::Tags->make_iterator2
- ([ \&iter_get_kids, $article, $articles ],
- 'child', 'children', \@children, \$child_index),
+ ifError => $if_error,
+ $ita->make
+ (
+ code => [ \&iter_get_kids, $article, $articles ],
+ single => 'child',
+ plural => 'children',
+ data => \@children,
+ index => \$child_index,
+ ),
ifchildren => \&tag_if_children,
childtype => [ \&tag_art_type, $article->{level}+1, $cfg ],
ifHaveChildType => [ \&tag_if_have_child_type, $article->{level}, $cfg ],
templates => [ \&tag_templates, $self, $article, $cfg, $cgi ],
titleImages => [ \&tag_title_images, $self, $article, $cfg, $cgi ],
editParent => [ \&tag_edit_parent, $article ],
- DevHelp::Tags->make_iterator2
- ([ \&iter_allkids, $article ], 'kid', 'kids', \@allkids, \$allkid_index),
+ $ita->make
+ (
+ code => [ \&iter_allkids, $article ],
+ single => 'kid',
+ plural => 'kids',
+ data => \@allkids,
+ index => \$allkid_index,
+ ),
ifStepKid =>
[ \&tag_if_step_kid, $article, \@allkids, \$allkid_index, \%stepkids ],
stepkid => [ \&tag_step_kid, $article, \@allkids, \$allkid_index,
ifPossibles =>
[ \&tag_if_possible_stepkids, \%stepkids, $request, $article,
\@possstepkids, $articles, $cgi ],
- DevHelp::Tags->make_iterator2
- ( [ \&iter_get_stepparents, $article ], 'stepparent', 'stepparents',
- \@stepparents, \$stepparent_index),
+ $ita->make
+ (
+ code => [ \&iter_get_stepparents, $article ],
+ single => 'stepparent',
+ plural => 'stepparents',
+ data => \@stepparents,
+ index => \$stepparent_index,
+ ),
ifStepParents => \&tag_ifStepParents,
stepparent_targ =>
[ \&tag_stepparent_targ, $article, \@stepparent_targs,
stepparent_possibles =>
[ \&tag_stepparent_possibles, $cgi, $request, $article, $articles,
\@stepparent_targs, \@stepparentpossibles, ],
- DevHelp::Tags->make_iterator2
- ([ iter_files => $self, $article ], 'file', 'files', \@files, \$file_index ),
+ $ito->make
+ (
+ code => [ iter_files => $self, $article ],
+ single => 'file',
+ plural => 'files',
+ data => \@files,
+ index => \$file_index,
+ ),
movefiles =>
[ \&tag_movefiles, $self, $request, $article, \@files, \$file_index ],
+ $it->make
+ (
+ code => [ iter_file_metas => $self, \@files, \$file_index ],
+ plural => "file_metas",
+ single => "file_meta",
+ nocache => 1,
+ ),
+ ifFileExists => sub {
+ @files && $file_index >= 0 && $file_index < @files
+ or return 0;
+
+ return -f ($files[$file_index]->full_filename($cfg));
+ },
+ file_display => [ tag_file_display => $self, \@files, \$file_index ],
DevHelp::Tags->make_iterator2
(\&iter_admin_users, 'iadminuser', 'adminusers'),
DevHelp::Tags->make_iterator2
error => [ $tag_hash, $errors ],
error_img => [ \&tag_error_img, $cfg, $errors ],
ifFieldPerm => [ \&tag_if_field_perm, $request, $article ],
- parent => [ $tag_hash, $parent ],
+ parent => [ \&tag_article, $parent, $cfg ],
DevHelp::Tags->make_iterator2
([ \&iter_flags, $self ], 'flag', 'flags' ),
ifFlagSet => [ \&tag_if_flag_set, $article ],
$it->make_iterator([ iter_file_stores => $self],
'file_store', 'file_stores'),
ifGroupRequired => [ \&tag_ifGroupRequired, $article, \$current_group ],
+ category => [ tag_category => $self, $articles, $article ],
+ $ito->make
+ (
+ single => "tag",
+ plural => "tags",
+ code => [ iter_tags => $self, $article ],
+ ),
);
}
sub _file_manager {
my ($self) = @_;
- require BSE::StorageMgr::Files;
+ require BSE::TB::ArticleFiles;
- return BSE::StorageMgr::Files->new(cfg => $self->cfg);
+ return BSE::TB::ArticleFiles->file_manager($self->cfg);
}
sub iter_file_stores {
my ($self) = @_;
- my $mgr = $self->_file_manager;
+ require BSE::TB::ArticleFiles;
+ my $mgr = $self->_file_manager($self->cfg);
return map +{ name => $_->name, description => $_->description },
$mgr->all_stores;
sub tag_ifGroupRequired {
my ($article, $rgroup) = @_;
+ $article->{id}
+ or return 0;
+
$$rgroup or return 0;
$article->is_accessible_to($$rgroup);
return $self->low_edit_form($request, $article, $articles, $msg, $errors);
}
-sub add_form {
- my ($self, $req, $articles, $msg, $errors) = @_;
+sub _dummy_article {
+ my ($self, $req, $articles, $rmsg) = @_;
my $level;
my $cgi = $req->cgi;
}
my %article;
- my @cols = Article->columns;
+ my @cols = BSE::TB::Article->columns;
@article{@cols} = ('') x @cols;
$article{id} = '';
$article{parentid} = $parentid;
my ($values, $labels) = $self->possible_parents(\%article, $articles, $req);
unless (@$values) {
- require BSE::Edit::Site;
- my $site = BSE::Edit::Site->new(cfg=>$req->cfg, db=> BSE::DB->single);
- return $site->edit_sections($req, $articles,
- "You can't add children to any article at that level");
+ $$rmsg = "You can't add children to any article at that level";
+ return;
}
- return $self->low_edit_form($req, \%article, $articles, $msg, $errors);
+ return $self->_make_dummy_article(\%article);
+}
+
+sub _make_dummy_article {
+ my ($self, $article) = @_;
+
+ require BSE::DummyArticle;
+ return bless $article, "BSE::DummyArticle";
+}
+
+sub add_form {
+ my ($self, $req, $article, $articles, $msg, $errors) = @_;
+
+ return $self->low_edit_form($req, $article, $articles, $msg, $errors);
}
-sub generator { 'Generate::Article' }
+sub generator { 'BSE::Generate::Article' }
sub typename {
my ($self) = @_;
$errors->{linkAlias} = "Link alias must contain only alphanumerics and contain at least one letter";
}
}
+
+ if (defined $data->{category}) {
+ unless (first { $_->{id} eq $data->{category} } $self->categories($articles)) {
+ $errors->{category} = "msg:bse/admin/edit/category/unknown";
+ }
+ }
+
+ require DevHelp::Validate;
+ DevHelp::Validate->import('dh_validate_hash');
+ dh_validate_hash($data, $errors,
+ {
+ fields => $self->_custom_fields,
+ optional => 1,
+ dbh => BSE::DB->single->dbh,
+ },
+ $self->cfg, ARTICLE_CUSTOM_FIELDS_CFG);
}
sub validate {
}
sub validate_old {
- my ($self, $article, $data, $articles, $errors) = @_;
+ my ($self, $article, $data, $articles, $errors, $ajax) = @_;
$self->_validate_common($data, $articles, $errors, $article);
custom_class($self->{cfg})
->article_validate($data, $article, $self->typename, $errors);
- if (exists $data->{release} && !valid_date($data->{release})) {
- $errors->{release} = "Invalid release date";
+ if (exists $data->{release}) {
+ if ($ajax && !dh_parse_sql_date($data->{release})
+ || !$ajax && !dh_parse_date($data->{release})) {
+ $errors->{release} = "Invalid release date";
+ }
}
if (!$errors->{linkAlias}
sub fill_new_data {
my ($self, $req, $data, $articles) = @_;
+ my $custom = $self->_custom_fields;
+ for my $key (keys %$custom) {
+ my ($value) = $req->cgi->param($key);
+ if (defined $value) {
+ if ($key =~ /^customDate/) {
+ require DevHelp::Date;
+ my $msg;
+ if (my ($year, $month, $day) =
+ DevHelp::Date::dh_parse_date($value, \$msg)) {
+ $data->{$key} = sprintf("%04d-%02d-%02d", $year, $month, $day);
+ }
+ else {
+ $data->{$key} = undef;
+ }
+ }
+ elsif ($key =~ /^customInt/) {
+ if ($value =~ /\S/) {
+ $data->{$key} = $value;
+ }
+ else {
+ $data->{$key} = undef;
+ }
+ }
+ else {
+ $data->{$key} = $value;
+ }
+ }
+ }
+
custom_class($self->{cfg})
->article_fill_new($data, $self->typename);
sub make_link {
my ($self, $article) = @_;
+ $article->is_linked
+ or return "";
+
+ my $title = $article->title;
if ($article->is_dynamic) {
- return "/cgi-bin/page.pl?page=$article->{id}&title=".escape_uri($article->{title});
+ (my $extra = $title) =~ tr/A-Za-z0-9/-/sc;
+ return "/cgi-bin/page.pl?page=$article->{id}&title=".escape_uri($extra);
}
my $article_uri = $self->link_path($article);
my $link = "$article_uri/$article->{id}.html";
my $link_titles = $self->{cfg}->entryBool('basic', 'link_titles', 0);
if ($link_titles) {
- (my $extra = lc $article->{title}) =~ tr/a-z0-9/_/sc;
+ (my $extra = $title) =~ tr/A-Za-z0-9/-/sc;
$link .= "/" . $extra . "_html";
}
$link;
}
+sub save_columns {
+ my ($self, $table_object) = @_;
+
+ my @columns = $table_object->rowClass->columns;
+ shift @columns;
+
+ return @columns;
+}
+
+sub _validate_tags {
+ my ($self, $tags, $errors) = @_;
+
+ my $fail = 0;
+ my @errors;
+ for my $tag (@$tags) {
+ my $error;
+ if ($tag =~ /\S/
+ && !BSE::TB::Tags->valid_name($tag, \$error)) {
+ push @errors, "msg:bse/admin/edit/tags/invalid/$error";
+ $errors->{tags} = \@errors;
+ ++$fail;
+ }
+ else {
+ push @errors, undef;
+ }
+ }
+
+ return $fail;
+}
+
sub save_new {
- my ($self, $req, $articles) = @_;
+ my ($self, $req, $article, $articles) = @_;
+
+ $req->check_csrf("admin_add_article")
+ or return $self->csrf_error($req, undef, "admin_add_article", "Add Article");
my $cgi = $req->cgi;
my %data;
my $table_object = $self->table_object($articles);
- my @columns = $table_object->rowClass->columns;
+ my @columns = $self->save_columns($table_object);
$self->save_thumbnail($cgi, undef, \%data);
for my $name (@columns) {
$data{$name} = $cgi->param($name)
elsif ($data{parentid} !~ /^(?:-1|\d+)$/) {
$errors{parentid} = "Invalid parent selection (template bug)";
}
- $self->validate(\%data, $articles, \%errors)
- or return $self->add_form($req, $articles, $msg, \%errors);
+ $self->validate(\%data, $articles, \%errors);
+
+ my $save_tags = $cgi->param("_save_tags");
+ my @tags;
+ if ($save_tags) {
+ @tags = $cgi->param("tags");
+ $self->_validate_tags(\@tags, \%errors);
+ }
+
+ my $meta;
+ if ($cgi->param("_save_meta")) {
+ require BSE::ArticleMetaMeta;
+ $meta = BSE::ArticleMetaMeta->retrieve($req, $article, \%errors);
+ }
+
+ if (keys %errors) {
+ if ($req->is_ajax) {
+ return $req->json_content
+ (
+ success => 0,
+ errors => \%errors,
+ error_code => "FIELD",
+ message => $req->message(\%errors),
+ );
+ }
+ else {
+ return $self->add_form($req, $article, $articles, $msg, \%errors);
+ }
+ }
my $parent;
+ my $parent_msg;
+ my $parent_code;
if ($data{parentid} > 0) {
$parent = $articles->getByPkey($data{parentid}) or die;
- $req->user_can('edit_add_child', $parent)
- or return $self->add_form($req, $articles,
- "You cannot add a child to that article");
- for my $name (@columns) {
- if (exists $data{$name} &&
- !$req->user_can("edit_add_field_$name", $parent)) {
- delete $data{$name};
+ if ($req->user_can('edit_add_child', $parent)) {
+ for my $name (@columns) {
+ if (exists $data{$name} &&
+ !$req->user_can("edit_add_field_$name", $parent)) {
+ delete $data{$name};
+ }
}
}
+ else {
+ $parent_msg = "You cannot add a child to that article";
+ $parent_code = "ACCESS";
+ }
}
else {
- $req->user_can('edit_add_child')
- or return $self->add_form($req, $articles,
- "You cannot create a top-level article");
- for my $name (@columns) {
- if (exists $data{$name} &&
- !$req->user_can("edit_add_field_$name")) {
- delete $data{$name};
+ if ($req->user_can('edit_add_child')) {
+ for my $name (@columns) {
+ if (exists $data{$name} &&
+ !$req->user_can("edit_add_field_$name")) {
+ delete $data{$name};
+ }
}
}
+ else {
+ $parent_msg = "You cannot create a top-level article";
+ $parent_code = "ACCESS";
+ }
+ }
+ if (!$parent_msg) {
+ $self->validate_parent(\%data, $articles, $parent, \$parent_msg)
+ or $parent_code = "PARENT";
+ }
+ if ($parent_msg) {
+ if ($req->is_ajax) {
+ return $req->json_content
+ (
+ success => 0,
+ message => $parent_msg,
+ error_code => $parent_code,
+ errors => {},
+ );
+ }
+ else {
+ return $self->add_form($req, $article, $articles, $parent_msg);
+ }
}
-
- $self->validate_parent(\%data, $articles, $parent, \$msg)
- or return $self->add_form($req, $articles, $msg);
my $level = $parent ? $parent->{level}+1 : 1;
$data{level} = $level;
# end adrian
$self->fill_new_data($req, \%data, $articles);
- for my $col (qw(titleImage imagePos template keyword menu titleAlias linkAlias)) {
+ for my $col (qw(titleImage imagePos template keyword menu titleAlias linkAlias body author summary category)) {
defined $data{$col}
or $data{$col} = $self->default_value($req, \%data, $col);
}
}
}
- for my $col (qw(release expire)) {
- $data{$col} = sql_date($data{$col});
+ unless ($req->is_ajax) {
+ for my $col (qw(release expire)) {
+ $data{$col} = sql_date($data{$col});
+ }
}
# these columns are handled a little differently
or $data{$col} = $self->default_value($req, \%data, $col);
}
- shift @columns;
- my $article = $table_object->add(@data{@columns});
+ my @cols = $table_object->rowClass->columns;
+ shift @cols;
+
+ # fill out anything else from defaults
+ for my $col (@columns) {
+ exists $data{$col}
+ or $data{$col} = $self->default_value($req, \%data, $col);
+ }
+
+ $article = $table_object->add(@data{@cols});
+
+ $self->save_new_more($req, $article, \%data);
# we now have an id - generate the links
$article->setLink($self->make_link($article));
$article->save();
- use Util 'generate_article';
+ my ($after_id) = $cgi->param("_after");
+ if (defined $after_id) {
+ BSE::TB::Articles->reorder_child($article->{parentid}, $article->{id}, $after_id);
+ # reload, the displayOrder probably changed
+ $article = $articles->getByPkey($article->{id});
+ }
+
+ if ($save_tags) {
+ my $error;
+ $article->set_tags([ grep /\S/, @tags ], \$error);
+ }
+
+ if ($meta) {
+ BSE::ArticleMetaMeta->save($article, $meta);
+ }
+
generate_article($articles, $article) if $Constants::AUTO_GENERATE;
+ if ($req->is_ajax) {
+ return $req->json_content
+ (
+ {
+ success => 1,
+ article => $self->_article_data($req, $article),
+ },
+ );
+ }
+
my $r = $cgi->param('r');
if ($r) {
$r .= ($r =~ /\?/) ? '&' : '?';
$r .= "id=$article->{id}";
}
else {
-
$r = admin_base_url($req->cfg) . $article->{admin};
}
return BSE::Template->get_refresh($r, $self->{cfg});
-
}
sub fill_old_data {
$data->{body} =~ s/\x0D\x0A/\n/g;
$data->{body} =~ tr/\r/\n/;
}
- for my $col (Article->columns) {
+ for my $col (BSE::TB::Article->columns) {
next if $col =~ /^custom/;
$article->{$col} = $data->{$col}
if exists $data->{$col} && $col ne 'id' && $col ne 'parentid';
}
+ my $custom = $self->_custom_fields;
+ for my $key (keys %$custom) {
+ if (exists $data->{$key}) {
+ if ($key =~ /^customDate/) {
+ require DevHelp::Date;
+ my $msg;
+ if (my ($year, $month, $day) =
+ DevHelp::Date::dh_parse_date($data->{$key}, \$msg)) {
+ $article->set($key, sprintf("%04d-%02d-%02d", $year, $month, $day));
+ }
+ else {
+ $article->set($key => undef);
+ }
+ }
+ elsif ($key =~ /^customInt/) {
+ if ($data->{$key} =~ /\S/) {
+ $article->set($key => $data->{$key});
+ }
+ else {
+ $article->set($key => undef);
+ }
+ }
+ else {
+ $article->set($key => $data->{$key});
+ }
+ }
+ }
custom_class($self->{cfg})
->article_fill_old($article, $data, $self->typename);
return 1;
}
+sub _article_data {
+ my ($self, $req, $article) = @_;
+
+ my $article_data = $article->data_only;
+ $article_data->{link} = $article->link($req->cfg);
+ $article_data->{images} =
+ [
+ map $self->_image_data($req->cfg, $_), $article->images
+ ];
+ $article_data->{files} =
+ [
+ map $_->data_only, $article->files,
+ ];
+ $article_data->{tags} =
+ [
+ $article->tags, # just the names
+ ];
+
+ return $article_data;
+}
+
+sub save_more {
+ my ($self, $req, $article, $data) = @_;
+ # nothing to do here
+}
+
+sub save_new_more {
+ my ($self, $req, $article, $data) = @_;
+ # nothing to do here
+}
+
+=over
+
+=item save
+
+Error codes:
+
+=over
+
+=item *
+
+ACCESS - user doesn't have access to this article.
+
+=item *
+
+LASTMOD - lastModified value doesn't match that in the article
+
+=item *
+
+PARENT - invalid parentid specified
+
+=back
+
+=cut
+
sub save {
my ($self, $req, $article, $articles) = @_;
+ $req->check_csrf("admin_save_article")
+ or return $self->csrf_error($req, $article, "admin_save_article", "Save Article");
+
$req->user_can(edit_save => $article)
- or return $self->edit_form($req, $article, $articles,
- "You don't have access to save this article");
+ or return $self->_service_error
+ ($req, $article, $articles, "You don't have access to save this article",
+ {}, "ACCESS");
my $old_dynamic = $article->is_dynamic;
my $cgi = $req->cgi;
my %data;
- for my $name ($article->columns) {
+ my $table_object = $self->table_object($articles);
+ my @save_cols = $self->save_columns($table_object);
+ for my $name (@save_cols) {
$data{$name} = $cgi->param($name)
if defined($cgi->param($name)) and $name ne 'id' && $name ne 'parentid'
&& $req->user_can("edit_field_edit_$name", $article);
}
print STDERR "non-matching lastModified, article not saved\n";
my $msg = "Article changes not saved, this article was modified $whoModified at $timeModified since this editor was loaded";
- return $self->edit_form($req, $article, $articles, $msg);
+ return $self->_service_error($req, $article, $articles, $msg, {}, "LASTMOD");
}
}
# end adrian
$data{flags} = join '', sort $cgi->param('flags')
if $req->user_can("edit_field_edit_flags", $article);
my %errors;
- $self->validate_old($article, \%data, $articles, \%errors)
- or return $self->edit_form($req, $article, $articles, undef, \%errors);
- $self->save_thumbnail($cgi, $article, \%data)
- if $req->user_can('edit_field_edit_thumbImage', $article);
- $self->fill_old_data($req, $article, \%data);
if (exists $article->{template} &&
$article->{template} =~ m|\.\.|) {
- my $msg = "Please only select templates from the list provided";
- return $self->edit_form($req, $article, $articles, $msg);
+ $errors{template} = "Please only select templates from the list provided";
}
-
- # reparenting
- my $newparentid = $cgi->param('parentid');
- if ($newparentid && $req->user_can('edit_field_edit_parentid', $article)) {
- if ($newparentid == $article->{parentid}) {
- # nothing to do
- }
- elsif ($newparentid != -1) {
- print STDERR "Reparenting...\n";
- my $newparent = $articles->getByPkey($newparentid);
- if ($newparent) {
- if ($newparent->{level} != $article->{level}-1) {
- # the article cannot become a child of itself or one of it's
- # children
- if ($article->{id} == $newparentid
- || $self->is_descendant($article->{id}, $newparentid, $articles)) {
- my $msg = "Cannot become a child of itself or of a descendant";
- return $self->edit_form($req, $article, $articles, $msg);
- }
- my $shopid = $self->{cfg}->entryErr('articles', 'shop');
- if ($self->is_descendant($article->{id}, $shopid, $articles)) {
- my $msg = "Cannot become a descendant of the shop";
- return $self->edit_form($req, $article, $articles, $msg);
- }
- my $msg;
- $self->reparent($article, $newparentid, $articles, \$msg)
- or return $self->edit_form($req, $article, $articles, $msg);
- }
- else {
- # stays at the same level, nothing special
- $article->{parentid} = $newparentid;
- }
- }
- # else ignore it
+
+ my $meta;
+ if ($cgi->param("_save_meta")) {
+ require BSE::ArticleMetaMeta;
+ $meta = BSE::ArticleMetaMeta->retrieve($req, $article, \%errors);
+ }
+
+ my $save_tags = $cgi->param("_save_tags");
+ my @tags;
+ if ($save_tags) {
+ @tags = $cgi->param("tags");
+ $self->_validate_tags(\@tags, \%errors);
+ }
+ $self->validate_old($article, \%data, $articles, \%errors, scalar $req->is_ajax)
+ or return $self->_service_error($req, $article, $articles, undef, \%errors, "FIELD");
+ $self->save_thumbnail($cgi, $article, \%data)
+ if $req->user_can('edit_field_edit_thumbImage', $article);
+ if (exists $data{flags} && $data{flags} =~ /D/) {
+ $article->remove_html;
+ }
+ $self->fill_old_data($req, $article, \%data);
+
+ # reparenting
+ my $newparentid = $cgi->param('parentid');
+ if ($newparentid
+ && $req->user_can('edit_field_edit_parentid', $article)
+ && $newparentid != $article->{parentid}) {
+ my $newparent;
+ my $parent_editor;
+ if ($newparentid == -1) {
+ require BSE::Edit::Site;
+ $newparent = BSE::TB::Site->new;
+ $parent_editor = BSE::Edit::Site->new(cfg => $req->cfg);
}
else {
- # becoming a section
+ $newparent = $articles->getByPkey($newparentid);
+ ($parent_editor, $newparent) = $self->article_class($newparent, $articles, $req->cfg);
+ }
+ if ($newparent) {
my $msg;
- $self->reparent($article, -1, $articles, \$msg)
- or return $self->edit_form($req, $article, $articles, $msg);
+ if ($self->can_reparent_to($article, $newparent, $parent_editor, $articles, \$msg)
+ && $self->reparent($article, $newparentid, $articles, \$msg)) {
+ # nothing to do here
+ }
+ else {
+ return $self->_service_error($req, $article, $articles, $msg, {}, "PARENT");
+ }
+ }
+ else {
+ return $self->_service_error($req, $article, $articles, "No such parent article", {}, "PARENT");
}
}
$article->{listed} = $cgi->param('listed')
if defined $cgi->param('listed') &&
$req->user_can('edit_field_edit_listed', $article);
- $article->{release} = sql_date($cgi->param('release'))
- if defined $cgi->param('release') &&
- $req->user_can('edit_field_edit_release', $article);
-
+
+ if ($req->user_can('edit_field_edit_release', $article)) {
+ my $release = $cgi->param("release");
+ if (defined $release && $release =~ /\S/) {
+ if ($req->is_ajax) {
+ $article->{release} = $release;
+ }
+ else {
+ $article->{release} = sql_date($release)
+ }
+ }
+ }
+
$article->{expire} = sql_date($cgi->param('expire')) || $Constants::D_99
if defined $cgi->param('expire') &&
$req->user_can('edit_field_edit_expire', $article);
- $article->{lastModified} = now_sqldatetime();
for my $col (qw/force_dynamic inherit_siteuser_rights/) {
if ($req->user_can("edit_field_edit_$col", $article)
&& $cgi->param("save_$col")) {
}
}
-# Added by adrian
- my $user = $req->getuser;
- $article->{lastModifiedBy} = $user ? $user->{logon} : '';
-# end adrian
+ $article->mark_modified(actor => $req->getuser || "U");
my @save_group_ids = $cgi->param('save_group_id');
if ($req->user_can('edit_field_edit_group_id')
my $old_link = $article->{link};
# this need to go last
$article->update_dynamic($self->{cfg});
- if ($article->{link} &&
- !$self->{cfg}->entry('protect link', $article->{id})) {
+ if (!$self->{cfg}->entry('protect link', $article->{id})) {
my $article_uri = $self->make_link($article);
$article->setLink($article_uri);
}
$article->save();
+ if ($save_tags) {
+ my $error;
+ $article->set_tags([ grep /\S/, @tags ], \$error);
+ }
+
+use Data::Dumper;
+print STDERR Dumper($meta);
+ if ($meta) {
+ BSE::ArticleMetaMeta->save($article, $meta);
+ }
+
# fix the kids too
my @extra_regen;
@extra_regen = $self->update_child_dynamic($article, $articles, $req);
}
}
- use Util 'generate_article';
+ my ($after_id) = $cgi->param("_after");
+ if (defined $after_id) {
+ BSE::TB::Articles->reorder_child($article->{parentid}, $article->{id}, $after_id);
+ # reload, the displayOrder probably changed
+ $article = $articles->getByPkey($article->{id});
+ }
+
if ($Constants::AUTO_GENERATE) {
generate_article($articles, $article);
for my $regen_id (@extra_regen) {
my $regen = $articles->getByPkey($regen_id);
- Util::generate_low($articles, $regen, $self->{cfg});
+ BSE::Regen::generate_low($articles, $regen, $self->{cfg});
}
}
+ $self->save_more($req, $article, \%data);
+
+ if ($req->is_ajax) {
+ return $req->json_content
+ (
+ {
+ success => 1,
+ article => $self->_article_data($req, $article),
+ },
+ );
+ }
+
return $self->refresh($article, $cgi, undef, 'Article saved');
}
+sub can_reparent_to {
+ my ($self, $article, $newparent, $parent_editor, $articles, $rmsg) = @_;
+
+ my @child_types = $parent_editor->child_types;
+ if (!grep $_ eq ref $self, @child_types) {
+ my ($child_type) = (ref $self) =~ /(\w+)$/;
+ my ($parent_type) = (ref $parent_editor) =~ /(\w+)$/;
+
+ $$rmsg = "A $child_type cannot be a child of a $parent_type";
+ return;
+ }
+
+ # the article cannot become a child of itself or one of it's
+ # children
+ if ($article->{id} == $newparent->id
+ || $self->is_descendant($article->id, $newparent->id, $articles)) {
+ $$rmsg = "Cannot become a child of itself or of a descendant";
+ return;
+ }
+
+ my $shopid = $self->{cfg}->entryErr('articles', 'shop');
+ if ($self->shop_article) { # if this article belongs in the shop
+ unless ($newparent->id == $shopid
+ || $self->is_descendant($shopid, $newparent->{id}, $articles)) {
+ $$rmsg = "This article belongs in the shop";
+ return;
+ }
+ }
+ else {
+ if ($newparent->id == $shopid
+ || $self->is_descendant($shopid, $newparent->id, $articles)) {
+ $$rmsg = "This article doesn't belong in the shop";
+ return;
+ }
+ }
+
+ return 1;
+}
+
+sub shop_article { 0 }
+
sub update_child_dynamic {
my ($self, $article, $articles, $req) = @_;
unlink("$imagedir/$original->{thumbImage}");
@$newdata{qw/thumbImage thumbWidth thumbHeight/} = ('', 0, 0);
}
- my $image = $cgi->param('thumbnail');
- if ($image && -s $image) {
+ my $image_name = $cgi->param('thumbnail');
+ my $image = $cgi->upload('thumbnail');
+ if ($image_name && -s $image) {
# where to put it...
my $name = '';
- $image =~ /([\w.-]+)$/ and $name = $1;
+ $image_name =~ /([\w.-]+)$/ and $name = $1;
my $filename = time . "_" . $name;
use Fcntl;
close OUTPUT
or die "Could not close image output file: $!";
- use Image::Size;
+ require BSE::ImageSize;
if ($original && $original->{thumbImage}) {
#unlink("$imagedir/$original->{thumbImage}");
}
- @$newdata{qw/thumbWidth thumbHeight/} = imgsize("$imagedir/$filename");
+ @$newdata{qw/thumbWidth thumbHeight/} =
+ BSE::ImageSize::imgsize("$imagedir/$filename");
$newdata->{thumbImage} = $filename;
}
}
return ( 'BSE::Edit::Article' );
}
+=item add_stepkid
+
+Add a step child to an article.
+
+Parameters:
+
+=over
+
+=item *
+
+id - parent article id (required)
+
+=item *
+
+stepkid - child article id (required)
+
+=item *
+
+_after - id of the allkid of id to position the stepkid after
+(optional)
+
+=back
+
+Returns a FIELD error for an invalid stepkid.
+
+Returns an ACCESS error for insufficient access.
+
+Return an ADD error for a general add failure.
+
+On success returns:
+
+ {
+ success: 1,
+ relationship: { childId: I<childid>, parentId: I<parentid> }
+ }
+
+=back
+
+=cut
+
sub add_stepkid {
my ($self, $req, $article, $articles) = @_;
+ $req->check_csrf("admin_add_stepkid")
+ or return $self->csrf_error($req, $article, "admin_add_stepkid", "Add Stepkid");
+
$req->user_can(edit_stepkid_add => $article)
- or return $self->edit_form($req, $article, $articles,
- "You don't have access to add step children to this article");
+ or return $self->_service_error($req, $article, $articles,
+ "You don't have access to add step children to this article", {}, "ACCESS");
my $cgi = $req->cgi;
- require 'BSE/Admin/StepParents.pm';
- eval {
- my $childId = $cgi->param('stepkid');
- defined $childId
- or die "No stepkid supplied to add_stepkid";
+ require BSE::Admin::StepParents;
+
+ my %errors;
+ my $childId = $cgi->param('stepkid');
+ defined $childId
+ or $errors{stepkid} = "No stepkid supplied to add_stepkid";
+ unless ($errors{stepkid}) {
$childId =~ /^\d+$/
- or die "Invalid stepkid supplied to add_stepkid";
- my $child = $articles->getByPkey($childId)
- or die "Article $childId not found";
+ or $errors{stepkid} = "Invalid stepkid supplied to add_stepkid";
+ }
+ my $child;
+ unless ($errors{stepkid}) {
+ $child = $articles->getByPkey($childId)
+ or $errors{stepkid} = "Article $childId not found";
+ }
+ keys %errors
+ and return $self->_service_error
+ ($req, $article, $articles, $errors{stepkid}, \%errors, "FIELD");
+
+ $req->user_can(edit_stepparent_add => $child)
+ or return $self->_service_error($req, $article, $articles, "You don't have access to add a stepparent to that article", {}, "ACCESS");
- $req->user_can(edit_stepparent_add => $child)
- or die "You don't have access to add a stepparent to that article\n";
+ my $new_entry;
+ eval {
- use BSE::Util::Valid qw/valid_date/;
my $release = $cgi->param('release');
- valid_date($release) or $release = undef;
+ dh_parse_date($release) or $release = undef;
my $expire = $cgi->param('expire');
- valid_date($expire) or $expire = undef;
+ dh_parse_date($expire) or $expire = undef;
- my $newentry =
+ $new_entry =
BSE::Admin::StepParents->add($article, $child, $release, $expire);
};
if ($@) {
- return $self->edit_form($req, $article, $articles, $@);
+ return $self->_service_error($req, $article, $articles, $@, {}, "ADD");
+ }
+
+ my $after_id = $cgi->param("_after");
+ if (defined $after_id) {
+ BSE::TB::Articles->reorder_child($article->id, $child->id, $after_id);
}
- use Util 'generate_article';
generate_article($articles, $article) if $Constants::AUTO_GENERATE;
- return $self->refresh($article, $cgi, 'step', 'Stepchild added');
+ if ($req->is_ajax) {
+ return $req->json_content
+ (
+ success => 1,
+ relationship => $new_entry->data_only,
+ );
+ }
+ else {
+ $self->refresh($article, $cgi, 'step', 'Stepchild added');
+ }
}
+=item del_stepkid
+
+Remove a stepkid relationship.
+
+Parameters:
+
+=over
+
+=item *
+
+id - parent article id (required)
+
+=item *
+
+stepkid - child article id (required)
+
+=back
+
+Returns a FIELD error for an invalid stepkid.
+
+Returns an ACCESS error for insufficient access.
+
+Return a DELETE error for a general delete failure.
+
+=cut
+
sub del_stepkid {
my ($self, $req, $article, $articles) = @_;
+ $req->check_csrf("admin_remove_stepkid")
+ or return $self->csrf_error($req, $article, "admin_del_stepkid", "Delete Stepkid");
$req->user_can(edit_stepkid_delete => $article)
- or return $self->edit_form($req, $article, $articles,
- "You don't have access to delete stepchildren from this article");
+ or return $self->_service_error($req, $article, $articles,
+ "You don't have access to delete stepchildren from this article", {}, "ACCESS");
my $cgi = $req->cgi;
- require 'BSE/Admin/StepParents.pm';
- eval {
- my $childId = $cgi->param('stepkid');
- defined $childId
- or die "No stepkid supplied to add_stepkid";
+
+ my %errors;
+ my $childId = $cgi->param('stepkid');
+ defined $childId
+ or $errors{stepkid} = "No stepkid supplied to add_stepkid";
+ unless ($errors{stepkid}) {
$childId =~ /^\d+$/
- or die "Invalid stepkid supplied to add_stepkid";
- my $child = $articles->getByPkey($childId)
- or die "Article $childId not found";
+ or $errors{stepkid} = "Invalid stepkid supplied to add_stepkid";
+ }
+ my $child;
+ unless ($errors{stepkid}) {
+ $child = $articles->getByPkey($childId)
+ or $errors{stepkid} = "Article $childId not found";
+ }
+ keys %errors
+ and return $self->_service_error
+ ($req, $article, $articles, $errors{stepkid}, \%errors, "FIELD");
- $req->user_can(edit_stepparent_delete => $child)
- or die "You cannot remove stepparents from that article\n";
+ $req->user_can(edit_stepparent_delete => $child)
+ or return _service_error($req, $article, $article, "You cannot remove stepparents from that article", {}, "ACCESS");
+
+ require BSE::Admin::StepParents;
+ eval {
BSE::Admin::StepParents->del($article, $child);
};
if ($@) {
- return $self->edit_form($req, $article, $articles, $@);
+ return $self->_service_error($req, $article, $articles, $@, {}, "DELETE");
}
- use Util 'generate_article';
generate_article($articles, $article) if $Constants::AUTO_GENERATE;
- return $self->refresh($article, $cgi, 'step', 'Stepchild deleted');
+ if ($req->is_ajax) {
+ return $req->json_content(success => 1);
+ }
+ else {
+ return $self->refresh($article, $cgi, 'step', 'Stepchild deleted');
+ }
}
sub save_stepkids {
my ($self, $req, $article, $articles) = @_;
+ $req->check_csrf("admin_save_stepkids")
+ or return $self->csrf_error($req, $article, "admin_save_stepkids", "Save Stepkids");
+
$req->user_can(edit_stepkid_save => $article)
or return $self->edit_form($req, $article, $articles,
"No access to save stepkid data for this article");
my $cgi = $req->cgi;
require 'BSE/Admin/StepParents.pm';
- my @stepcats = OtherParents->getBy(parentId=>$article->{id});
+ my @stepcats = BSE::TB::OtherParents->getBy(parentId=>$article->{id});
my %stepcats = map { $_->{parentId}, $_ } @stepcats;
my %datedefs = ( release => '2000-01-01', expire=>'2999-12-31' );
for my $stepcat (@stepcats) {
if ($date eq '') {
$date = $datedefs{$name};
}
- elsif (valid_date($date)) {
+ elsif (dh_parse_date($date)) {
use BSE::Util::SQL qw/date_to_sql/;
$date = date_to_sql($date);
}
};
$@ and return $self->refresh($article, $cgi, '', $@);
}
- use Util 'generate_article';
generate_article($articles, $article) if $Constants::AUTO_GENERATE;
return $self->refresh($article, $cgi, 'step', 'Stepchild information saved');
}
+=item a_restepkid
+
+Moves a stepkid from one parent to another, and sets the order within
+that new stepparent.
+
+Parameters:
+
+=over
+
+=item *
+
+id - id of the step kid to move (required)
+
+=item *
+
+parentid - id of the parent in the stepkid relationship (required)
+
+=item *
+
+newparentid - the new parent for the stepkid relationship (optional)
+
+=item *
+
+_after - id of the allkid under newparentid (or parentid if
+newparentid isn't supplied) to place the stepkid after (0 to place at
+the start)
+
+=back
+
+Errors:
+
+=over
+
+=item *
+
+NOPARENTID - parentid parameter not supplied
+
+=item *
+
+BADPARENTID - non-numeric parentid supplied
+
+=item *
+
+NOTFOUND - no stepkid relationship from parentid was found
+
+=item *
+
+BADNEWPARENT - newparentid is non-numeric
+
+=item *
+
+UNKNOWNNEWPARENT - no article id newparentid found
+
+=item *
+
+NEWPARENTDUP - there's already a stepkid relationship between
+newparentid and id.
+
+=back
+
+=cut
+
+sub req_restepkid {
+ my ($self, $req, $article, $articles) = @_;
+
+ # first, identify the stepkid link
+ my $cgi = $req->cgi;
+ require BSE::TB::OtherParents;
+ my $parentid = $cgi->param("parentid");
+ defined $parentid
+ or return $self->_service_error($req, $article, $articles, "Missing parentid", {}, "NOPARENTID");
+ $parentid =~ /^\d+$/
+ or return $self->_service_error($req, $article, $articles, "Invalid parentid", {}, "BADPARENTID");
+
+ my ($step) = BSE::TB::OtherParents->getBy(parentId => $parentid, childId => $article->id)
+ or return $self->_service_error($req, $article, $articles, "Unknown relationship", {}, "NOTFOUND");
+
+ my $newparentid = $cgi->param("newparentid");
+ if ($newparentid) {
+ $newparentid =~ /^\d+$/
+ or return $self->_service_error($req, $article, $articles, "Bad new parent id", {}, "BADNEWPARENT");
+ my $new_parent = BSE::TB::Articles->getByPkey($newparentid)
+ or return $self->_service_error($req, $article, $articles, "Unknown new parent id", {}, "UNKNOWNNEWPARENT");
+ my $existing =
+ BSE::TB::OtherParents->getBy(parentId=>$newparentid, childId=>$article->id)
+ and return $self->_service_error($req, $article, $articles, "New parent is duplicate", {}, "NEWPARENTDUP");
+
+ $step->{parentId} = $newparentid;
+ $step->save;
+ }
+
+ my $after_id = $cgi->param("_after");
+ if (defined $after_id) {
+ BSE::TB::Articles->reorder_child($step->{parentId}, $article->id, $after_id);
+ }
+
+ if ($req->is_ajax) {
+ return $req->json_content
+ (
+ success => 1,
+ relationshop => $step->data_only,
+ );
+ }
+ else {
+ return $self->refresh($article, $cgi, 'step', "Stepchild moved");
+ }
+}
+
sub add_stepparent {
my ($self, $req, $article, $articles) = @_;
+ $req->check_csrf("admin_add_stepparent")
+ or return $self->csrf_error($req, $article, "admin_add_stepparent", "Add Stepparent");
+
$req->user_can(edit_stepparent_add => $article)
or return $self->edit_form($req, $article, $articles,
"You don't have access to add stepparents to this article");
my $release = $cgi->param('release');
defined $release
or $release = "01/01/2000";
- use BSE::Util::Valid qw/valid_date/;
- $release eq '' or valid_date($release)
+ $release eq '' or dh_parse_date($release)
or die "Invalid release date";
my $expire = $cgi->param('expire');
defined $expire
or $expire = '31/12/2999';
- $expire eq '' or valid_date($expire)
+ $expire eq '' or dh_parse_date($expire)
or die "Invalid expire data";
my $newentry =
};
$@ and return $self->refresh($article, $cgi, 'step', $@);
- use Util 'generate_article';
generate_article($articles, $article) if $Constants::AUTO_GENERATE;
return $self->refresh($article, $cgi, 'stepparents', 'Stepparent added');
sub del_stepparent {
my ($self, $req, $article, $articles) = @_;
+ $req->check_csrf("admin_remove_stepparent")
+ or return $self->csrf_error($req, $article, "admin_del_stepparent", "Delete Stepparent");
+
$req->user_can(edit_stepparent_delete => $article)
or return $self->edit_form($req, $article, $articles,
"You cannot remove stepparents from that article");
};
$@ and return $self->refresh($article, $cgi, 'stepparents', $@);
- use Util 'generate_article';
generate_article($articles, $article) if $Constants::AUTO_GENERATE;
return $self->refresh($article, $cgi, 'stepparents', 'Stepparent deleted');
sub save_stepparents {
my ($self, $req, $article, $articles) = @_;
+ $req->check_csrf("admin_save_stepparents")
+ or return $self->csrf_error($req, $article, "admin_save_stepparents", "Save Stepparents");
$req->user_can(edit_stepparent_save => $article)
or return $self->edit_form($req, $article, $articles,
"No access to save stepparent data for this artice");
my $cgi = $req->cgi;
require 'BSE/Admin/StepParents.pm';
- my @stepparents = OtherParents->getBy(childId=>$article->{id});
+ my @stepparents = BSE::TB::OtherParents->getBy(childId=>$article->{id});
my %stepparents = map { $_->{parentId}, $_ } @stepparents;
my %datedefs = ( release => '2000-01-01', expire=>'2999-12-31' );
for my $stepparent (@stepparents) {
if ($date eq '') {
$date = $datedefs{$name};
}
- elsif (valid_date($date)) {
+ elsif (dh_parse_date($date)) {
use BSE::Util::SQL qw/date_to_sql/;
$date = date_to_sql($date);
}
$@ and return $self->refresh($article, $cgi, '', $@);
}
- use Util 'generate_article';
generate_article($articles, $article) if $Constants::AUTO_GENERATE;
return $self->refresh($article, $cgi, 'stepparents',
if ($url !~ /[?&](m|message)=/ && $message) {
# add in messages if none in the provided refresh
my @msgs = ref $message ? @$message : $message;
+ my $sep = $url =~ /\?/ ? "&" : "?";
for my $msg (@msgs) {
- $url .= "&m=" . CGI::escape($msg);
+ $url .= $sep . "m=" . CGI::escape($msg);
}
}
}
sub save_image_changes {
my ($self, $req, $article, $articles) = @_;
+ $req->check_csrf("admin_save_images")
+ or return $self->csrf_error($req, $article, "admin_save_images", "Save Images");
+
$req->user_can(edit_images_save => $article)
or return $self->edit_form($req, $article, $articles,
"You don't have access to save image information for this article");
my $cgi = $req->cgi;
my $image_pos = $cgi->param('imagePos');
if ($image_pos
- && $image_pos =~ /^(?:tl|tr|bl|br)$/
+ && $image_pos =~ /^(?:tl|tr|bl|br|xx)$/
&& $image_pos ne $article->{imagePos}) {
$article->{imagePos} = $image_pos;
$article->save;
my $name = $cgi->param("name$id");
if (defined $name && $name ne $image->{name}) {
if ($name eq '') {
- if ($article->{id} > 0) {
- $changes{$id}{name} = '';
- }
- else {
- $errors{"name$id"} = "Identifiers are required for global images";
- }
+ $changes{$id}{name} = '';
}
elsif ($name =~ /^[a-z_]\w*$/i) {
my $msg;
if (defined $filename && length $filename) {
my $in_fh = $cgi->upload("image$id");
if ($in_fh) {
- # work out where to put it
- require DevHelp::FileUpload;
- my $msg;
- my ($image_name, $out_fh) = DevHelp::FileUpload->make_img_filename
- ($image_dir, $filename . '', \$msg);
- if ($image_name) {
- local $/ = \8192;
- my $data;
- while ($data = <$in_fh>) {
- print $out_fh $data;
- }
- close $out_fh;
+ my $basename;
+ my $image_error;
+ my ($width, $height, $type) = $self->_validate_image
+ ($filename, $in_fh, \$basename, \$image_error);
- my $full_filename = "$image_dir/$image_name";
- require Image::Size;
- my ($width, $height, $type) = Image::Size::imgsize($full_filename);
- if ($width) {
- $old_images{$id} =
- {
- image => $image->{image},
- storage => $image->{storage}
- };
- push @new_images, $image_name;
+ unless ($type) {
+ $errors{"image$id"} = $image_error;
+ }
- $changes{$id}{image} = $image_name;
- $changes{$id}{storage} = 'local';
- $changes{$id}{src} = "/images/$image_name";
- $changes{$id}{width} = $width;
- $changes{$id}{height} = $height;
+ unless ($errors{"image$id"}) {
+ # work out where to put it
+ require DevHelp::FileUpload;
+ my $msg;
+ my ($image_name, $out_fh) = DevHelp::FileUpload->make_img_filename
+ ($image_dir, $basename, \$msg);
+ if ($image_name) {
+ local $/ = \8192;
+ my $data;
+ while ($data = <$in_fh>) {
+ print $out_fh $data;
+ }
+ close $out_fh;
+
+ my $full_filename = "$image_dir/$image_name";
+ if ($width) {
+ $old_images{$id} =
+ {
+ image => $image->{image},
+ storage => $image->{storage}
+ };
+ push @new_images, $image_name;
+
+ $changes{$id}{image} = $image_name;
+ $changes{$id}{storage} = 'local';
+ $changes{$id}{src} = cfg_image_uri() . "/" . $image_name;
+ $changes{$id}{width} = $width;
+ $changes{$id}{height} = $height;
+ $changes{$id}{ftype} = $self->_image_ftype($type);
+ }
}
else {
- $errors{"image$id"} = $type;
+ $errors{"image$id"} = $msg;
}
}
- else {
- $errors{"image$id"} = $msg;
- }
}
else {
# problem uploading
}
if ($changes_found) {
- use Util 'generate_article';
generate_article($articles, $article) if $Constants::AUTO_GENERATE;
}
=cut
sub _service_error {
- my ($self, $req, $article, $articles, $msg, $error) = @_;
+ my ($self, $req, $article, $articles, $msg, $error, $code, $method) = @_;
+
+ unless ($article) {
+ my $mymsg;
+ $article = $self->_dummy_article($req, $articles, \$mymsg);
+ $article ||=
+ {
+ map $_ => '', BSE::TB::Article->columns
+ };
+ }
if ($req->cgi->param('_service')) {
my $body = '';
};
}
elsif ((() = $req->cgi->param('_')) ||
- (defined $ENV{HTTP_X_REQUESTED_WITH}
+ (exists $ENV{HTTP_X_REQUESTED_WITH}
&& $ENV{HTTP_X_REQUESTED_WITH} =~ /XMLHttpRequest/)) {
$error ||= {};
- my $result = { errors => $error };
+ my $result =
+ {
+ errors => $error,
+ success => 0,
+ };
$msg and $result->{message} = $msg;
- return $req->json_content($result);
+ $code and $result->{error_code} = $code;
+ my $json_result = $req->json_content($result);
+
+ if (!exists $ENV{HTTP_X_REQUESTED_WITH}
+ || $ENV{HTTP_X_REQUESTED_WITH} !~ /XMLHttpRequest/) {
+ $json_result->{type} = "text/plain";
+ }
+
+ return $json_result;
}
else {
- return $self->edit_form($req, $article, $articles, $msg, $error);
+ $method ||= "edit_form";
+ return $self->$method($req, $article, $articles, $msg, $error);
}
}
};
}
+# FIXME: eliminate this method and call get_ftype directly
+sub _image_ftype {
+ my ($self, $type) = @_;
+
+ require BSE::TB::Images;
+ return BSE::TB::Images->get_ftype($type);
+}
+
+my %valid_exts =
+ (
+ tiff => "tiff,tif",
+ jpg => "jpeg,jpg",
+ pnm => "pbm,pgm,ppm",
+ );
+
+sub _validate_image {
+ my ($self, $filename, $fh, $rbasename, $error) = @_;
+
+ if ($fh) {
+ if (-z $fh) {
+ $$error = 'Image file is empty';
+ return;
+ }
+ }
+ else {
+ $$error = 'Please enter an image filename';
+ return;
+ }
+ my $imagename = $filename;
+ $imagename .= ''; # force it into a string
+ (my $basename = $imagename) =~ tr/A-Za-z0-9_./-/cs;
+
+ require BSE::ImageSize;
+
+ my ($width,$height, $type) = BSE::ImageSize::imgsize($fh);
+
+ unless (defined $width) {
+ $$error = "Unknown image file type";
+ return;
+ }
+
+ my $lctype = lc $type;
+ my @valid_exts = split /,/,
+ BSE::Cfg->single->entry("valid image extensions", $lctype,
+ $valid_exts{$lctype} || $lctype);
+
+ my ($ext) = $basename =~ /\.(\w+)\z/;
+ if (!$ext || !grep $_ eq lc $ext, @valid_exts) {
+ $basename .= ".$valid_exts[0]";
+ }
+ $$rbasename = $basename;
+
+ return ($width, $height, $type);
+}
+
+my $last_display_order = 0;
+
sub do_add_image {
my ($self, $cfg, $article, $image, %opts) = @_;
or $errors->{name} = $workmsg;
}
- if ($image) {
- if (-z $image) {
- $errors->{image} = 'Image file is empty';
- }
- }
- else {
- $errors->{image} = 'Please enter an image filename';
+ my $image_error;
+ my $basename;
+ my ($width, $height, $type) =
+ $self->_validate_image($opts{filename} || $image, $image, \$basename,
+ \$image_error);
+ unless ($width) {
+ $errors->{image} = $image_error;
}
+
keys %$errors
and return;
- my $imagename = $opts{filename} || $image;
- $imagename .= ''; # force it into a string
- my $basename = '';
- $imagename =~ tr/ //d;
- $imagename =~ /([\w.-]+)$/ and $basename = $1;
-
- # create a filename that we hope is unique
- my $filename = time. '_'. $basename;
-
# for the sysopen() constants
use Fcntl;
my $imagedir = cfg_image_dir($cfg);
- # loop until we have a unique filename
- my $counter="";
- $filename = time. '_' . $counter . '_' . $basename
- until sysopen( OUTPUT, "$imagedir/$filename", O_WRONLY| O_CREAT| O_EXCL)
- || ++$counter > 100;
-
- fileno(OUTPUT) or die "Could not open image file: $!";
- # for OSs with special text line endings
- binmode OUTPUT;
+ require DevHelp::FileUpload;
+ my $msg;
+ my ($filename, $fh) =
+ DevHelp::FileUpload->make_img_filename($imagedir, $basename, \$msg);
+ unless ($filename) {
+ $errors->{image} = $msg;
+ return;
+ }
my $buffer;
+ binmode $fh;
+
no strict 'refs';
# read the image in from the browser and output it to our output filehandle
- print OUTPUT $buffer while read $image, $buffer, 1024;
+ print $fh $buffer while read $image, $buffer, 1024;
# close and flush
- close OUTPUT
+ close $fh
or die "Could not close image file $filename: $!";
- use Image::Size;
-
-
- my($width,$height) = imgsize("$imagedir/$filename");
+ my $display_order = time;
+ if ($display_order <= $last_display_order) {
+ $display_order = $last_display_order + 1;
+ }
+ $last_display_order = $display_order;
my $alt = $opts{alt};
defined $alt or $alt = '';
width=>$width,
height => $height,
url => $url,
- displayOrder=>time,
+ displayOrder => $display_order,
name => $imageref,
storage => 'local',
- src => '/images/' . $filename,
+ src => cfg_image_uri() . '/' . $filename,
+ ftype => $self->_image_ftype($type),
);
- require Images;
- my @cols = Image->columns;
+ require BSE::TB::Images;
+ my @cols = BSE::TB::Image->columns;
shift @cols;
- my $imageobj = Images->add(@image{@cols});
+ my $imageobj = BSE::TB::Images->add(@image{@cols});
my $storage = $opts{storage};
defined $storage or $storage = 'local';
return $imageobj;
}
+sub _image_data {
+ my ($self, $cfg, $image) = @_;
+
+ my $data = $image->data_only;
+ $data->{src} = $image->image_url($cfg);
+
+ return $data;
+}
+
sub add_image {
my ($self, $req, $article, $articles) = @_;
+ $req->check_csrf("admin_add_image")
+ or return $self->csrf_error($req, $article, "admin_add_image", "Add Image");
$req->user_can(edit_images_add => $article)
or return $self->_service_error($req, $article, $articles,
"You don't have access to add new images to this article");
my $cgi = $req->cgi;
+
my %errors;
+
+ my $save_tags = $cgi->param("_save_tags");
+ my @tags;
+ if ($save_tags) {
+ @tags = $cgi->param("tags");
+ $self->_validate_tags(\@tags, \%errors);
+ }
+
my $imageobj =
$self->do_add_image
(
$req->cfg,
$article,
- scalar($cgi->param('image')),
+ scalar($cgi->upload('image')),
name => scalar($cgi->param('name')),
alt => scalar($cgi->param('altIn')),
url => scalar($cgi->param('url')),
storage => scalar($cgi->param('storage')),
errors => \%errors,
+ filename => scalar($cgi->param("image")),
);
$imageobj
or return $self->_service_error($req, $article, $articles, undef, \%errors);
+ if ($save_tags) {
+ my $error;
+ $imageobj->set_tags([ grep /\S/, @tags ], \$error);
+ }
+
# typically a soft failure from the storage
$errors{flash}
and $req->flash($errors{flash});
- use Util 'generate_article';
generate_article($articles, $article) if $Constants::AUTO_GENERATE;
if ($cgi->param('_service')) {
},
);
}
+ elsif ($cgi->param("_") || $req->is_ajax) {
+ my $resp = $req->json_content
+ (
+ success => 1,
+ image => $self->_image_data($req->cfg, $imageobj),
+ );
+
+ # the browser handles this directly, tell it that it's text
+ $resp->{type} = "text/plain";
+
+ return $resp;
+ }
else {
return $self->refresh($article, $cgi, undef, 'New image added');
}
sub _image_manager {
my ($self) = @_;
- require BSE::StorageMgr::Images;
-
- return BSE::StorageMgr::Images->new(cfg => $self->cfg);
+ require BSE::TB::Images;
+ return BSE::TB::Images->storage_manager;
}
# remove an image
sub remove_img {
my ($self, $req, $article, $articles, $imageid) = @_;
+ $req->check_csrf("admin_remove_image")
+ or return $self->csrf_error($req, $article, "admin_remove_image", "Remove Image");
+
$req->user_can(edit_images_delete => $article)
- or return $self->edit_form($req, $article, $articles,
- "You don't have access to delete images from this article");
+ or return $self->_service_error($req, $article, $articles,
+ "You don't have access to delete images from this article", {}, "ACCESS");
$imageid or die;
my @images = $self->get_images($article);
- my ($image) = grep $_->{id} == $imageid, @images
- or return $self->show_images($req, $article, $articles, "No such image");
+ my ($image) = grep $_->{id} == $imageid, @images;
+ unless ($image) {
+ if ($req->want_json_response) {
+ return $self->_service_error($req, $article, $articles, "No such image", {}, "NOTFOUND");
+ }
+ else {
+ return $self->show_images($req, $article, $articles, "No such image");
+ }
+ }
if ($image->{storage} ne 'local') {
my $mgr = $self->_image_manager($req->cfg);
}
my $imagedir = cfg_image_dir($req->cfg);
- unlink "$imagedir$image->{image}";
$image->remove;
- use Util 'generate_article';
generate_article($articles, $article) if $Constants::AUTO_GENERATE;
+ if ($req->want_json_response) {
+ return $req->json_content
+ (
+ success => 1,
+ );
+ }
+
return $self->refresh($article, $req->cgi, undef, 'Image removed');
}
sub move_img_up {
my ($self, $req, $article, $articles) = @_;
+ $req->check_csrf("admin_move_image")
+ or return $self->csrf_error($req, $article, "admin_move_image", "Move Image");
$req->user_can(edit_images_reorder => $article)
or return $self->edit_form($req, $article, $articles,
"You don't have access to reorder images in this article");
$to->save;
$from->save;
- use Util 'generate_article';
generate_article($articles, $article) if $Constants::AUTO_GENERATE;
return $self->refresh($article, $req->cgi, undef, 'Image moved');
sub move_img_down {
my ($self, $req, $article, $articles) = @_;
+ $req->check_csrf("admin_move_image")
+ or return $self->csrf_error($req, $article, "admin_move_image", "Move Image");
$req->user_can(edit_images_reorder => $article)
or return $self->edit_form($req, $article, $articles,
"You don't have access to reorder images in this article");
$to->save;
$from->save;
- use Util 'generate_article';
generate_article($articles, $article) if $Constants::AUTO_GENERATE;
return $self->refresh($article, $req->cgi, undef, 'Image moved');
my $geometry_id = $cgi->param('g');
defined $geometry_id or $geometry_id = 'editor';
my $geometry = $cfg->entry('thumb geometries', $geometry_id, 'scale(200x200)');
- my $imagedir = $cfg->entry('paths', 'images', $Constants::IMAGEDIR);
+ my $imagedir = cfg_image_dir();
my $error;
- ($data, $type) = $thumb_obj->
- thumb_data("$imagedir/$image->{image}", $geometry, \$error)
+ ($data, $type) = $thumb_obj->thumb_data
+ (
+ filename => "$imagedir/$image->{image}",
+ geometry => $geometry,
+ error => \$error
+ )
or return
{
type => 'text/plain',
}
else {
# grab the nothumb image
- my $uri = $cfg->entry('editor', 'default_thumbnail', '/images/admin/nothumb.png');
- my $filebase = $Constants::CONTENTBASE;
+ my $uri = $cfg->entry('editor', 'default_thumbnail', cfg_dist_image_uri() . '/admin/nothumb.png');
+ my $filebase = $cfg->content_base_path;
if (open IMG, "<$filebase/$uri") {
binmode IMG;
my $data = do { local $/; <IMG> };
}
}
+=item edit_image
+
+Display a form to allow editing an image.
+
+Tags:
+
+=over
+
+=item *
+
+eimage - the image being edited
+
+=item *
+
+normal article edit tags.
+
+=back
+
+Variables:
+
+eimage - the image being edited.
+
+=cut
+
sub req_edit_image {
my ($self, $req, $article, $articles, $errors) = @_;
or return $self->edit_form($req, $article, $articles,
"You don't have access to save image information for this article");
+ $req->set_variable(eimage => $image);
+
my %acts;
%acts =
(
return $req->response('admin/image_edit', \%acts);
}
+=item a_save_image
+
+Save changes to an image.
+
+Parameters:
+
+=over
+
+=item *
+
+id - article id
+
+=item *
+
+image_id - image id
+
+=item *
+
+alt, url, name - text fields to update
+
+=item *
+
+image - replacement image data (if any)
+
+=back
+
+=cut
+
sub req_save_image {
my ($self, $req, $article, $articles) = @_;
+ $req->check_csrf("admin_save_image")
+ or return $self->csrf_error($req, $article, "admin_save_image", "Save Image");
my $cgi = $req->cgi;
my $id = $cgi->param('image_id');
my @images = $self->get_images($article);
my ($image) = grep $_->{id} == $id, @images
- or return $self->edit_form($req, $article, $articles,
- "No such image");
+ or return $self->_service_error($req, $article, $articles, "No such image",
+ {}, "NOTFOUND");
$req->user_can(edit_images_save => $article)
- or return $self->edit_form($req, $article, $articles,
- "You don't have access to save image information for this article");
+ or return $self->_service_error($req, $article, $articles,
+ "You don't have access to save image information for this article", {}, "ACCESS");
my $image_dir = cfg_image_dir($req->cfg);
}
}
else {
- if ($article->{id} == -1) {
- $errors{name} = "Identifiers are required for global images";
- }
- else {
- $image->{name} = '';
- }
+ $image->{name} = '';
}
}
my $filename = $cgi->param('image');
if (defined $filename && length $filename) {
my $in_fh = $cgi->upload('image');
if ($in_fh) {
- require DevHelp::FileUpload;
- my $msg;
- my ($image_name, $out_fh) = DevHelp::FileUpload->make_img_filename
- ($image_dir, $filename . '', \$msg);
- if ($image_name) {
- {
- local $/ = \8192;
- my $data;
- while ($data = <$in_fh>) {
- print $out_fh $data;
+ my $basename;
+ my $image_error;
+ my ($width, $height, $type) = $self->_validate_image
+ ($filename, $in_fh, \$basename, \$image_error);
+ if ($type) {
+ require DevHelp::FileUpload;
+ my $msg;
+ my ($image_name, $out_fh) = DevHelp::FileUpload->make_img_filename
+ ($image_dir, $basename, \$msg);
+ if ($image_name) {
+ {
+ local $/ = \8192;
+ my $data;
+ while ($data = <$in_fh>) {
+ print $out_fh $data;
+ }
+ close $out_fh;
}
- close $out_fh;
- }
- my $full_filename = "$image_dir/$image_name";
- require Image::Size;
- my ($width, $height, $type) = Image::Size::imgsize($full_filename);
- if ($width) {
+ my $full_filename = "$image_dir/$image_name";
$delete_file = $image->{image};
$image->{image} = $image_name;
$image->{width} = $width;
$image->{height} = $height;
$image->{storage} = 'local'; # not on the remote store yet
- $image->{src} = '/images/' . $image_name;
+ $image->{src} = cfg_image_uri() . '/' . $image_name;
+ $image->{ftype} = $self->_image_ftype($type);
}
else {
- $errors{image} = $type;
+ $errors{image} = $msg;
}
}
else {
- $errors{image} = $msg;
+ $errors{image} = $image_error;
}
}
else {
$errors{image} = "No image file received";
}
}
- keys %errors
- and return $self->req_edit_image($req, $article, $articles, \%errors);
+ my $save_tags = $cgi->param("_save_tags");
+ my @tags;
+ if ($save_tags) {
+ @tags = $cgi->param("tags");
+ $self->_validate_tags(\@tags, \%errors);
+ }
+ if (keys %errors) {
+ if ($req->want_json_response) {
+ return $self->_service_error($req, $article, $articles, undef,
+ \%errors, "FIELD");
+ }
+ else {
+ return $self->req_edit_image($req, $article, $articles, \%errors);
+ }
+ }
my $new_storage = $cgi->param('storage');
defined $new_storage or $new_storage = $image->{storage};
$image->save;
+ if ($save_tags) {
+ my $error;
+ $image->set_tags([ grep /\S/, @tags ], \$error);
+ }
my $mgr = $self->_image_manager($req->cfg);
if ($delete_file) {
if ($old_storage ne 'local') {
$@ and $req->flash("There was a problem removing if from the old storage: $@");
}
+ if ($req->want_json_response) {
+ return $req->json_content
+ (
+ success => 1,
+ image => $self->_image_data($req->cfg, $image),
+ );
+ }
+
return $self->refresh($article, $cgi);
}
+=item a_order_images
+
+Change the order of images for an article (or global images).
+
+Ajax only.
+
+=over
+
+=item *
+
+id - id of the article to change the image order for (-1 for global
+images)
+
+=item *
+
+order - comma-separated list of image ids in the new order.
+
+=back
+
+=cut
+
+sub req_order_images {
+ my ($self, $req, $article, $articles) = @_;
+
+ $req->is_ajax
+ or return $self->_service_error($req, $article, $articles, "The function only permitted from Ajax", {}, "AJAXONLY");
+
+ my $order = $req->cgi->param("order");
+ defined $order
+ or return $self->_service_error($req, $article, $articles, "order not supplied", {}, "NOORDER");
+ $order =~ /^\d+(,\d+)*$/
+ or return $self->_service_error($req, $article, $articles, "order not supplied", {}, "BADORDER");
+
+ my @order = split /,/, $order;
+
+ my @images = $article->set_image_order(\@order);
+
+ return $req->json_content
+ (
+ success => 1,
+ images =>
+ [
+ map $self->_image_data($req->cfg, $_), @images
+ ],
+ );
+}
+
sub get_article {
my ($self, $articles, $article) = @_;
description => 'Identifier',
maxlength => 80,
},
+ category =>
+ {
+ description => "Category",
+ maxlength => 20,
+ },
);
sub fileadd {
my ($self, $req, $article, $articles) = @_;
+ $req->check_csrf("admin_add_file")
+ or return $self->csrf_error($req, $article, "admin_add_file", "Add File");
$req->user_can(edit_files_add => $article)
- or return $self->edit_form($req, $article, $articles,
+ or return $self->_service_error($req, $article, $articles,
"You don't have access to add files to this article");
my %file;
my $cgi = $req->cgi;
- require ArticleFile;
- my @cols = ArticleFile->columns;
+ require BSE::TB::ArticleFiles;
+ my @cols = BSE::TB::ArticleFile->columns;
shift @cols;
for my $col (@cols) {
if (defined $cgi->param($col)) {
$req->validate(errors => \%errors,
fields => \%file_fields,
section => $article->{id} == -1 ? 'Global File Validation' : 'Article File Validation');
-
- $file{forSale} = 0 + exists $file{forSale};
- $file{articleId} = $article->{id};
- $file{download} = 0 + exists $file{download};
- $file{requireUser} = 0 + exists $file{requireUser};
- $file{hide_from_list} = 0 + exists $file{hide_from_list};
-
- my $downloadPath = $self->{cfg}->entryVar('paths', 'downloads');
-
+
# build a filename
- my $file = $cgi->param('file');
+ my $file = $cgi->upload('file');
+ my $filename = $cgi->param("file");
unless ($file) {
$errors{file} = 'Please enter a filename';
}
if ($file && -z $file) {
$errors{file} = 'File is empty';
}
-
- unless ($file{contentType}) {
- unless ($file =~ /\.([^.]+)$/) {
- $file{contentType} = "application/octet-stream";
- }
- unless ($file{contentType}) {
- $file{contentType} = content_type($self->cfg, $file);
- }
- }
+
+ $file{forSale} = 0 + exists $file{forSale};
+ $file{articleId} = $article->{id};
+ $file{download} = 0 + exists $file{download};
+ $file{requireUser} = 0 + exists $file{requireUser};
+ $file{hide_from_list} = 0 + exists $file{hide_from_list};
+ $file{category} ||= '';
defined $file{name} or $file{name} = '';
- if ($article->{id} == -1 && $file{name} eq '') {
- $errors{name} = 'Identifier is required for global files';
- }
if (!$errors{name} && length $file{name} && $file{name} !~/^\w+$/) {
$errors{name} = "Identifier must be a single word";
}
}
keys %errors
- and return $self->edit_form($req, $article, $articles, undef, \%errors);
+ and return $self->_service_error($req, $article, $articles, undef, \%errors);
my $basename = '';
- my $workfile = $file;
+ my $workfile = $filename;
$workfile =~ s![^\w.:/\\-]+!_!g;
$workfile =~ tr/_/_/s;
$workfile =~ /([ \w.-]+)$/ and $basename = $1;
$basename =~ tr/ /_/;
-
- # if the user supplies a really long filename, it can overflow the
- # filename field
-
- my $work_filename = $basename;
- if (length $work_filename > 60) {
- $work_filename = substr($work_filename, -60);
- }
-
- my $filename = time. '_'. $work_filename;
-
- # for the sysopen() constants
- use Fcntl;
-
- # loop until we have a unique filename
- my $counter="";
- $filename = time. '_' . $counter . '_' . $work_filename
- until sysopen( OUTPUT, "$downloadPath/$filename",
- O_WRONLY| O_CREAT| O_EXCL)
- || ++$counter > 100;
-
- fileno(OUTPUT) or die "Could not open file: $!";
-
- # for OSs with special text line endings
- binmode OUTPUT;
-
- my $buffer;
-
- no strict 'refs';
-
- # read the image in from the browser and output it to our output filehandle
- print OUTPUT $buffer while read $file, $buffer, 8192;
-
- # close and flush
- close OUTPUT
- or die "Could not close file $filename: $!";
-
- use BSE::Util::SQL qw/now_datetime/;
- $file{filename} = $filename;
$file{displayName} = $basename;
- $file{sizeInBytes} = -s $file;
- $file{displayOrder} = time;
- $file{whenUploaded} = now_datetime();
- $file{storage} = 'local';
- $file{src} = '';
+ $file{file} = $file;
- require ArticleFiles;
- my $fileobj = ArticleFiles->add(@file{@cols});
+ local $SIG{__DIE__};
+ my $fileobj =
+ eval {
+ $article->add_file($self->cfg, %file);
+ };
- $req->flash("New file added");
+ $fileobj
+ or return $self->_service_error($req, $article, $articles, $@);
- my $storage = $cgi->param('storage');
- defined $storage or $storage = 'local';
- my $file_manager = $self->_file_manager($req->cfg);
+ unless ($req->is_ajax) {
+ $req->flash("New file added");
+ }
- local $SIG{__DIE__};
+ my $json =
+ {
+ success => 1,
+ file => $fileobj->data_only,
+ warnings => [],
+ };
+ my $storage = $cgi->param("storage") || "";
eval {
- my $src;
- $storage = $self->_select_filestore($req, $file_manager, $storage, $fileobj);
- $src = $file_manager->store($filename, $storage, $fileobj);
-
- if ($src) {
- $fileobj->{src} = $src;
- $fileobj->{storage} = $storage;
- $fileobj->save;
+ my $msg;
+
+ $article->apply_storage($self->cfg, $fileobj, $storage, \$msg);
+
+ if ($msg) {
+ if ($req->is_ajax) {
+ push @{$json->{warnings}}, $msg;
+ }
+ else {
+ $req->flash_error($msg);
+ }
}
};
if ($@) {
- $req->flash($@);
+ if ($req->is_ajax) {
+ push @{$json->{warnings}}, $@;
+ }
+ else {
+ $req->flash_error($@);
+ }
}
- use Util 'generate_article';
generate_article($articles, $article) if $Constants::AUTO_GENERATE;
- $self->_refresh_filelist($req, $article);
+ if ($req->is_ajax) {
+ return $req->json_content($json);
+ }
+ else {
+ $self->_refresh_filelist($req, $article);
+ }
}
sub fileswap {
my ($self, $req, $article, $articles) = @_;
+ $req->check_csrf("admin_move_file")
+ or return $self->csrf_error($req, $article, "admin_move_file", "Move File");
+
$req->user_can('edit_files_reorder', $article)
or return $self->edit_form($req, $article, $articles,
"You don't have access to reorder files in this article");
}
}
- use Util 'generate_article';
generate_article($articles, $article) if $Constants::AUTO_GENERATE;
$self->refresh($article, $req->cgi, undef, 'File moved');
sub filedel {
my ($self, $req, $article, $articles) = @_;
+ $req->check_csrf("admin_remove_file")
+ or return $self->csrf_error($req, $article, "admin_remove_file", "Delete File");
$req->user_can('edit_files_delete', $article)
or return $self->edit_form($req, $article, $articles,
"You don't have access to delete files from this article");
if ($file) {
if ($file->{storage} ne 'local') {
- my $mgr = $self->_file_manager;
+ my $mgr = $self->_file_manager($self->cfg);
$mgr->unstore($file->{filename}, $file->{storage});
}
}
}
- use Util 'generate_article';
generate_article($articles, $article) if $Constants::AUTO_GENERATE;
$self->_refresh_filelist($req, $article, 'File deleted');
}
-# only some files can be stored remotely
-sub _select_filestore {
- my ($self, $req, $mgr, $storage, $file) = @_;
-
- my $store = $mgr->select_store($file->{filename}, $storage, $file);
- if ($store ne 'local') {
- if ($file->{forSale} || $file->{requireUser}) {
- $store = 'local';
- $req->flash("For sale or user required files can only be stored locally");
- }
- elsif ($file->{articleId} != -1 && $file->article->is_access_controlled) {
- $store = 'local';
- $req->flash("Files for access controlled articles can only be stored locally");
- }
- }
-
- return $store;
-}
-
sub filesave {
my ($self, $req, $article, $articles) = @_;
+ $req->check_csrf("admin_save_files")
+ or return $self->csrf_error($req, $article, "admin_save_files", "Save Files");
+
$req->user_can('edit_files_save', $article)
or return $self->edit_form($req, $article, $articles,
"You don't have access to save file information for this article");
my @files = $self->get_files($article);
- my $download_path = $self->{cfg}->entryVar('paths', 'downloads');
+ my $download_path = BSE::TB::ArticleFiles->download_path($self->{cfg});
my $cgi = $req->cgi;
my %names;
my @old_files;
my @new_files;
my %store_anyway;
+ my $change_count = 0;
+ my @content_changed;
for my $file (@files) {
my $id = $file->{id};
+ my $orig = $file->data_only;
my $desc = $cgi->param("description_$id");
defined $desc and $file->{description} = $desc;
my $type = $cgi->param("contentType_$id");
}
my $notes = $cgi->param("notes_$id");
defined $notes and $file->{notes} = $notes;
+ my $category = $cgi->param("category_$id");
+ defined $category and $file->{category} = $category;
my $name = $cgi->param("name_$id");
if (defined $name) {
$file->{name} = $name;
$errors{"name_$id"} = "Invalid file identifier $name";
}
}
- elsif ($article->{id} == -1) {
- $errors{"name_$id"} = "Identifier is required for global files";
- }
}
else {
push @{$names{$file->{name}}}, $id
$file->{filename} = $file_name;
$file->{storage} = 'local';
$file->{sizeInBytes} = -s $full_name;
- $file->{whenUploaded} = now_datetime();
+ $file->{whenUploaded} = now_sqldatetime();
$file->{displayName} = $display_name;
+ push @content_changed, $file;
}
else {
$errors{"file_$id"} = $msg;
$errors{"file_$id"} = "Filename too long";
}
}
+
+ my $new = $file->data_only;
+ COLUMN:
+ for my $col ($file->columns) {
+ if ($new->{$col} ne $orig->{$col}) {
+ ++$change_count;
+ last COLUMN;
+ }
+ }
}
for my $name (keys %names) {
if (@{$names{$name}} > 1) {
return $self->edit_form($req, $article, $articles, undef, \%errors);
}
- $req->flash('File information saved');
- my $mgr = $self->_file_manager;
+ if ($change_count) {
+ $req->flash("msg:bse/admin/edit/file/save/success_count", [ $change_count ]);
+ }
+ else {
+ $req->flash("msg:bse/admin/edit/file/save/success_none");
+ }
+ my $mgr = $self->_file_manager($self->cfg);
for my $file (@files) {
$file->save;
my $storage = $cgi->param("storage_$file->{id}");
defined $storage or $storage = 'local';
- $storage = $self->_select_filestore($req, $mgr, $storage, $file);
+ my $msg;
+ $storage = $article->select_filestore($mgr, $file, $storage, \$msg);
+ $msg and $req->flash($msg);
if ($storage ne $file->{storage} || $store_anyway{$file->{id}}) {
my $old_storage = $file->{storage};
eval {
unlink "$download_path/$filename";
}
- use Util 'generate_article';
+ # update file type metadatas
+ for my $file (@content_changed) {
+ $file->set_handler($self->{cfg});
+ $file->save;
+ }
+
generate_article($articles, $article) if $Constants::AUTO_GENERATE;
$self->_refresh_filelist($req, $article);
}
+sub req_filemeta {
+ my ($self, $req, $article, $articles, $errors) = @_;
+
+ my $cgi = $req->cgi;
+
+ my $id = $cgi->param('file_id');
+
+ my ($file) = grep $_->{id} == $id, $self->get_files($article)
+ or return $self->edit_form($req, $article, $articles,
+ "No such file");
+ $req->user_can(edit_files_save => $article)
+ or return $self->edit_form($req, $article, $articles,
+ "You don't have access to save file information for this article");
+
+ my $name = $cgi->param('name');
+ $name && $name =~ /^\w+$/
+ or return $self->edit_form($req, $article, $articles,
+ "Missing or invalid metadata name");
+
+ my $meta = $file->meta_by_name($name)
+ or return $self->edit_form($req, $article, $articles,
+ "Metadata $name not defined for this file");
+
+ return
+ {
+ type => $meta->content_type,
+ content => $meta->value,
+ };
+}
+
sub tag_old_checked {
my ($errors, $cgi, $file, $key) = @_;
return $errors ? $cgi->param($key) : $file->{$key};
}
+sub tag_filemeta_value {
+ my ($file, $args, $acts, $funcname, $templater) = @_;
+
+ my ($name) = DevHelp::Tags->get_parms($args, $acts, $templater)
+ or return "* no meta name supplied *";
+
+ my $meta = $file->meta_by_name($name)
+ or return "";
+
+ $meta->content_type eq "text/plain"
+ or return "* $name has type " . $meta->content_type . " and cannot be displayed inline *";
+
+ return escape_html($meta->value);
+}
+
+sub tag_ifFilemeta_set {
+ my ($file, $args, $acts, $funcname, $templater) = @_;
+
+ my ($name) = DevHelp::Tags->get_parms($args, $acts, $templater)
+ or return "* no meta name supplied *";
+
+ my $meta = $file->meta_by_name($name)
+ or return 0;
+
+ return 1;
+}
+
+sub tag_filemeta_source {
+ my ($file, $args, $acts, $funcname, $templater) = @_;
+
+ my ($name) = DevHelp::Tags->get_parms($args, $acts, $templater)
+ or return "* no meta name supplied *";
+
+ return "$ENV{SCRIPT_NAME}?a_filemeta=1&id=$file->{articleId}&file_id=$file->{id}&name=$name";
+}
+
+sub tag_filemeta_select {
+ my ($cgi, $allmeta, $rcurr_meta, $file, $args, $acts, $funcname, $templater) = @_;
+
+ my $meta;
+ if ($args =~ /\S/) {
+ my ($name) = DevHelp::Tags->get_parms($args, $acts, $templater)
+ or return "* cannot parse *";
+ ($meta) = grep $_->name eq $name, @$allmeta
+ or return "* cannot find meta field *";
+ }
+ elsif ($$rcurr_meta) {
+ $meta = $$rcurr_meta;
+ }
+ else {
+ return "* use in filemeta iterator or supply a name *";
+ }
+
+ $meta->type eq "enum"
+ or return "* can only use filemeta_select on enum metafields *";
+
+ my %labels;
+ my @values = $meta->values;
+ @labels{@values} = $meta->labels;
+
+ my $field_name = "meta_" . $meta->name;
+ my ($def) = $cgi->param($field_name);
+ unless (defined $def) {
+ my $value = $file->meta_by_name($meta->name);
+ if ($value && $value->is_text) {
+ $def = $value->value;
+ }
+ }
+ defined $def or $def = $values[0];
+
+ return popup_menu
+ (
+ -name => $field_name,
+ -values => \@values,
+ -labels => \%labels,
+ -default => $def,
+ );
+}
+
+sub tag_filemeta_select_label {
+ my ($allmeta, $rcurr_meta, $file, $args, $acts, $funcname, $templater) = @_;
+
+ my $meta;
+ if ($args =~ /\S/) {
+ my ($name) = DevHelp::Tags->get_parms($args, $acts, $templater)
+ or return "* cannot parse *";
+ ($meta) = grep $_->name eq $name, @$allmeta
+ or return "* cannot find meta field *";
+ }
+ elsif ($$rcurr_meta) {
+ $meta = $$rcurr_meta;
+ }
+ else {
+ return "* use in filemeta iterator or supply a name *";
+ }
+
+ $meta->type eq "enum"
+ or return "* can only use filemeta_select_label on enum metafields *";
+
+ my %labels;
+ my @values = $meta->values;
+ @labels{@values} = $meta->labels;
+
+ my $field_name = "meta_" . $meta->name;
+ my $value = $file->meta_by_name($meta->name);
+ if ($value) {
+ if ($value->is_text) {
+ if (exists $labels{$value->value}) {
+ return escape_html($labels{$value->value});
+ }
+ else {
+ return escape_html($value->value);
+ }
+ }
+ else {
+ return "* cannot display type " . $value->content_type . " inline *";
+ }
+ }
+ else {
+ return "* " . $meta->name . " not set *";
+ }
+}
+
sub req_edit_file {
my ($self, $req, $article, $articles, $errors) = @_;
or return $self->edit_form($req, $article, $articles,
"You don't have access to save file information for this article");
+ my @metafields = $file->metafields($self->cfg);
+
+ $req->set_variable(file => $file);
+ $req->set_variable(fields => { BSE::TB::ArticleFile->fields });
+
+ my $it = BSE::Util::Iterate->new;
+ my $current_meta;
my %acts;
%acts =
(
$self->low_edit_tags(\%acts, $req, $article, $articles, undef,
$errors),
- efile => [ \&tag_hash, $file ],
+ efile => [ \&tag_object, $file ],
error_img => [ \&tag_error_img, $req->cfg, $errors ],
ifOldChecked =>
[ \&tag_old_checked, $errors, $cgi, $file ],
+ $it->make
+ (
+ plural => "filemetas",
+ single => "filemeta",
+ data => \@metafields,
+ store => \$current_meta,
+ ),
+ filemeta_value =>
+ [ \&tag_filemeta_value, $file ],
+ ifFilemeta_set =>
+ [ \&tag_ifFilemeta_set, $file ],
+ filemeta_source =>
+ [ \&tag_filemeta_source, $file ],
+ filemeta_select =>
+ [ \&tag_filemeta_select, $cgi, \@metafields, \$current_meta, $file ],
+ filemeta_select_label =>
+ [ \&tag_filemeta_select_label, \@metafields, \$current_meta, $file ],
);
return $req->response('admin/file_edit', \%acts);
sub req_save_file {
my ($self, $req, $article, $articles) = @_;
+ $req->check_csrf("admin_save_file")
+ or return $self->csrf_error($req, $article, "admin_save_file", "Save File");
+
my $cgi = $req->cgi;
my @files = $self->get_files($article);
"You don't have access to save file information for this article");
my @other_files = grep $_->{id} != $id, @files;
- my $download_path = $self->{cfg}->entryVar('paths', 'downloads');
+ my $download_path = BSE::TB::ArticleFiles->download_path($self->{cfg});
my %errors;
my $notes = $cgi->param("notes");
defined $notes and $file->{notes} = $notes;
my $name = $cgi->param("name");
+ require BSE::ImageSize;
if (defined $name) {
$file->{name} = $name;
if (length $name) {
$errors{name} = "Invalid file identifier $name";
}
}
- if (!$errors{name} && $article->{id} == -1) {
- length $name
- or $errors{name} = "Identifier is required for global files";
- }
}
+ require BSE::FileMetaMeta;
+ my $meta = BSE::FileMetaMeta->retrieve($req, $file, \%errors);
+
if ($cgi->param('save_file_flags')) {
my $download = 0 + defined $cgi->param("download");
if ($download ne $file->{download}) {
$file->{filename} = $file_name;
$file->{sizeInBytes} = -s $full_name;
- $file->{whenUploaded} = now_datetime();
+ $file->{whenUploaded} = now_sqldatetime();
$file->{displayName} = $display_name;
$file->{storage} = 'local';
}
}
$file->save;
- $req->flash('File information saved');
- my $mgr = $self->_file_manager;
+ $file->set_handler($self->cfg);
+ $file->save;
+
+ $req->flash("msg:bse/admin/edit/file/save/success", [ $file->displayName ]);
+ my $mgr = $self->_file_manager($self->cfg);
my $storage = $cgi->param('storage');
defined $storage or $storage = $file->{storage};
- $storage = $self->_select_filestore($req, $mgr, $storage, $file);
+ my $msg;
+ $storage = $article->select_filestore($mgr, $file, $storage, \$msg);
+ $msg and $req->flash($msg);
if ($storage ne $file->{storage} || $store_anyway) {
my $old_storage = $file->{storage};
eval {
and $req->flash("Could not move $file->{displayName} to $storage: $@");
}
+ BSE::FileMetaMeta->save($file, $meta);
+
# remove the replaced files
if (my ($old_name, $old_storage) = @old_file) {
$mgr->unstore($old_name, $old_storage);
unlink "$download_path/$old_name";
}
- use Util 'generate_article';
generate_article($articles, $article) if $Constants::AUTO_GENERATE;
$self->_refresh_filelist($req, $article);
}
sub can_remove {
- my ($self, $req, $article, $articles, $rmsg) = @_;
+ my ($self, $req, $article, $articles, $rmsg, $rcode) = @_;
unless ($req->user_can('edit_delete_article', $article, $rmsg)) {
$$rmsg ||= "Access denied";
+ $$rcode = "ACCESS";
return;
}
if ($articles->children($article->{id})) {
$$rmsg = "This article has children. You must delete the children first (or change their parents)";
+ $$rcode = "CHILDREN";
return;
}
- if (grep $_ == $article->{id}, @Constants::NO_DELETE) {
+ if (grep($_ == $article->{id}, @Constants::NO_DELETE)
+ || $req->cfg->entry("undeletable articles", $article->{id})) {
$$rmsg = "Sorry, these pages are essential to the site structure - they cannot be deleted";
+ $$rcode = "ESSENTIAL";
return;
}
if ($article->{id} == $Constants::SHOPID) {
$$rmsg = "Sorry, these pages are essential to the store - they cannot be deleted - you may want to hide the store instead.";
+ $$rcode = "SHOP";
return;
}
return 1;
}
+=item remove
+
+Error codes:
+
+=over
+
+=item *
+
+ACCESS - access denied
+
+=item *
+
+CHILDREN - the article has children
+
+=item *
+
+ESSENTIAL - the article is marked essential
+
+=item *
+
+SHOP - the article is an essential part of the shop (the shop article
+itself)
+
+=back
+
+JSON success response: { success: 1, article_id: I<id> }
+
+=cut
+
sub remove {
my ($self, $req, $article, $articles) = @_;
+ $req->check_csrf("admin_remove_article")
+ or return $self->csrf_error($req, $article, "admin_remove_article", "Remove Article");
+
my $why_not;
- unless ($self->can_remove($req, $article, $articles, \$why_not)) {
- return $self->edit_form($req, $article, $articles, $why_not);
+ my $code;
+ unless ($self->can_remove($req, $article, $articles, \$why_not, \$code)) {
+ return $self->_service_error($req, $article, $articles, $why_not, {}, $code);
}
+ my $data = $article->data_only;
+
my $parentid = $article->{parentid};
$article->remove($req->cfg);
+ if ($req->is_ajax) {
+ return $req->json_content
+ (
+ success => 1,
+ article_id => $data->{id},
+ );
+ }
+
my $url = $req->cgi->param('r');
unless ($url) {
- my $urlbase = admin_base_url($req->cfg);
- $url = "$urlbase$ENV{SCRIPT_NAME}?id=$parentid";
- $url .= "&message=Article+deleted";
+ $url = $req->cfg->admin_url("add", { id => $parentid });
}
+
+ $req->flash_notice("msg:bse/admin/edit/remove", [ $data ]);
+
return BSE::Template->get_refresh($url, $self->{cfg});
}
sub unhide {
my ($self, $req, $article, $articles) = @_;
+ $req->check_csrf("admin_save_article")
+ or return $self->csrf_error($req, $article, "admin_save_article", "Unhide article");
+
if ($req->user_can(edit_field_edit_listed => $article)
&& $req->user_can(edit_save => $article)) {
$article->{listed} = 1;
$article->save;
- use Util 'generate_article';
generate_article($articles, $article) if $Constants::AUTO_GENERATE;
}
return $self->refresh($article, $req->cgi, undef, 'Article unhidden');
sub hide {
my ($self, $req, $article, $articles) = @_;
+ $req->check_csrf("admin_save_article")
+ or return $self->csrf_error($req, $article, "admin_save_article", "Hide article");
+
if ($req->user_can(edit_field_edit_listed => $article)
&& $req->user_can(edit_save => $article)) {
$article->{listed} = 0;
$article->save;
- use Util 'generate_article';
generate_article($articles, $article) if $Constants::AUTO_GENERATE;
}
my $r = $req->cgi->param('r');
menu => 0,
titleAlias => '',
linkAlias => '',
+ author => '',
+ summary => '',
+ category => '',
);
sub default_value {
if ($col eq 'threshold') {
my $parent = defined $article->{parentid} && $article->{parentid} != -1
- && Articles->getByPkey($article->{parentid});
+ && BSE::TB::Articles->getByPkey($article->{parentid});
$parent and return $parent->{threshold};
if ($col eq 'summaryLength') {
my $parent = defined $article->{parentid} && $article->{parentid} != -1
- && Articles->getByPkey($article->{parentid});
+ && BSE::TB::Articles->getByPkey($article->{parentid});
$parent and return $parent->{summaryLength};
if ($Constants::AUTO_GENERATE) {
require Util;
- Util::generate_article($articles, $article);
+ generate_article($articles, $article);
for my $regen_id (@extra_regen) {
my $regen = $articles->getByPkey($regen_id);
- Util::generate_low($articles, $regen, $self->{cfg});
+ BSE::Regen::generate_low($articles, $regen, $self->{cfg});
}
}
};
}
+sub iter_file_metas {
+ my ($self, $files, $rfile_index) = @_;
+
+ $$rfile_index < 0 || $$rfile_index >= @$files
+ and return;
+
+ my $file = $files->[$$rfile_index];
+
+ return $file->text_metadata;
+}
+
my %settable_fields = qw(title keyword author pageTitle);
if ($Constants::AUTO_GENERATE) {
require Util;
- Util::generate_article($articles, $article);
+ generate_article($articles, $article);
for my $regen_id (@extra_regen) {
my $regen = $articles->getByPkey($regen_id);
- Util::generate_low($articles, $regen, $self->{cfg});
+ BSE::Regen::generate_low($articles, $regen, $self->{cfg});
}
}
my %errors;
my $msg = $req->csrf_error;
$errors{_csrfp} = $msg;
- return $self->_service_error($req, $article, 'Articles', $msg, \%errors);
+ my $mymsg;
+ $article ||= $self->_dummy_article($req, 'BSE::TB::Articles', \$mymsg);
+ unless ($article) {
+ require BSE::Edit::Site;
+ my $site = BSE::Edit::Site->new(cfg=>$req->cfg, db=> BSE::DB->single);
+ return $site->edit_sections($req, 'BSE::TB::Articles', $mymsg);
+ }
+ return $self->_service_error($req, $article, 'BSE::TB::Articles', $msg, \%errors);
+}
+
+=item a_csrp
+
+Returns the csrf token for a given action.
+
+Must only be callable from Ajax requests.
+
+In general Ajax requests won't require a token, but some types of
+requests initiated by an Ajax based client might need a token, in
+particular: file uploads.
+
+=cut
+
+sub req_csrfp {
+ my ($self, $req, $article, $articles) = @_;
+
+ $req->is_ajax
+ or return $self->_service_error($req, $article, $articles,
+ "Only usable from Ajax", undef, "NOTAJAX");
+
+ $ENV{REQUEST_METHOD} eq 'POST'
+ or return $self->_service_error($req, $article, "BSE::TB::Articles",
+ "POST required for this action", {}, "NOTPOST");
+
+ my %errors;
+ my (@names) = $req->cgi->param("name");
+ @names or $errors{name} = "Missing parameter 'name'";
+ unless ($errors{name}) {
+ for my $name (@names) {
+ $name =~ /^\w+\z/
+ or $errors{name} = "Invalid name: must be an identifier";
+ }
+ }
+
+ keys %errors
+ and return $self->_service_error($req, $article, $articles,
+ "Invalid parameter", \%errors, "FIELD");
+
+ return $req->json_content
+ (
+ {
+ success => 1,
+ tokens =>
+ {
+ map { $_ => $req->get_csrf_token($_) } @names,
+ },
+ },
+ );
+}
+
+sub _article_kid_summary {
+ my ($article_id, $depth) = @_;
+
+ my @kids = BSE::DB->query(bseArticleKidSummary => $article_id);
+ if (--$depth > 0) {
+ for my $kid (@kids) {
+ $kid->{children} = [ _article_kid_summary($kid->{id}, $depth) ];
+ $kid->{allkids} = [ BSE::TB::Articles->allkid_summary($kid->{id}) ];
+ }
+ }
+
+ return @kids;
+}
+
+=item a_tree
+
+Returns a JSON tree of articles.
+
+Requires an article id (-1 to start from the root).
+
+Takes an optional tree depth. 1 only shows immediate children of the
+article.
+
+=cut
+
+sub req_tree {
+ my ($self, $req, $article, $articles) = @_;
+
+ my $depth = $req->cgi->param("depth");
+ defined $depth && $depth =~ /^\d+$/ and $depth >= 1
+ or $depth = 10000; # something large
+
+ $req->is_ajax
+ or return $self->_service_error($req, $article, $articles, "Only available to Ajax requests", {}, "NOTAJAX");
+
+ return $req->json_content
+ (
+ success => 1,
+ articles =>
+ [
+ _article_kid_summary($article->id, $depth),
+ ],
+ allkids =>
+ [
+ BSE::TB::Articles->allkid_summary($article->id)
+ ],
+ );
+}
+
+=item a_article
+
+Returns the article as JSON.
+
+Populates images with images and files with files.
+
+The article data is in the article member of the returned object.
+
+=cut
+
+sub req_article {
+ my ($self, $req, $article, $articles) = @_;
+
+ $req->is_ajax
+ or return $self->_service_error($req, $article, $articles, "Only available to Ajax requests", {}, "NOTAJAX");
+
+ return $req->json_content
+ (
+ success => 1,
+ article => $self->_article_data($req, $article),
+ );
+}
+
+sub templates_long {
+ my ($self, $article) = @_;
+
+ my @templates = $self->templates($article);
+
+ my $cfg = $self->{cfg};
+ return map
+ +{
+ name => $_,
+ description => $cfg->entry("template descriptions", $_, $_),
+ }, @templates;
+}
+
+sub _populate_config {
+ my ($self, $req, $article, $articles, $conf) = @_;
+
+ my $cfg = $req->cfg;
+ my %geos = $cfg->entries("thumb geometries");
+ my %defaults;
+ my @cols = $self->table_object($articles)->rowClass->columns;
+ shift @cols;
+ for my $col (@cols) {
+ my $def = $self->default_value($req, $article, $col);
+ defined $def and $defaults{$col} = $def;
+ }
+ my @templates = $self->templates($article);
+ $defaults{template} =
+ $self->default_template($article, $req->cfg, \@templates);
+
+ $conf->{templates} = [ $self->templates_long($article) ];
+ $conf->{thumb_geometries} =
+ [
+ map
+ {
+ +{
+ name => $_,
+ description => $cfg->entry("thumb geometry $_", "description", $_),
+ };
+ } sort keys %geos
+ ];
+ $conf->{defaults} = \%defaults;
+ $conf->{upload_progress} = $req->_tracking_uploads;
+ my @child_types = $self->child_types($article);
+ s/^BSE::Edit::// for @child_types;
+ $conf->{child_types} = \@child_types;
+ $conf->{flags} = [ $self->flags ];
+}
+
+=item a_config
+
+Returns configuration information as JSON.
+
+Returns an object of the form:
+
+ {
+ success: 1,
+ templates:
+ [
+ "template.tmpl":
+ {
+ description: "template.tmpl", // or from [template descriptions]
+ },
+ ...
+ ],
+ thumb_geometries:
+ [
+ "geoid":
+ {
+ description: "geoid", // or from [thumb geometry id].description
+ },
+ ],
+ defaults:
+ {
+ field: value,
+ ...
+ },
+ child_types: [ "Article" ],
+ flags:
+ [
+ { id => "A", desc => "description" },
+ ...
+ ],
+ // possibible custom data
+ }
+
+To define custom data add entries to the [extra a_config] section,
+keys become the keys in the returned structure pointing at hashes
+containing that section from the system configuration. Custom keys
+may not conflict with system defined keys.
+
+=cut
+
+sub req_config {
+ my ($self, $req, $article, $articles) = @_;
+
+ $req->is_ajax
+ or return $self->_service_error($req, $article, $articles, "Only available to Ajax requests", {}, "NOTAJAX");
+
+ my %conf;
+ $self->_populate_config($req, $article, $articles, \%conf);
+ $conf{success} = 1;
+
+ my $cfg = $req->cfg;
+ my %custom = $cfg->entries("extra a_config");
+ for my $key (keys %custom) {
+ exists $conf{$key} and next;
+
+ my $section = $custom{$key};
+ $section =~ s/\{(level|generator|parentid|template)\}/$article->{$1}/g;
+
+ $section eq "db" and die;
+
+ $conf{$key} = { $cfg->entries($section) };
+ }
+
+ return $req->json_content
+ (
+ \%conf
+ );
}
1;