1 package BSE::Edit::Article;
3 use base qw(BSE::Edit::Base);
4 use BSE::Util::Tags qw(tag_error_img tag_article tag_object);
5 use BSE::Util::SQL qw(now_sqldate now_sqldatetime);
7 use BSE::Util::HTML qw(:default popup_menu);
9 use BSE::CfgInfo qw(custom_class admin_base_url cfg_image_dir cfg_dist_image_uri cfg_image_uri);
10 use BSE::Util::Iterate;
12 use BSE::Util::ContentType qw(content_type);
13 use BSE::Regen 'generate_article';
14 use DevHelp::Date qw(dh_parse_date dh_parse_sql_date);
15 use List::Util qw(first);
16 use constant MAX_FILE_DISPLAYNAME_LENGTH => 255;
17 use constant ARTICLE_CUSTOM_FIELDS_CFG => "article custom fields";
19 our $VERSION = "1.057";
23 BSE::Edit::Article - editing functionality for BSE articles
27 Provides the base article editing functionality.
29 This is badly organized and documented.
38 my ($self, $req) = @_;
41 # AJAX/Prototype request
42 return $req->json_content
46 message => "Access forbidden: user not logged on",
48 error_code => "LOGON",
52 elsif ($req->cgi->param('_service')) {
55 content => 'Access Forbidden: login timed out',
57 "Status: 403", # forbidden
62 BSE::Template->get_refresh($req->url('logon'), $req->cfg);
66 sub article_dispatch {
67 my ($self, $req, $article, $articles) = @_;
69 BSE::Permissions->check_logon($req)
70 or return $self->not_logged_on($req);
74 my %actions = $self->article_actions;
75 for my $check (keys %actions) {
76 if ($cgi->param($check) || $cgi->param("$check.x")
77 || $cgi->param("a_$check") || $cgi->param("a_$check.x")) {
84 ($action, @extraargs) = $self->other_article_actions($cgi);
87 my $method = $actions{$action};
88 return $self->$method($req, $article, $articles, @extraargs);
91 sub noarticle_dispatch {
92 my ($self, $req, $articles) = @_;
94 BSE::Permissions->check_logon($req)
95 or return BSE::Template->get_refresh($req->url('logon'), $req->cfg);
98 my $article = $self->_dummy_article($req, $articles, \$mymsg);
100 require BSE::Edit::Site;
101 my $site = BSE::Edit::Site->new(cfg=>$req->cfg, db=> BSE::DB->single);
102 return $site->edit_sections($req, $articles, $mymsg);
107 my %actions = $self->noarticle_actions;
108 for my $check (keys %actions) {
109 if ($cgi->param($check) || $cgi->param("$check.x")) {
114 my $method = $actions{$action};
115 return $self->$method($req, $article, $articles);
118 sub article_actions {
125 add_stepkid => 'add_stepkid',
126 del_stepkid => 'del_stepkid',
127 save_stepkids => 'save_stepkids',
128 add_stepparent => 'add_stepparent',
129 del_stepparent => 'del_stepparent',
130 save_stepparents => 'save_stepparents',
131 artimg => 'save_image_changes',
132 addimg => 'add_image',
133 a_edit_image => 'req_edit_image',
134 a_save_image => 'req_save_image',
135 a_order_images => 'req_order_images',
137 showimages => 'show_images',
138 process => 'save_image_changes',
139 removeimg => 'remove_img',
140 moveimgup => 'move_img_up',
141 moveimgdown => 'move_img_down',
142 filelist => 'filelist',
143 fileadd => 'fileadd',
144 fileswap => 'fileswap',
145 filedel => 'filedel',
146 filesave => 'filesave',
147 a_edit_file => 'req_edit_file',
148 a_save_file => 'req_save_file',
151 a_thumb => 'req_thumb',
152 a_ajax_get => 'req_ajax_get',
153 a_ajax_save_body => 'req_ajax_save_body',
154 a_ajax_set => 'req_ajax_set',
155 a_filemeta => 'req_filemeta',
156 a_csrfp => 'req_csrfp',
157 a_tree => 'req_tree',
158 a_article => 'req_article',
159 a_config => 'req_config',
160 a_restepkid => 'req_restepkid',
164 sub other_article_actions {
165 my ($self, $cgi) = @_;
167 for my $param ($cgi->param) {
168 if ($param =~ /^removeimg_(\d+)(\.x)?$/) {
169 return ('removeimg', $1 );
176 sub noarticle_actions {
181 a_csrfp => 'req_csrfp',
182 a_config => 'req_config',
187 my ($self, $parentid, $articles) = @_;
189 if ($parentid == -1) {
193 title=>'All Sections',
200 return $articles->getByPkey($parentid);
205 my ($object, $args) = @_;
207 my $value = $object->{$args};
208 defined $value or $value = '';
209 if ($value =~ /\cJ/ && $value =~ /\cM/) {
216 my ($object, $args) = @_;
218 my $value = $object->{$args};
219 defined $value or $value = '';
220 if ($value =~ /\cJ/ && $value =~ /\cM/) {
223 escape_html($value, '<>&"');
227 my ($level, $cfg) = @_;
229 escape_html($cfg->entry('level names', $level, 'Article'));
238 sub reparent_updown {
242 sub should_be_catalog {
243 my ($self, $article, $parent, $articles) = @_;
245 if ($article->{parentid} && (!$parent || $parent->{id} != $article->{parentid})) {
246 $parent = $articles->getByPkey($article->{id});
249 my $shopid = $self->cfg->entryErr('articles', 'shop');
251 return $article->{parentid} && $parent &&
252 ($article->{parentid} == $shopid ||
253 $parent->{generator} eq 'BSE::Generate::Catalog');
256 sub possible_parents {
257 my ($self, $article, $articles, $req) = @_;
262 my $shopid = $self->cfg->entryErr('articles', 'shop');
263 my @parents = $articles->getBy('level', $article->{level}-1);
264 @parents = grep { $_->{generator} eq 'BSE::Generate::Article'
265 && $_->{id} != $shopid } @parents;
267 # user can only select parent they can add to
268 @parents = grep $req->user_can('edit_add_child', $_), @parents;
270 @values = ( map {$_->{id}} @parents );
271 %labels = ( map { $_->{id} => "$_->{title} ($_->{id})" } @parents );
273 if ($article->{level} == 1 && $req->user_can('edit_add_child')) {
275 $labels{-1} = "No parent - this is a section";
278 if ($article->{id} && $self->reparent_updown($article)) {
279 # we also list the siblings and grandparent (if any)
280 my @siblings = grep $_->{id} != $article->{id} && $_->{id} != $shopid,
281 $articles->getBy(parentid => $article->{parentid});
282 @siblings = grep $req->user_can('edit_add_child', $_), @siblings;
283 push @values, map $_->{id}, @siblings;
284 @labels{map $_->{id}, @siblings} =
285 map { "-- move down a level -- $_->{title} ($_->{id})" } @siblings;
287 if ($article->{parentid} != -1) {
288 my $parent = $articles->getByPkey($article->{parentid});
289 if ($parent->{parentid} != -1) {
290 my $gparent = $articles->getByPkey($parent->{parentid});
291 if ($req->user_can('edit_add_child', $gparent)) {
292 push @values, $gparent->{id};
293 $labels{$gparent->{id}} =
294 "-- move up a level -- $gparent->{title} ($gparent->{id})";
298 if ($req->user_can('edit_add_child')) {
300 $labels{-1} = $req->catmsg("msg:bse/admin/edit/uplabelsect");
306 return (\@values, \%labels);
310 my ($self, $article, $articles, $cgi, $req, $what) = @_;
312 if ($what eq 'listed') {
313 my @values = qw(0 1);
314 my %labels = ( 0=>"No", 1=>"Yes");
315 if ($article->{level} <= 2) {
316 $labels{2} = "In Sections, but not menu";
320 $labels{2} = "In content, but not menus";
323 return popup_menu(-name=>'listed',
326 -default=>$article->{listed});
329 my ($values, $labels) = $self->possible_parents($article, $articles, $req);
331 if (defined $article->{parentid}) {
332 $html = popup_menu(-name=>'parentid',
335 -default => $article->{parentid},
339 $html = popup_menu(-name=>'parentid',
345 # munge the html - we display a default value, so we need to wrap the
346 # default <select /> around this one
347 $html =~ s!^<select[^>]+>|</select>!!gi;
353 my ($arg, $acts, $funcname, $templater) = @_;
354 my ($func, $args) = split ' ', $arg, 2;
355 return $templater->perform($acts, $func, $args) ? 'checked' : '';
358 sub iter_get_images {
359 my ($self, $article) = @_;
361 $article->{id} or return;
362 $self->get_images($article);
366 my ($article, $articles) = @_;
369 $article->{id} or return;
370 if (UNIVERSAL::isa($article, 'BSE::TB::Article')) {
371 @children = $article->children;
373 elsif ($article->{id}) {
374 @children = $articles->children($article->{id});
377 return sort { $b->{displayOrder} <=> $a->{displayOrder} } @children;
380 sub tag_if_have_child_type {
381 my ($level, $cfg) = @_;
383 defined $cfg->entry("level names", $level+1);
387 my ($args, $acts, $isname, $templater) = @_;
389 my ($func, $funcargs) = split ' ', $args, 2;
390 return $templater->perform($acts, $func, $funcargs) ? 'Yes' : 'No';
393 sub default_template {
394 my ($self, $article, $cfg, $templates) = @_;
396 if ($article->{parentid}) {
397 my $template = $cfg->entry("children of $article->{parentid}", "template");
399 if $template && grep $_ eq $template, @$templates;
401 if ($article->{level}) {
402 my $template = $cfg->entry("level $article->{level}", "template");
404 if $template && grep $_ eq $template, @$templates;
406 return $templates->[0];
410 my ($self, $article, $cfg, $cgi) = @_;
412 my @templates = sort { $a->{name} cmp $b->{name} } $self->templates_long($article);
414 if ($article->{template} && grep $_->{name} eq $article->{template}, @templates) {
415 $default = $article->{template};
418 my @template_names = map $_->{name}, @templates;
419 $default = $self->default_template($article, $cfg, \@template_names);
426 $_->{name} eq $_->{description}
428 : "$_->{description} ($_->{name})"
431 return popup_menu(-name => 'template',
432 -values => [ map $_->{name}, @templates ],
434 -default => $default,
439 my ($self, $article) = @_;
442 my $imagedir = cfg_image_dir($self->cfg);
443 if (opendir TITLE_IMAGES, "$imagedir/titles") {
445 grep -f "$imagedir/titles/$_" && /\.(gif|jpeg|jpg|png)$/i,
446 readdir TITLE_IMAGES;
447 closedir TITLE_IMAGES;
453 sub tag_title_images {
454 my ($self, $article, $cfg, $cgi) = @_;
456 my @images = $self->title_images($article);
457 my @values = ( '', @images );
458 my %labels = ( '' => 'None', map { $_ => $_ } @images );
460 popup_menu(-name=>'titleImage',
463 -default=>$article->{id} ? $article->{titleImage} : '',
467 sub base_template_dirs {
472 my ($self, $article) = @_;
474 my @dirs = $self->base_template_dirs;
475 if (my $parentid = $article->{parentid}) {
476 my $section = "children of $parentid";
477 if (my $dirs = $self->cfg->entry($section, 'template_dirs')) {
478 push @dirs, split /,/, $dirs;
481 if (my $id = $article->{id}) {
482 my $section = "article $id";
483 if (my $dirs = $self->{cfg}->entry($section, 'template_dirs')) {
484 push @dirs, split /,/, $dirs;
487 if ($article->{level}) {
488 push @dirs, $article->{level};
489 my $dirs = $self->{cfg}->entry("level $article->{level}", 'template_dirs');
490 push @dirs, split /,/, $dirs if $dirs;
497 my ($self, $article) = @_;
499 my @dirs = $self->template_dirs($article);
501 my @basedirs = BSE::Template->template_dirs($self->{cfg});
502 for my $basedir (@basedirs) {
503 for my $dir (@dirs) {
504 my $path = File::Spec->catdir($basedir, $dir);
506 if (opendir TEMPLATE_DIR, $path) {
507 push(@templates, sort map "$dir/$_",
508 grep -f "$path/$_" && /\.(tmpl|html)$/i, readdir TEMPLATE_DIR);
509 closedir TEMPLATE_DIR;
515 # eliminate any dups, and order it nicely
517 @templates = sort { lc($a) cmp lc($b) }
518 grep !$seen{$_}++, @templates;
520 return (@templates, $self->extra_templates($article));
523 sub extra_templates {
524 my ($self, $article) = @_;
526 my $basedir = $self->{cfg}->entryVar('paths', 'templates');
528 if (my $id = $article->{id}) {
529 push @templates, 'index.tmpl'
530 if $id == 1 && -f "$basedir/index.html";
531 push @templates, 'index2.tmpl'
532 if $id == 2 && -f "$basedir/index2.html";
533 my $shopid = $self->{cfg}->entryErr('articles', 'shop');
534 push @templates, "shop_sect.tmpl"
535 if $id == $shopid && -f "$basedir/shop_sect.tmpl";
536 my $section = "article $id";
537 my $extras = $self->{cfg}->entry($section, 'extra_templates');
538 push @templates, grep /\.(tmpl|html)$/i, split /,/, $extras
546 my ($self, $articles) = @_;
548 return $articles->categories;
554 return '' unless $article->{id} && $article->{id} != -1;
556 <a href="$ENV{SCRIPT_NAME}?id=$article->{parentid}">Edit parent</a> |
563 return unless $article->{id} && $article->{id} > 0;
567 sub _load_step_kids {
568 my ($article, $step_kids) = @_;
570 require BSE::TB::OtherParents;
571 my @stepkids = BSE::TB::OtherParents->getBy(parentId=>$article->{id}) if $article->{id};
572 %$step_kids = map { $_->{childId} => $_ } @stepkids;
573 $step_kids->{loaded} = 1;
576 sub tag_if_step_kid {
577 my ($article, $allkids, $rallkid_index, $step_kids) = @_;
579 _load_step_kids($article, $step_kids) unless $step_kids->{loaded};
581 my $kid = $allkids->[$$rallkid_index]
583 exists $step_kids->{$kid->{id}};
587 my ($article, $allkids, $rallkid_index, $step_kids, $arg) = @_;
589 _load_step_kids($article, $step_kids) unless $step_kids->{loaded};
591 my $kid = $allkids->[$$rallkid_index]
593 my $step_kid = $step_kids->{$kid->{id}}
596 #print STDERR "found kid (want $arg): ", Dumper($kid), Dumper($step_kid);
597 escape_html($step_kid->{$arg});
600 sub tag_move_stepkid {
601 my ($self, $cgi, $req, $article, $allkids, $rallkids_index, $arg,
602 $acts, $funcname, $templater) = @_;
604 $req->user_can(edit_reorder_children => $article)
607 @$allkids > 1 or return '';
609 my ($img_prefix, $urladd) = DevHelp::Tags->get_parms($arg, $acts, $templater);
610 $img_prefix = '' unless defined $img_prefix;
611 $urladd = '' unless defined $urladd;
613 my $cgi_uri = $self->{cfg}->entry('uri', 'cgi', '/cgi-bin');
614 my $url = $ENV{SCRIPT_NAME} . "?id=$article->{id}";
615 if ($cgi->param('_t')) {
616 $url .= "&_t=".$cgi->param('_t');
621 if ($$rallkids_index < $#$allkids) {
622 $down_url = "$cgi_uri/admin/move.pl?stepparent=$article->{id}&d=swap&id=$allkids->[$$rallkids_index]{id}&other=$allkids->[$$rallkids_index+1]{id}";
625 if ($$rallkids_index > 0) {
626 $up_url = "$cgi_uri/admin/move.pl?stepparent=$article->{id}&d=swap&id=$allkids->[$$rallkids_index]{id}&other=$allkids->[$$rallkids_index-1]{id}";
629 return make_arrows($req->cfg, $down_url, $up_url, $url, $img_prefix);
632 sub possible_stepkids {
633 my ($req, $article, $articles, $stepkids) = @_;
635 $req->user_can(edit_stepkid_add => $article)
641 my @possible = sort { lc $a->{title} cmp lc $b->{title} }
642 $article->possible_stepchildren;
643 if ($req->access_control && $req->cfg->entry('basic', 'access_filter_steps', 0)) {
644 @possible = grep $req->user_can(edit_stepparent_add => $_->{id}), @possible;
649 sub tag_possible_stepkids {
650 my ($step_kids, $req, $article, $possstepkids, $articles, $cgi) = @_;
652 _load_step_kids($article, $step_kids) unless $step_kids->{loaded};
653 @$possstepkids = possible_stepkids($req, $article, $articles, $step_kids)
654 unless @$possstepkids;
655 my %labels = map { $_->{id} => "$_->{title} ($_->{id})" } @$possstepkids;
657 popup_menu(-name=>'stepkid',
658 -values=> [ map $_->{id}, @$possstepkids ],
659 -labels => \%labels);
662 sub tag_if_possible_stepkids {
663 my ($step_kids, $req, $article, $possstepkids, $articles, $cgi) = @_;
665 _load_step_kids($article, $step_kids) unless $step_kids->{loaded};
666 @$possstepkids = possible_stepkids($req, $article, $articles, $step_kids)
667 unless @$possstepkids;
672 sub iter_get_stepparents {
675 return unless $article->{id} && $article->{id} > 0;
677 require BSE::TB::OtherParents;
678 BSE::TB::OtherParents->getBy(childId=>$article->{id});
681 sub tag_ifStepParents {
682 my ($args, $acts, $funcname, $templater) = @_;
684 return $templater->perform($acts, 'ifStepparents', '');
687 sub tag_stepparent_targ {
688 my ($article, $targs, $rindex, $arg) = @_;
690 if ($article->{id} && $article->{id} > 0 && !@$targs) {
691 @$targs = $article->step_parents;
693 escape_html($targs->[$$rindex]{$arg});
696 sub tag_move_stepparent {
697 my ($self, $cgi, $req, $article, $stepparents, $rindex, $arg,
698 $acts, $funcname, $templater) = @_;
700 $req->user_can(edit_reorder_stepparents => $article)
703 @$stepparents > 1 or return '';
705 my ($img_prefix, $urladd) = DevHelp::Tags->get_parms($arg, $acts, $templater);
706 $img_prefix = '' unless defined $img_prefix;
707 $urladd = '' unless defined $urladd;
709 my $cgi_uri = $self->{cfg}->entry('uri', 'cgi', '/cgi-bin');
710 my $images_uri = cfg_dist_image_uri();
712 my $url = $ENV{SCRIPT_NAME} . "?id=$article->{id}";
713 if ($cgi->param('_t')) {
714 $url .= "&_t=".$cgi->param('_t');
717 $url .= "#stepparents";
718 my $blank = qq!<img src="$images_uri/trans_pixel.gif" width="17" height="13" border="0" align="absbottom" alt="" />!;
720 if ($$rindex < $#$stepparents) {
721 $down_url = "$cgi_uri/admin/move.pl?stepchild=$article->{id}&id=$stepparents->[$$rindex]{parentId}&d=swap&other=$stepparents->[$$rindex+1]{parentId}";
725 $up_url = "$cgi_uri/admin/move.pl?stepchild=$article->{id}&id=$stepparents->[$$rindex]{parentId}&d=swap&other=$stepparents->[$$rindex-1]{parentId}";
728 return make_arrows($req->cfg, $down_url, $up_url, $url, $img_prefix);
731 sub _stepparent_possibles {
732 my ($req, $article, $articles, $targs) = @_;
734 $req->user_can(edit_stepparent_add => $article)
740 @$targs = $article->step_parents unless @$targs;
741 my %targs = map { $_->{id}, 1 } @$targs;
742 my @possibles = $article->possible_stepparents;
743 if ($req->access_control && $req->cfg->entry('basic', 'access_filter_steps', 0)) {
744 @possibles = grep $req->user_can(edit_stepkid_add => $_->{id}), @possibles;
746 @possibles = sort { lc $a->{title} cmp lc $b->{title} } @possibles;
751 sub tag_if_stepparent_possibles {
752 my ($req, $article, $articles, $targs, $possibles) = @_;
754 if ($article->{id} && $article->{id} > 0 && !@$possibles) {
755 @$possibles = _stepparent_possibles($req, $article, $articles, $targs);
760 sub tag_stepparent_possibles {
761 my ($cgi, $req, $article, $articles, $targs, $possibles) = @_;
763 if ($article->{id} && $article->{id} > 0 && !@$possibles) {
764 @$possibles = _stepparent_possibles($req, $article, $articles, $targs);
766 popup_menu(-name=>'stepparent',
767 -values => [ map $_->{id}, @$possibles ],
768 -labels => { map { $_->{id}, "$_->{title} ($_->{id})" }
773 my ($self, $article) = @_;
775 return $self->get_files($article);
779 my ($self, $article) = @_;
781 return unless $article->{id} && $article->{id} > 0;
783 return $article->files;
786 sub tag_edit_parent {
789 return '' unless $article->{id} && $article->{id} != -1;
792 <a href="$ENV{SCRIPT_NAME}?id=$article->{parentid}">Edit parent</a> |
796 sub tag_if_children {
797 my ($args, $acts, $funcname, $templater) = @_;
799 return $templater->perform($acts, 'ifChildren', '');
803 my ($self, $req, $article, $kids, $rindex, $arg,
804 $acts, $funcname, $templater) = @_;
806 $req->user_can('edit_reorder_children', $article)
809 @$kids > 1 or return '';
811 $$rindex >=0 && $$rindex < @$kids
812 or return '** movechild can only be used in the children iterator **';
814 my ($img_prefix, $urladd) = DevHelp::Tags->get_parms($arg, $acts, $templater);
815 $img_prefix = '' unless defined $img_prefix;
816 $urladd = '' unless defined $urladd;
818 my $cgi_uri = $self->{cfg}->entry('uri', 'cgi', '/cgi-bin');
819 my $images_uri = cfg_dist_image_uri();
820 my $urlbase = admin_base_url($req->cfg);
821 my $refresh_url = "$urlbase$ENV{SCRIPT_NAME}?id=$article->{id}";
822 my $t = $req->cgi->param('_t');
823 if ($t && $t =~ /^\w+$/) {
824 $refresh_url .= "&_t=$t";
827 $refresh_url .= $urladd;
829 my $id = $kids->[$$rindex]{id};
831 if ($$rindex < $#$kids) {
832 $down_url = "$cgi_uri/admin/move.pl?id=$id&d=down&edit=1&all=1";
836 $up_url = "$cgi_uri/admin/move.pl?id=$id&d=up&edit=1&all=1"
839 return make_arrows($req->cfg, $down_url, $up_url, $refresh_url, $img_prefix);
843 my ($self, $articles, $article) = @_;
845 my @cats = $self->categories($articles);
847 my %labels = map { $_->{id}, $_->{name} } @cats;
849 return popup_menu(-name => 'category',
850 -values => [ map $_->{id}, @cats ],
852 -default => $article->{category});
856 my ($cfg, $article, $args, $acts, $funcname, $templater) = @_;
857 my ($which, $name) = split / /, $args, 2;
861 && ($gen_class = $templater->perform($acts, $which, 'generator'))) {
862 eval "use $gen_class";
864 my $gen = $gen_class->new(top => $article, cfg => $cfg);
865 my $link = $gen->edit_link($templater->perform($acts, $which, 'id'));
866 return qq!<a href="$link">$name</a>!;
873 my ($req, $article, $rindex, $images, $arg,
874 $acts, $funcname, $templater) = @_;
876 $req->user_can(edit_images_reorder => $article)
879 @$images > 1 or return '';
881 $$rindex >= 0 && $$rindex < @$images
882 or return '** imgmove can only be used in image iterator **';
884 my ($img_prefix, $urladd) = DevHelp::Tags->get_parms($arg, $acts, $templater);
885 $img_prefix = '' unless defined $img_prefix;
886 $urladd = '' unless defined $urladd;
888 my $urlbase = admin_base_url($req->cfg);
889 my $url = "$urlbase$ENV{SCRIPT_NAME}?id=$article->{id}";
890 my $t = $req->cgi->param('_t');
891 if ($t && $t =~ /^\w+$/) {
896 my $image = $images->[$$rindex];
897 my $csrfp = $req->get_csrf_token("admin_move_image");
898 my $baseurl = "$ENV{SCRIPT_NAME}?id=$article->{id}&imageid=$image->{id}&";
899 $baseurl .= "_csrfp=$csrfp&";
901 if ($$rindex < $#$images) {
902 $down_url = $baseurl . "moveimgdown=1";
906 $up_url = $baseurl . "moveimgup=1";
908 return make_arrows($req->cfg, $down_url, $up_url, $url, $img_prefix);
912 my ($self, $req, $article, $files, $rindex, $arg,
913 $acts, $funcname, $templater) = @_;
915 $req->user_can('edit_files_reorder', $article)
918 @$files > 1 or return '';
920 my ($img_prefix, $urladd) = DevHelp::Tags->get_parms($arg, $acts, $templater);
921 $img_prefix = '' unless defined $img_prefix;
922 $urladd = '' unless defined $urladd;
924 $$rindex >= 0 && $$rindex < @$files
925 or return '** movefiles can only be used in the files iterator **';
927 my $urlbase = admin_base_url($req->cfg);
928 my $url = "$urlbase$ENV{SCRIPT_NAME}?id=$article->{id}$urladd";
929 my $t = $req->cgi->param('_t');
930 if ($t && $t =~ /^\w+$/) {
935 my $csrfp = $req->get_csrf_token("admin_move_file");
936 my $baseurl = "$ENV{SCRIPT_NAME}?fileswap=1&id=$article->{id}&";
937 $baseurl .= "_csrfp=$csrfp&";
938 if ($$rindex < $#$files) {
939 $down_url = $baseurl . "file1=$files->[$$rindex]{id}&file2=$files->[$$rindex+1]{id}";
943 $up_url = $baseurl . "file1=$files->[$$rindex]{id}&file2=$files->[$$rindex-1]{id}";
946 return make_arrows($req->cfg, $down_url, $up_url, $url, $img_prefix);
950 my ($article, $cgi, $args, $acts, $funcname, $templater) = @_;
952 my ($col, $func, $funcargs) = split ' ', $args, 3;
953 my $value = $cgi->param($col);
954 if (defined $value) {
955 return escape_html($value);
959 return $templater->perform($acts, $func, $funcargs);
962 $value = $article->{$args};
963 defined $value or $value = '';
964 return escape_html($value);
969 sub iter_admin_users {
970 require BSE::TB::AdminUsers;
972 BSE::TB::AdminUsers->all;
975 sub iter_admin_groups {
976 require BSE::TB::AdminGroups;
978 BSE::TB::AdminGroups->all;
981 sub tag_if_field_perm {
982 my ($req, $article, $field) = @_;
984 unless ($field =~ /^\w+$/) {
985 print STDERR "Bad fieldname '$field'\n";
988 if ($article->{id}) {
989 return $req->user_can("edit_field_edit_$field", $article);
992 #print STDERR "adding, always successful\n";
998 my ($self, $req, $article, $args, $acts, $funcname, $templater) = @_;
1000 my ($col, $func, $funcargs) = split ' ', $args, 3;
1001 if ($article->{id}) {
1003 return $templater->perform($acts, $func, $funcargs);
1006 my $value = $article->{$args};
1007 defined $value or $value = '';
1008 return escape_html($value, '<>&"');
1012 my $value = $self->default_value($req, $article, $col);
1013 defined $value or $value = '';
1014 return escape_html($value, '<>&"');
1024 sub tag_if_flag_set {
1025 my ($article, $arg, $acts, $funcname, $templater) = @_;
1027 my @args = DevHelp::Tags->get_parms($arg, $acts, $templater);
1030 return index($article->{flags}, $args[0]) >= 0;
1034 my ($article, $articles) = @_;
1037 my $temp = $article;
1038 defined($temp->{parentid}) or return;
1039 while ($temp->{parentid} > 0
1040 and my $crumb = $articles->getByPkey($temp->{parentid})) {
1041 unshift @crumbs, $crumb;
1049 my ($args, $acts, $funcname, $templater) = @_;
1051 exists $acts->{$args} or return "** need an article name **";
1052 my $generator = $templater->perform($acts, $args, 'generator');
1054 $generator =~ /^(?:BSE::)?Generate::(\w+)$/
1055 or return "** invalid generator $generator **";
1060 sub _get_thumbs_class {
1063 $self->{cfg}->entry('editor', 'allow_thumb', 0)
1066 my $class = $self->{cfg}->entry('editor', 'thumbs_class')
1069 (my $filename = "$class.pm") =~ s!::!/!g;
1070 eval { require $filename; };
1072 print STDERR "** Error loading thumbs_class $class ($filename): $@\n";
1076 eval { $obj = $class->new($self->{cfg}) };
1078 print STDERR "** Error creating thumbs objects $class: $@\n";
1085 sub tag_thumbimage {
1086 my ($cfg, $thumbs_obj, $current_image, $args) = @_;
1088 $thumbs_obj or return '';
1090 $$current_image or return '** no current image **';
1092 my $imagedir = cfg_image_dir($cfg);
1094 my $filename = "$imagedir/$$current_image->{image}";
1095 -e $filename or return "** image file missing **";
1097 defined $args && $args =~ /\S/
1098 or $args = "editor";
1100 my $image = $$current_image;
1101 return $image->thumb
1109 sub tag_file_display {
1110 my ($self, $files, $file_index) = @_;
1112 $$file_index >= 0 && $$file_index < @$files
1113 or return "* file_display only usable inside a files iterator *";
1114 my $file = $files->[$$file_index];
1116 my $disp_type = $self->cfg->entry("editor", "file_display", "");
1118 return $file->inline
1121 field => $disp_type,
1126 my ($self, $cfg, $rcurrent, $args) = @_;
1131 my ($align, $rest) = split ' ', $args, 2;
1133 if ($align && exists $im->{$align}) {
1134 if ($align eq 'src') {
1135 return escape_html($im->image_url($self->{cfg}));
1138 return escape_html($im->{$align});
1142 return $im->formatted
1152 my ($self, $article) = @_;
1157 return $article->tag_objects;
1160 my %base_custom_validation =
1221 require DevHelp::Validate;
1222 DevHelp::Validate->import;
1223 return DevHelp::Validate::dh_configure_fields
1225 \%base_custom_validation,
1227 ARTICLE_CUSTOM_FIELDS_CFG,
1228 BSE::DB->single->dbh,
1232 sub _custom_fields {
1235 my $fields = $self->custom_fields;
1237 for my $key (keys %$fields) {
1238 $fields->{$key}{description}
1239 and $active{$key} = $fields->{$key};
1247 =head1 Common Edit Page Tags
1255 C<article> - the article being edited. This is a dummy article when a
1256 new article is being created.
1260 C<isnew> - true if a new article is being created.
1264 C<custom> - describes custom tags.
1268 C<errors> - errors from the last submission of the page.
1272 C<image_stores> - a function returning an array of possible image
1277 C<thumbs> - for the image list, whether thumbs should be displayed
1278 instead of full size images.
1282 C<can_thumbs> - true if thumbnails are available.
1289 my ($self, $acts, $request, $article, $articles, $msg, $errors) = @_;
1291 my $cgi = $request->cgi;
1292 my $show_full = $cgi->param('f_showfull');
1293 my $if_error = $msg || ($errors && keys %$errors) || $request->cgi->param("_e");
1294 #$msg ||= join "\n", map escape_html($_), $cgi->param('message'), $cgi->param('m');
1295 $msg .= $request->message($errors);
1297 if ($article->{id}) {
1298 if ($article->{parentid} > 0) {
1299 $parent = $article->parent;
1302 $parent = { title=>"No parent - this is a section", id=>-1 };
1306 $parent = { title=>"How did we get here?", id=>0 };
1308 $request->set_article(article => $article);
1309 $request->set_variable(ifnew => !$article->{id});
1310 my $cfg = $self->{cfg};
1311 my $mbcs = $cfg->entry('html', 'mbcs', 0);
1312 my $tag_hash = $mbcs ? \&tag_hash_mbcs : \&tag_hash;
1313 my $thumbs_obj_real = $self->_get_thumbs_class();
1314 my $thumbs_obj = $show_full ? undef : $thumbs_obj_real;
1325 my $stepparent_index;
1326 my @stepparent_targs;
1327 my @stepparentpossibles;
1332 my $it = BSE::Util::Iterate->new;
1333 my $ito = BSE::Util::Iterate::Objects->new;
1334 my $ita = BSE::Util::Iterate::Article->new(req => $request);
1336 my $custom = $self->_custom_fields;
1337 # only return the fields that are defined
1338 $request->set_variable(custom => $custom);
1339 $request->set_variable(errors => $errors || {});
1340 my $article_type = $cfg->entry('level names', $article->{level}, 'Article');
1341 $request->set_variable(article_type => $article_type);
1342 $request->set_variable(thumbs => defined $thumbs_obj);
1343 $request->set_variable(can_thumbs => defined $thumbs_obj_real);
1344 $request->set_variable(image_stores =>
1346 $self->iter_image_stores;
1351 $request->admin_tags,
1352 article => sub { tag_article($article, $cfg, $_[0]) },
1353 old => [ \&tag_old, $article, $cgi ],
1354 default => [ \&tag_default, $self, $request, $article ],
1355 articleType => escape_html($article_type),
1356 parentType => [ \&tag_art_type, $article->{level}-1, $cfg ],
1357 ifNew => [ \&tag_if_new, $article ],
1358 list => [ \&tag_list, $self, $article, $articles, $cgi, $request ],
1359 script => $ENV{SCRIPT_NAME},
1360 level => $article->{level},
1361 checked => \&tag_checked,
1363 ([ \&iter_get_images, $self, $article ], 'image', 'images', \@images,
1364 \$image_index, undef, \$current_image),
1365 image => [ tag_image => $self, $cfg, \$current_image ],
1366 thumbimage => [ \&tag_thumbimage, $cfg, $thumbs_obj, \$current_image ],
1367 ifThumbs => defined($thumbs_obj),
1368 ifCanThumbs => defined($thumbs_obj_real),
1369 imgmove => [ \&tag_imgmove, $request, $article, \$image_index, \@images ],
1371 ifError => $if_error,
1374 code => [ \&iter_get_kids, $article, $articles ],
1376 plural => 'children',
1378 index => \$child_index,
1380 ifchildren => \&tag_if_children,
1381 childtype => [ \&tag_art_type, $article->{level}+1, $cfg ],
1382 ifHaveChildType => [ \&tag_if_have_child_type, $article->{level}, $cfg ],
1383 movechild => [ \&tag_movechild, $self, $request, $article, \@children,
1386 templates => [ \&tag_templates, $self, $article, $cfg, $cgi ],
1387 titleImages => [ \&tag_title_images, $self, $article, $cfg, $cgi ],
1388 editParent => [ \&tag_edit_parent, $article ],
1391 code => [ \&iter_allkids, $article ],
1395 index => \$allkid_index,
1398 [ \&tag_if_step_kid, $article, \@allkids, \$allkid_index, \%stepkids ],
1399 stepkid => [ \&tag_step_kid, $article, \@allkids, \$allkid_index,
1402 [ \&tag_move_stepkid, $self, $cgi, $request, $article, \@allkids,
1404 possible_stepkids =>
1405 [ \&tag_possible_stepkids, \%stepkids, $request, $article,
1406 \@possstepkids, $articles, $cgi ],
1408 [ \&tag_if_possible_stepkids, \%stepkids, $request, $article,
1409 \@possstepkids, $articles, $cgi ],
1412 code => [ \&iter_get_stepparents, $article ],
1413 single => 'stepparent',
1414 plural => 'stepparents',
1415 data => \@stepparents,
1416 index => \$stepparent_index,
1418 ifStepParents => \&tag_ifStepParents,
1420 [ \&tag_stepparent_targ, $article, \@stepparent_targs,
1421 \$stepparent_index ],
1423 [ \&tag_move_stepparent, $self, $cgi, $request, $article, \@stepparents,
1424 \$stepparent_index ],
1425 ifStepparentPossibles =>
1426 [ \&tag_if_stepparent_possibles, $request, $article, $articles,
1427 \@stepparent_targs, \@stepparentpossibles, ],
1428 stepparent_possibles =>
1429 [ \&tag_stepparent_possibles, $cgi, $request, $article, $articles,
1430 \@stepparent_targs, \@stepparentpossibles, ],
1433 code => [ iter_files => $self, $article ],
1437 index => \$file_index,
1440 [ \&tag_movefiles, $self, $request, $article, \@files, \$file_index ],
1443 code => [ iter_file_metas => $self, \@files, \$file_index ],
1444 plural => "file_metas",
1445 single => "file_meta",
1448 ifFileExists => sub {
1449 @files && $file_index >= 0 && $file_index < @files
1452 return -f ($files[$file_index]->full_filename($cfg));
1454 file_display => [ tag_file_display => $self, \@files, \$file_index ],
1455 DevHelp::Tags->make_iterator2
1456 (\&iter_admin_users, 'iadminuser', 'adminusers'),
1457 DevHelp::Tags->make_iterator2
1458 (\&iter_admin_groups, 'iadmingroup', 'admingroups'),
1459 edit => [ \&tag_edit_link, $cfg, $article ],
1460 error => [ $tag_hash, $errors ],
1461 error_img => [ \&tag_error_img, $cfg, $errors ],
1462 ifFieldPerm => [ \&tag_if_field_perm, $request, $article ],
1463 parent => [ \&tag_article, $parent, $cfg ],
1464 DevHelp::Tags->make_iterator2
1465 ([ \&iter_flags, $self ], 'flag', 'flags' ),
1466 ifFlagSet => [ \&tag_if_flag_set, $article ],
1467 DevHelp::Tags->make_iterator2
1468 ([ \&iter_crumbs, $article, $articles ], 'crumb', 'crumbs' ),
1469 typename => \&tag_typename,
1470 $it->make_iterator([ \&iter_groups, $request ],
1471 'group', 'groups', \@groups, undef, undef,
1473 $it->make_iterator([ iter_image_stores => $self],
1474 'image_store', 'image_stores'),
1475 $it->make_iterator([ iter_file_stores => $self],
1476 'file_store', 'file_stores'),
1477 ifGroupRequired => [ \&tag_ifGroupRequired, $article, \$current_group ],
1478 category => [ tag_category => $self, $articles, $article ],
1483 code => [ iter_tags => $self, $article ],
1488 sub iter_image_stores {
1491 my $mgr = $self->_image_manager;
1493 return map +{ name => $_->name, description => $_->description },
1500 require BSE::TB::ArticleFiles;
1502 return BSE::TB::ArticleFiles->file_manager($self->cfg);
1505 sub iter_file_stores {
1508 require BSE::TB::ArticleFiles;
1509 my $mgr = $self->_file_manager($self->cfg);
1511 return map +{ name => $_->name, description => $_->description },
1518 require BSE::TB::SiteUserGroups;
1519 BSE::TB::SiteUserGroups->admin_and_query_groups($req->cfg);
1522 sub tag_ifGroupRequired {
1523 my ($article, $rgroup) = @_;
1528 $$rgroup or return 0;
1530 $article->is_accessible_to($$rgroup);
1534 my ($self, $article, $cgi) = @_;
1536 my $base = $article->{level};
1537 my $t = $cgi->param('_t');
1538 if ($t && $t =~ /^\w+$/) {
1541 return $self->{cfg}->entry('admin templates', $base,
1542 "admin/edit_$base");
1546 my ($self, $article, $cgi) = @_;
1548 $self->edit_template($article, $cgi);
1552 my ($self, $request, $article, $articles, $msg, $errors) = @_;
1554 my $cgi = $request->cgi;
1556 %acts = $self->low_edit_tags(\%acts, $request, $article, $articles, $msg,
1558 my $template = $article->{id} ?
1559 $self->edit_template($article, $cgi) : $self->add_template($article, $cgi);
1561 return $request->response($template, \%acts);
1565 my ($self, $request, $article, $articles, $msg, $errors) = @_;
1567 return $self->low_edit_form($request, $article, $articles, $msg, $errors);
1570 sub _dummy_article {
1571 my ($self, $req, $articles, $rmsg) = @_;
1574 my $cgi = $req->cgi;
1575 my $parentid = $cgi->param('parentid');
1577 if ($parentid =~ /^\d+$/) {
1578 if (my $parent = $self->get_parent($parentid, $articles)) {
1579 $level = $parent->{level}+1;
1585 elsif ($parentid eq "-1") {
1589 unless (defined $level) {
1590 $level = $cgi->param('level');
1591 undef $level unless defined $level && $level =~ /^\d+$/
1592 && $level > 0 && $level < 100;
1593 defined $level or $level = 3;
1597 my @cols = BSE::TB::Article->columns;
1598 @article{@cols} = ('') x @cols;
1600 $article{parentid} = $parentid;
1601 $article{level} = $level;
1602 $article{body} = '<maximum of 64Kb>';
1603 $article{listed} = 1;
1604 $article{generator} = $self->generator;
1606 my ($values, $labels) = $self->possible_parents(\%article, $articles, $req);
1608 $$rmsg = "You can't add children to any article at that level";
1612 return $self->_make_dummy_article(\%article);
1615 sub _make_dummy_article {
1616 my ($self, $article) = @_;
1618 require BSE::DummyArticle;
1619 return bless $article, "BSE::DummyArticle";
1623 my ($self, $req, $article, $articles, $msg, $errors) = @_;
1625 return $self->low_edit_form($req, $article, $articles, $msg, $errors);
1628 sub generator { 'BSE::Generate::Article' }
1633 my $gen = $self->generator;
1635 ($gen =~ /(\w+)$/)[0] || 'Article';
1638 sub _validate_common {
1639 my ($self, $data, $articles, $errors, $article) = @_;
1641 # if (defined $data->{parentid} && $data->{parentid} =~ /^(?:-1|\d+)$/) {
1642 # unless ($data->{parentid} == -1 or
1643 # $articles->getByPkey($data->{parentid})) {
1644 # $errors->{parentid} = "Selected parent article doesn't exist";
1648 # $errors->{parentid} = "You need to select a valid parent";
1650 if (exists $data->{title} && $data->{title} !~ /\S/) {
1651 $errors->{title} = "Please enter a title";
1654 if (exists $data->{template} && $data->{template} =~ /\.\./) {
1655 $errors->{template} = "Please only select templates from the list provided";
1657 if (exists $data->{linkAlias}
1658 && length $data->{linkAlias}) {
1659 unless ($data->{linkAlias} =~ /\A[a-zA-Z0-9-_]+\z/
1660 && $data->{linkAlias} =~ /[A-Za-z]/) {
1661 $errors->{linkAlias} = "Link alias must contain only alphanumerics and contain at least one letter";
1665 if (defined $data->{category}) {
1666 unless (first { $_->{id} eq $data->{category} } $self->categories($articles)) {
1667 $errors->{category} = "msg:bse/admin/edit/category/unknown";
1671 require DevHelp::Validate;
1672 DevHelp::Validate->import('dh_validate_hash');
1673 dh_validate_hash($data, $errors,
1675 fields => $self->_custom_fields,
1677 dbh => BSE::DB->single->dbh,
1679 $self->cfg, ARTICLE_CUSTOM_FIELDS_CFG);
1683 my ($self, $data, $articles, $errors) = @_;
1685 $self->_validate_common($data, $articles, $errors);
1686 if (!$errors->{linkAlias} && defined $data->{linkAlias} && length $data->{linkAlias}) {
1687 my $other = $articles->getBy(linkAlias => $data->{linkAlias});
1689 and $errors->{linkAlias} =
1690 "Duplicate link alias - already used by article $other->{id}";
1692 custom_class($self->{cfg})
1693 ->article_validate($data, undef, $self->typename, $errors);
1695 return !keys %$errors;
1699 my ($self, $article, $data, $articles, $errors, $ajax) = @_;
1701 $self->_validate_common($data, $articles, $errors, $article);
1702 custom_class($self->{cfg})
1703 ->article_validate($data, $article, $self->typename, $errors);
1705 if (exists $data->{release}) {
1706 if ($ajax && !dh_parse_sql_date($data->{release})
1707 || !$ajax && !dh_parse_date($data->{release})) {
1708 $errors->{release} = "Invalid release date";
1712 if (!$errors->{linkAlias}
1713 && defined $data->{linkAlias}
1714 && length $data->{linkAlias}
1715 && $data->{linkAlias} ne $article->{linkAlias}) {
1716 my $other = $articles->getBy(linkAlias => $data->{linkAlias});
1717 $other && $other->{id} != $article->{id}
1718 and $errors->{linkAlias} = "Duplicate link alias - already used by article $other->{id}";
1721 return !keys %$errors;
1724 sub validate_parent {
1729 my ($self, $req, $data, $articles) = @_;
1731 my $custom = $self->_custom_fields;
1732 for my $key (keys %$custom) {
1733 my ($value) = $req->cgi->param($key);
1734 if (defined $value) {
1735 if ($key =~ /^customDate/) {
1736 require DevHelp::Date;
1738 if (my ($year, $month, $day) =
1739 DevHelp::Date::dh_parse_date($value, \$msg)) {
1740 $data->{$key} = sprintf("%04d-%02d-%02d", $year, $month, $day);
1743 $data->{$key} = undef;
1746 elsif ($key =~ /^customInt/) {
1747 if ($value =~ /\S/) {
1748 $data->{$key} = $value;
1751 $data->{$key} = undef;
1755 $data->{$key} = $value;
1760 custom_class($self->{cfg})
1761 ->article_fill_new($data, $self->typename);
1767 my ($self, $article) = @_;
1769 # check the config for the article and any of its ancestors
1770 my $work_article = $article;
1771 my $path = $self->{cfg}->entry('article uris', $work_article->{id});
1773 last if $work_article->{parentid} == -1;
1774 $work_article = $work_article->parent;
1775 $path = $self->{cfg}->entry('article uris', $work_article->{id});
1777 return $path if $path;
1779 $self->default_link_path($article);
1782 sub default_link_path {
1783 my ($self, $article) = @_;
1785 $self->{cfg}->entry('uri', 'articles', '/a');
1789 my ($self, $article) = @_;
1794 my $title = $article->title;
1795 if ($article->is_dynamic) {
1796 (my $extra = $title) =~ tr/A-Za-z0-9/-/sc;
1797 return "/cgi-bin/page.pl?page=$article->{id}&title=".escape_uri($extra);
1800 my $article_uri = $self->link_path($article);
1801 my $link = "$article_uri/$article->{id}.html";
1802 my $link_titles = $self->{cfg}->entryBool('basic', 'link_titles', 0);
1804 (my $extra = $title) =~ tr/A-Za-z0-9/-/sc;
1805 $link .= "/" . $extra . "_html";
1812 my ($self, $table_object) = @_;
1814 my @columns = $table_object->rowClass->columns;
1820 sub _validate_tags {
1821 my ($self, $tags, $errors) = @_;
1825 for my $tag (@$tags) {
1828 && !BSE::TB::Tags->valid_name($tag, \$error)) {
1829 push @errors, "msg:bse/admin/edit/tags/invalid/$error";
1830 $errors->{tags} = \@errors;
1834 push @errors, undef;
1842 my ($self, $req, $article, $articles) = @_;
1844 $req->check_csrf("admin_add_article")
1845 or return $self->csrf_error($req, undef, "admin_add_article", "Add Article");
1847 my $cgi = $req->cgi;
1849 my $table_object = $self->table_object($articles);
1850 my @columns = $self->save_columns($table_object);
1851 $self->save_thumbnail($cgi, undef, \%data);
1852 for my $name (@columns) {
1853 $data{$name} = $cgi->param($name)
1854 if defined $cgi->param($name);
1856 $data{flags} = join '', sort $cgi->param('flags');
1860 if (!defined $data{parentid} || $data{parentid} eq '') {
1861 $errors{parentid} = "Please select a parent";
1863 elsif ($data{parentid} !~ /^(?:-1|\d+)$/) {
1864 $errors{parentid} = "Invalid parent selection (template bug)";
1866 $self->validate(\%data, $articles, \%errors);
1868 my $save_tags = $cgi->param("_save_tags");
1871 @tags = $cgi->param("tags");
1872 $self->_validate_tags(\@tags, \%errors);
1876 if ($cgi->param("_save_meta")) {
1877 require BSE::ArticleMetaMeta;
1878 $meta = BSE::ArticleMetaMeta->retrieve($req, $article, \%errors);
1882 if ($req->is_ajax) {
1883 return $req->json_content
1887 error_code => "FIELD",
1888 message => $req->message(\%errors),
1892 return $self->add_form($req, $article, $articles, $msg, \%errors);
1899 if ($data{parentid} > 0) {
1900 $parent = $articles->getByPkey($data{parentid}) or die;
1901 if ($req->user_can('edit_add_child', $parent)) {
1902 for my $name (@columns) {
1903 if (exists $data{$name} &&
1904 !$req->user_can("edit_add_field_$name", $parent)) {
1905 delete $data{$name};
1910 $parent_msg = "You cannot add a child to that article";
1911 $parent_code = "ACCESS";
1915 if ($req->user_can('edit_add_child')) {
1916 for my $name (@columns) {
1917 if (exists $data{$name} &&
1918 !$req->user_can("edit_add_field_$name")) {
1919 delete $data{$name};
1924 $parent_msg = "You cannot create a top-level article";
1925 $parent_code = "ACCESS";
1929 $self->validate_parent(\%data, $articles, $parent, \$parent_msg)
1930 or $parent_code = "PARENT";
1933 if ($req->is_ajax) {
1934 return $req->json_content
1937 message => $parent_msg,
1938 error_code => $parent_code,
1943 return $self->add_form($req, $article, $articles, $parent_msg);
1947 my $level = $parent ? $parent->{level}+1 : 1;
1948 $data{level} = $level;
1949 $data{displayOrder} = time;
1951 $data{admin} ||= '';
1952 $data{generator} = $self->generator;
1953 $data{lastModified} = now_sqldatetime();
1954 $data{listed} = 1 unless defined $data{listed};
1957 $data{pageTitle} = '' unless defined $data{pageTitle};
1958 my $user = $req->getuser;
1959 $data{createdBy} = $user ? $user->{logon} : '';
1960 $data{lastModifiedBy} = $user ? $user->{logon} : '';
1961 $data{created} = now_sqldatetime();
1964 $data{force_dynamic} = 0;
1965 $data{cached_dynamic} = 0;
1966 $data{inherit_siteuser_rights} = 1;
1969 $data{metaDescription} = '' unless defined $data{metaDescription};
1970 $data{metaKeywords} = '' unless defined $data{metaKeywords};
1973 $self->fill_new_data($req, \%data, $articles);
1974 for my $col (qw(titleImage imagePos template keyword menu titleAlias linkAlias body author summary category)) {
1976 or $data{$col} = $self->default_value($req, \%data, $col);
1979 for my $col (qw/force_dynamic inherit_siteuser_rights/) {
1980 if ($req->user_can("edit_add_field_$col", $parent)
1981 && $cgi->param("save_$col")) {
1982 $data{$col} = $cgi->param($col) ? 1 : 0;
1985 $data{$col} = $self->default_value($req, \%data, $col);
1989 unless ($req->is_ajax) {
1990 for my $col (qw(release expire)) {
1991 $data{$col} = sql_date($data{$col});
1995 # these columns are handled a little differently
1996 for my $col (qw(release expire threshold summaryLength )) {
1998 or $data{$col} = $self->default_value($req, \%data, $col);
2001 my @cols = $table_object->rowClass->columns;
2004 # fill out anything else from defaults
2005 for my $col (@columns) {
2007 or $data{$col} = $self->default_value($req, \%data, $col);
2010 $article = $table_object->add(@data{@cols});
2012 $self->save_new_more($req, $article, \%data);
2014 # we now have an id - generate the links
2016 $article->update_dynamic($self->{cfg});
2017 my $cgi_uri = $self->{cfg}->entry('uri', 'cgi', '/cgi-bin');
2018 $article->setAdmin("$cgi_uri/admin/admin.pl?id=$article->{id}");
2019 $article->setLink($self->make_link($article));
2022 my ($after_id) = $cgi->param("_after");
2023 if (defined $after_id) {
2024 BSE::TB::Articles->reorder_child($article->{parentid}, $article->{id}, $after_id);
2025 # reload, the displayOrder probably changed
2026 $article = $articles->getByPkey($article->{id});
2031 $article->set_tags([ grep /\S/, @tags ], \$error);
2035 BSE::ArticleMetaMeta->save($article, $meta);
2038 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
2040 if ($req->is_ajax) {
2041 return $req->json_content
2045 article => $self->_article_data($req, $article),
2050 my $r = $cgi->param('r');
2052 $r .= ($r =~ /\?/) ? '&' : '?';
2053 $r .= "id=$article->{id}";
2056 $r = admin_base_url($req->cfg) . $article->{admin};
2058 return BSE::Template->get_refresh($r, $self->{cfg});
2062 my ($self, $req, $article, $data) = @_;
2064 if (exists $data->{body}) {
2065 $data->{body} =~ s/\x0D\x0A/\n/g;
2066 $data->{body} =~ tr/\r/\n/;
2068 for my $col (BSE::TB::Article->columns) {
2069 next if $col =~ /^custom/;
2070 $article->{$col} = $data->{$col}
2071 if exists $data->{$col} && $col ne 'id' && $col ne 'parentid';
2073 my $custom = $self->_custom_fields;
2074 for my $key (keys %$custom) {
2075 if (exists $data->{$key}) {
2076 if ($key =~ /^customDate/) {
2077 require DevHelp::Date;
2079 if (my ($year, $month, $day) =
2080 DevHelp::Date::dh_parse_date($data->{$key}, \$msg)) {
2081 $article->set($key, sprintf("%04d-%02d-%02d", $year, $month, $day));
2084 $article->set($key => undef);
2087 elsif ($key =~ /^customInt/) {
2088 if ($data->{$key} =~ /\S/) {
2089 $article->set($key => $data->{$key});
2092 $article->set($key => undef);
2096 $article->set($key => $data->{$key});
2100 custom_class($self->{cfg})
2101 ->article_fill_old($article, $data, $self->typename);
2107 my ($self, $req, $article) = @_;
2109 my $article_data = $article->data_only;
2110 $article_data->{link} = $article->link($req->cfg);
2111 $article_data->{images} =
2113 map $self->_image_data($req->cfg, $_), $article->images
2115 $article_data->{files} =
2117 map $_->data_only, $article->files,
2119 $article_data->{tags} =
2121 $article->tags, # just the names
2124 return $article_data;
2128 my ($self, $req, $article, $data) = @_;
2129 # nothing to do here
2133 my ($self, $req, $article, $data) = @_;
2134 # nothing to do here
2147 ACCESS - user doesn't have access to this article.
2151 LASTMOD - lastModified value doesn't match that in the article
2155 PARENT - invalid parentid specified
2162 my ($self, $req, $article, $articles) = @_;
2164 $req->check_csrf("admin_save_article")
2165 or return $self->csrf_error($req, $article, "admin_save_article", "Save Article");
2167 $req->user_can(edit_save => $article)
2168 or return $self->_service_error
2169 ($req, $article, $articles, "You don't have access to save this article",
2172 my $old_dynamic = $article->is_dynamic;
2173 my $cgi = $req->cgi;
2175 my $table_object = $self->table_object($articles);
2176 my @save_cols = $self->save_columns($table_object);
2177 for my $name (@save_cols) {
2178 $data{$name} = $cgi->param($name)
2179 if defined($cgi->param($name)) and $name ne 'id' && $name ne 'parentid'
2180 && $req->user_can("edit_field_edit_$name", $article);
2184 # checks editor lastModified against record lastModified
2185 if ($self->{cfg}->entry('editor', 'check_modified')) {
2186 if ($article->{lastModified} ne $cgi->param('lastModified')) {
2187 my $whoModified = '';
2188 my $timeModified = ampm_time($article->{lastModified});
2189 if ($article->{lastModifiedBy}) {
2190 $whoModified = "by '$article->{lastModifiedBy}'";
2192 print STDERR "non-matching lastModified, article not saved\n";
2193 my $msg = "Article changes not saved, this article was modified $whoModified at $timeModified since this editor was loaded";
2194 return $self->_service_error($req, $article, $articles, $msg, {}, "LASTMOD");
2199 # possibly this needs tighter error checking
2200 $data{flags} = join '', sort $cgi->param('flags')
2201 if $req->user_can("edit_field_edit_flags", $article);
2203 if (exists $article->{template} &&
2204 $article->{template} =~ m|\.\.|) {
2205 $errors{template} = "Please only select templates from the list provided";
2209 if ($cgi->param("_save_meta")) {
2210 require BSE::ArticleMetaMeta;
2211 $meta = BSE::ArticleMetaMeta->retrieve($req, $article, \%errors);
2214 my $save_tags = $cgi->param("_save_tags");
2217 @tags = $cgi->param("tags");
2218 $self->_validate_tags(\@tags, \%errors);
2220 $self->validate_old($article, \%data, $articles, \%errors, scalar $req->is_ajax)
2221 or return $self->_service_error($req, $article, $articles, undef, \%errors, "FIELD");
2222 $self->save_thumbnail($cgi, $article, \%data)
2223 if $req->user_can('edit_field_edit_thumbImage', $article);
2224 if (exists $data{flags} && $data{flags} =~ /D/) {
2225 $article->remove_html;
2227 $self->fill_old_data($req, $article, \%data);
2230 my $newparentid = $cgi->param('parentid');
2232 && $req->user_can('edit_field_edit_parentid', $article)
2233 && $newparentid != $article->{parentid}) {
2236 if ($newparentid == -1) {
2237 require BSE::Edit::Site;
2238 $newparent = BSE::TB::Site->new;
2239 $parent_editor = BSE::Edit::Site->new(cfg => $req->cfg);
2242 $newparent = $articles->getByPkey($newparentid);
2243 ($parent_editor, $newparent) = $self->article_class($newparent, $articles, $req->cfg);
2247 if ($self->can_reparent_to($article, $newparent, $parent_editor, $articles, \$msg)
2248 && $self->reparent($article, $newparentid, $articles, \$msg)) {
2249 # nothing to do here
2252 return $self->_service_error($req, $article, $articles, $msg, {}, "PARENT");
2256 return $self->_service_error($req, $article, $articles, "No such parent article", {}, "PARENT");
2260 $article->{listed} = $cgi->param('listed')
2261 if defined $cgi->param('listed') &&
2262 $req->user_can('edit_field_edit_listed', $article);
2264 if ($req->user_can('edit_field_edit_release', $article)) {
2265 my $release = $cgi->param("release");
2266 if (defined $release && $release =~ /\S/) {
2267 if ($req->is_ajax) {
2268 $article->{release} = $release;
2271 $article->{release} = sql_date($release)
2276 $article->{expire} = sql_date($cgi->param('expire')) || $Constants::D_99
2277 if defined $cgi->param('expire') &&
2278 $req->user_can('edit_field_edit_expire', $article);
2279 for my $col (qw/force_dynamic inherit_siteuser_rights/) {
2280 if ($req->user_can("edit_field_edit_$col", $article)
2281 && $cgi->param("save_$col")) {
2282 $article->{$col} = $cgi->param($col) ? 1 : 0;
2286 $article->mark_modified(actor => $req->getuser || "U");
2288 my @save_group_ids = $cgi->param('save_group_id');
2289 if ($req->user_can('edit_field_edit_group_id')
2290 && @save_group_ids) {
2291 require BSE::TB::SiteUserGroups;
2292 my %groups = map { $_->{id} => $_ }
2293 BSE::TB::SiteUserGroups->admin_and_query_groups($self->{cfg});
2294 my %set = map { $_ => 1 } $cgi->param('group_id');
2295 my %current = map { $_ => 1 } $article->group_ids;
2297 for my $group_id (@save_group_ids) {
2298 $groups{$group_id} or next;
2299 if ($current{$group_id} && !$set{$group_id}) {
2300 $article->remove_group_id($group_id);
2302 elsif (!$current{$group_id} && $set{$group_id}) {
2303 $article->add_group_id($group_id);
2308 my $old_link = $article->{link};
2309 # this need to go last
2310 $article->update_dynamic($self->{cfg});
2311 if (!$self->{cfg}->entry('protect link', $article->{id})) {
2312 my $article_uri = $self->make_link($article);
2313 $article->setLink($article_uri);
2320 $article->set_tags([ grep /\S/, @tags ], \$error);
2324 print STDERR Dumper($meta);
2326 BSE::ArticleMetaMeta->save($article, $meta);
2331 @extra_regen = $self->update_child_dynamic($article, $articles, $req);
2333 if ($article->is_dynamic || $old_dynamic) {
2334 if (!$old_dynamic and $old_link) {
2335 unlink $article->link_to_filename($self->{cfg}, $old_link);
2337 elsif (!$article->is_dynamic) {
2338 unlink $article->cached_filename($self->{cfg});
2342 my ($after_id) = $cgi->param("_after");
2343 if (defined $after_id) {
2344 BSE::TB::Articles->reorder_child($article->{parentid}, $article->{id}, $after_id);
2345 # reload, the displayOrder probably changed
2346 $article = $articles->getByPkey($article->{id});
2349 if ($Constants::AUTO_GENERATE) {
2350 generate_article($articles, $article);
2351 for my $regen_id (@extra_regen) {
2352 my $regen = $articles->getByPkey($regen_id);
2353 BSE::Regen::generate_low($articles, $regen, $self->{cfg});
2357 $self->save_more($req, $article, \%data);
2359 if ($req->is_ajax) {
2360 return $req->json_content
2364 article => $self->_article_data($req, $article),
2369 return $self->refresh($article, $cgi, undef, 'Article saved');
2372 sub can_reparent_to {
2373 my ($self, $article, $newparent, $parent_editor, $articles, $rmsg) = @_;
2375 my @child_types = $parent_editor->child_types;
2376 if (!grep $_ eq ref $self, @child_types) {
2377 my ($child_type) = (ref $self) =~ /(\w+)$/;
2378 my ($parent_type) = (ref $parent_editor) =~ /(\w+)$/;
2380 $$rmsg = "A $child_type cannot be a child of a $parent_type";
2384 # the article cannot become a child of itself or one of it's
2386 if ($article->{id} == $newparent->id
2387 || $self->is_descendant($article->id, $newparent->id, $articles)) {
2388 $$rmsg = "Cannot become a child of itself or of a descendant";
2392 my $shopid = $self->{cfg}->entryErr('articles', 'shop');
2393 if ($self->shop_article) { # if this article belongs in the shop
2394 unless ($newparent->id == $shopid
2395 || $self->is_descendant($shopid, $newparent->{id}, $articles)) {
2396 $$rmsg = "This article belongs in the shop";
2401 if ($newparent->id == $shopid
2402 || $self->is_descendant($shopid, $newparent->id, $articles)) {
2403 $$rmsg = "This article doesn't belong in the shop";
2411 sub shop_article { 0 }
2413 sub update_child_dynamic {
2414 my ($self, $article, $articles, $req) = @_;
2416 my $cfg = $req->cfg;
2417 my @stack = $article->children;
2420 my $workart = pop @stack;
2421 my $old_dynamic = $workart->is_dynamic; # before update
2422 my $old_link = $workart->{link};
2424 ($editor, $workart) = $self->article_class($workart, $articles, $cfg);
2426 $workart->update_dynamic($cfg);
2427 if ($old_dynamic != $workart->is_dynamic) {
2429 if ($article->{link} && !$cfg->entry('protect link', $workart->{id})) {
2430 my $uri = $editor->make_link($workart);
2431 $workart->setLink($uri);
2433 !$old_dynamic && $old_link
2434 and unlink $workart->link_to_filename($cfg, $old_link);
2435 $workart->is_dynamic
2436 or unlink $workart->cached_filename($cfg);
2439 # save dynamic cache change and link if that changed
2442 push @stack, $workart->children;
2443 push @regen, $workart->{id};
2451 my ($year, $month, $day);
2454 if (($day, $month, $year) = ($str =~ m!(\d+)/(\d+)/(\d+)!)) {
2455 $year += 2000 if $year < 100;
2457 return sprintf("%04d-%02d-%02d", $year, $month, $day);
2463 # Converts 24hr time to 12hr AM/PM time
2466 my ($hour, $minute, $second, $ampm);
2469 if (($hour, $minute, $second) = ($str =~ m!(\d+):(\d+):(\d+)!)) {
2475 $hour = 12 if $hour == 0;
2478 return sprintf("%02d:%02d:%02d $ampm", $hour, $minute, $second);
2485 my ($self, $article, $newparentid, $articles, $rmsg) = @_;
2488 if ($newparentid == -1) {
2492 my $parent = $articles->getByPkey($newparentid);
2494 $$rmsg = "Cannot get new parent article";
2497 $newlevel = $parent->{level} + 1;
2499 # the caller will save this one
2500 $article->{parentid} = $newparentid;
2501 $article->{level} = $newlevel;
2502 $article->{displayOrder} = time;
2504 my @change = ( [ $article->{id}, $newlevel ] );
2506 my $this = shift @change;
2507 my ($art, $level) = @$this;
2509 my @kids = $articles->getBy(parentid=>$art);
2510 push @change, map { [ $_->{id}, $level+1 ] } @kids;
2512 for my $kid (@kids) {
2513 $kid->{level} = $level+1;
2521 # tests if $desc is a descendant of $art
2522 # where both are article ids
2524 my ($self, $art, $desc, $articles) = @_;
2528 my $parent = shift @check;
2529 $parent == $desc and return 1;
2530 my @kids = $articles->getBy(parentid=>$parent);
2531 push @check, map $_->{id}, @kids;
2537 sub save_thumbnail {
2538 my ($self, $cgi, $original, $newdata) = @_;
2540 unless ($original) {
2541 @$newdata{qw/thumbImage thumbWidth thumbHeight/} = ('', 0, 0);
2543 my $imagedir = cfg_image_dir($self->{cfg});
2544 if ($cgi->param('remove_thumb') && $original && $original->{thumbImage}) {
2545 unlink("$imagedir/$original->{thumbImage}");
2546 @$newdata{qw/thumbImage thumbWidth thumbHeight/} = ('', 0, 0);
2548 my $image_name = $cgi->param('thumbnail');
2549 my $image = $cgi->upload('thumbnail');
2550 if ($image_name && -s $image) {
2551 # where to put it...
2553 $image_name =~ /([\w.-]+)$/ and $name = $1;
2554 my $filename = time . "_" . $name;
2558 $filename = time . '_' . $counter . '_' . $name
2559 until sysopen( OUTPUT, "$imagedir/$filename",
2560 O_WRONLY| O_CREAT| O_EXCL)
2561 || ++$counter > 100;
2563 fileno(OUTPUT) or die "Could not open image file: $!";
2569 # read the image in from the browser and output it to our
2571 print STDERR "\$image ",ref $image,"\n";
2573 print OUTPUT $buffer while sysread $image, $buffer, 1024;
2576 or die "Could not close image output file: $!";
2578 require BSE::ImageSize;
2580 if ($original && $original->{thumbImage}) {
2581 #unlink("$imagedir/$original->{thumbImage}");
2583 @$newdata{qw/thumbWidth thumbHeight/} =
2584 BSE::ImageSize::imgsize("$imagedir/$filename");
2585 $newdata->{thumbImage} = $filename;
2590 my ($self, $article) = @_;
2592 my $shopid = $self->{cfg}->entryErr('articles', 'shop');
2593 if ($article && $article->{id} && $article->{id} == $shopid) {
2594 return ( 'BSE::Edit::Catalog' );
2596 return ( 'BSE::Edit::Article' );
2601 Add a step child to an article.
2609 id - parent article id (required)
2613 stepkid - child article id (required)
2617 _after - id of the allkid of id to position the stepkid after
2622 Returns a FIELD error for an invalid stepkid.
2624 Returns an ACCESS error for insufficient access.
2626 Return an ADD error for a general add failure.
2632 relationship: { childId: I<childid>, parentId: I<parentid> }
2640 my ($self, $req, $article, $articles) = @_;
2642 $req->check_csrf("admin_add_stepkid")
2643 or return $self->csrf_error($req, $article, "admin_add_stepkid", "Add Stepkid");
2645 $req->user_can(edit_stepkid_add => $article)
2646 or return $self->_service_error($req, $article, $articles,
2647 "You don't have access to add step children to this article", {}, "ACCESS");
2649 my $cgi = $req->cgi;
2650 require BSE::Admin::StepParents;
2653 my $childId = $cgi->param('stepkid');
2655 or $errors{stepkid} = "No stepkid supplied to add_stepkid";
2656 unless ($errors{stepkid}) {
2658 or $errors{stepkid} = "Invalid stepkid supplied to add_stepkid";
2661 unless ($errors{stepkid}) {
2662 $child = $articles->getByPkey($childId)
2663 or $errors{stepkid} = "Article $childId not found";
2666 and return $self->_service_error
2667 ($req, $article, $articles, $errors{stepkid}, \%errors, "FIELD");
2669 $req->user_can(edit_stepparent_add => $child)
2670 or return $self->_service_error($req, $article, $articles, "You don't have access to add a stepparent to that article", {}, "ACCESS");
2675 my $release = $cgi->param('release');
2676 dh_parse_date($release) or $release = undef;
2677 my $expire = $cgi->param('expire');
2678 dh_parse_date($expire) or $expire = undef;
2681 BSE::Admin::StepParents->add($article, $child, $release, $expire);
2684 return $self->_service_error($req, $article, $articles, $@, {}, "ADD");
2687 my $after_id = $cgi->param("_after");
2688 if (defined $after_id) {
2689 BSE::TB::Articles->reorder_child($article->id, $child->id, $after_id);
2692 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
2694 if ($req->is_ajax) {
2695 return $req->json_content
2698 relationship => $new_entry->data_only,
2702 $self->refresh($article, $cgi, 'step', 'Stepchild added');
2708 Remove a stepkid relationship.
2716 id - parent article id (required)
2720 stepkid - child article id (required)
2724 Returns a FIELD error for an invalid stepkid.
2726 Returns an ACCESS error for insufficient access.
2728 Return a DELETE error for a general delete failure.
2733 my ($self, $req, $article, $articles) = @_;
2735 $req->check_csrf("admin_remove_stepkid")
2736 or return $self->csrf_error($req, $article, "admin_del_stepkid", "Delete Stepkid");
2737 $req->user_can(edit_stepkid_delete => $article)
2738 or return $self->_service_error($req, $article, $articles,
2739 "You don't have access to delete stepchildren from this article", {}, "ACCESS");
2741 my $cgi = $req->cgi;
2744 my $childId = $cgi->param('stepkid');
2746 or $errors{stepkid} = "No stepkid supplied to add_stepkid";
2747 unless ($errors{stepkid}) {
2749 or $errors{stepkid} = "Invalid stepkid supplied to add_stepkid";
2752 unless ($errors{stepkid}) {
2753 $child = $articles->getByPkey($childId)
2754 or $errors{stepkid} = "Article $childId not found";
2757 and return $self->_service_error
2758 ($req, $article, $articles, $errors{stepkid}, \%errors, "FIELD");
2760 $req->user_can(edit_stepparent_delete => $child)
2761 or return _service_error($req, $article, $article, "You cannot remove stepparents from that article", {}, "ACCESS");
2764 require BSE::Admin::StepParents;
2766 BSE::Admin::StepParents->del($article, $child);
2770 return $self->_service_error($req, $article, $articles, $@, {}, "DELETE");
2772 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
2774 if ($req->is_ajax) {
2775 return $req->json_content(success => 1);
2778 return $self->refresh($article, $cgi, 'step', 'Stepchild deleted');
2783 my ($self, $req, $article, $articles) = @_;
2785 $req->check_csrf("admin_save_stepkids")
2786 or return $self->csrf_error($req, $article, "admin_save_stepkids", "Save Stepkids");
2788 $req->user_can(edit_stepkid_save => $article)
2789 or return $self->edit_form($req, $article, $articles,
2790 "No access to save stepkid data for this article");
2792 my $cgi = $req->cgi;
2793 require 'BSE/Admin/StepParents.pm';
2794 my @stepcats = BSE::TB::OtherParents->getBy(parentId=>$article->{id});
2795 my %stepcats = map { $_->{parentId}, $_ } @stepcats;
2796 my %datedefs = ( release => '2000-01-01', expire=>'2999-12-31' );
2797 for my $stepcat (@stepcats) {
2798 $req->user_can(edit_stepparent_save => $stepcat->{childId})
2800 for my $name (qw/release expire/) {
2801 my $date = $cgi->param($name.'_'.$stepcat->{childId});
2802 if (defined $date) {
2804 $date = $datedefs{$name};
2806 elsif (dh_parse_date($date)) {
2807 use BSE::Util::SQL qw/date_to_sql/;
2808 $date = date_to_sql($date);
2811 return $self->refresh($article, $cgi, '', "Invalid date '$date'");
2813 $stepcat->{$name} = $date;
2819 $@ and return $self->refresh($article, $cgi, '', $@);
2821 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
2823 return $self->refresh($article, $cgi, 'step', 'Stepchild information saved');
2828 Moves a stepkid from one parent to another, and sets the order within
2829 that new stepparent.
2837 id - id of the step kid to move (required)
2841 parentid - id of the parent in the stepkid relationship (required)
2845 newparentid - the new parent for the stepkid relationship (optional)
2849 _after - id of the allkid under newparentid (or parentid if
2850 newparentid isn't supplied) to place the stepkid after (0 to place at
2861 NOPARENTID - parentid parameter not supplied
2865 BADPARENTID - non-numeric parentid supplied
2869 NOTFOUND - no stepkid relationship from parentid was found
2873 BADNEWPARENT - newparentid is non-numeric
2877 UNKNOWNNEWPARENT - no article id newparentid found
2881 NEWPARENTDUP - there's already a stepkid relationship between
2889 my ($self, $req, $article, $articles) = @_;
2891 # first, identify the stepkid link
2892 my $cgi = $req->cgi;
2893 require BSE::TB::OtherParents;
2894 my $parentid = $cgi->param("parentid");
2896 or return $self->_service_error($req, $article, $articles, "Missing parentid", {}, "NOPARENTID");
2897 $parentid =~ /^\d+$/
2898 or return $self->_service_error($req, $article, $articles, "Invalid parentid", {}, "BADPARENTID");
2900 my ($step) = BSE::TB::OtherParents->getBy(parentId => $parentid, childId => $article->id)
2901 or return $self->_service_error($req, $article, $articles, "Unknown relationship", {}, "NOTFOUND");
2903 my $newparentid = $cgi->param("newparentid");
2905 $newparentid =~ /^\d+$/
2906 or return $self->_service_error($req, $article, $articles, "Bad new parent id", {}, "BADNEWPARENT");
2907 my $new_parent = BSE::TB::Articles->getByPkey($newparentid)
2908 or return $self->_service_error($req, $article, $articles, "Unknown new parent id", {}, "UNKNOWNNEWPARENT");
2910 BSE::TB::OtherParents->getBy(parentId=>$newparentid, childId=>$article->id)
2911 and return $self->_service_error($req, $article, $articles, "New parent is duplicate", {}, "NEWPARENTDUP");
2913 $step->{parentId} = $newparentid;
2917 my $after_id = $cgi->param("_after");
2918 if (defined $after_id) {
2919 BSE::TB::Articles->reorder_child($step->{parentId}, $article->id, $after_id);
2922 if ($req->is_ajax) {
2923 return $req->json_content
2926 relationshop => $step->data_only,
2930 return $self->refresh($article, $cgi, 'step', "Stepchild moved");
2934 sub add_stepparent {
2935 my ($self, $req, $article, $articles) = @_;
2937 $req->check_csrf("admin_add_stepparent")
2938 or return $self->csrf_error($req, $article, "admin_add_stepparent", "Add Stepparent");
2940 $req->user_can(edit_stepparent_add => $article)
2941 or return $self->edit_form($req, $article, $articles,
2942 "You don't have access to add stepparents to this article");
2944 my $cgi = $req->cgi;
2945 require 'BSE/Admin/StepParents.pm';
2947 my $step_parent_id = $cgi->param('stepparent');
2948 defined($step_parent_id)
2949 or die "No stepparent supplied to add_stepparent";
2950 int($step_parent_id) eq $step_parent_id
2951 or die "Invalid stepcat supplied to add_stepcat";
2952 my $step_parent = $articles->getByPkey($step_parent_id)
2953 or die "Parent $step_parent_id not found\n";
2955 $req->user_can(edit_stepkid_add => $step_parent)
2956 or die "You don't have access to add a stepkid to that article\n";
2958 my $release = $cgi->param('release');
2960 or $release = "01/01/2000";
2961 $release eq '' or dh_parse_date($release)
2962 or die "Invalid release date";
2963 my $expire = $cgi->param('expire');
2965 or $expire = '31/12/2999';
2966 $expire eq '' or dh_parse_date($expire)
2967 or die "Invalid expire data";
2970 BSE::Admin::StepParents->add($step_parent, $article, $release, $expire);
2972 $@ and return $self->refresh($article, $cgi, 'step', $@);
2974 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
2976 return $self->refresh($article, $cgi, 'stepparents', 'Stepparent added');
2979 sub del_stepparent {
2980 my ($self, $req, $article, $articles) = @_;
2982 $req->check_csrf("admin_remove_stepparent")
2983 or return $self->csrf_error($req, $article, "admin_del_stepparent", "Delete Stepparent");
2985 $req->user_can(edit_stepparent_delete => $article)
2986 or return $self->edit_form($req, $article, $articles,
2987 "You cannot remove stepparents from that article");
2989 my $cgi = $req->cgi;
2990 require 'BSE/Admin/StepParents.pm';
2991 my $step_parent_id = $cgi->param('stepparent');
2992 defined($step_parent_id)
2993 or return $self->refresh($article, $cgi, 'stepparents',
2994 "No stepparent supplied to add_stepcat");
2995 int($step_parent_id) eq $step_parent_id
2996 or return $self->refresh($article, $cgi, 'stepparents',
2997 "Invalid stepparent supplied to add_stepparent");
2998 my $step_parent = $articles->getByPkey($step_parent_id)
2999 or return $self->refresh($article, $cgi, 'stepparent',
3000 "Stepparent $step_parent_id not found");
3002 $req->user_can(edit_stepkid_delete => $step_parent)
3003 or die "You don't have access to remove the stepkid from that article\n";
3006 BSE::Admin::StepParents->del($step_parent, $article);
3008 $@ and return $self->refresh($article, $cgi, 'stepparents', $@);
3010 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
3012 return $self->refresh($article, $cgi, 'stepparents', 'Stepparent deleted');
3015 sub save_stepparents {
3016 my ($self, $req, $article, $articles) = @_;
3018 $req->check_csrf("admin_save_stepparents")
3019 or return $self->csrf_error($req, $article, "admin_save_stepparents", "Save Stepparents");
3020 $req->user_can(edit_stepparent_save => $article)
3021 or return $self->edit_form($req, $article, $articles,
3022 "No access to save stepparent data for this artice");
3024 my $cgi = $req->cgi;
3026 require 'BSE/Admin/StepParents.pm';
3027 my @stepparents = BSE::TB::OtherParents->getBy(childId=>$article->{id});
3028 my %stepparents = map { $_->{parentId}, $_ } @stepparents;
3029 my %datedefs = ( release => '2000-01-01', expire=>'2999-12-31' );
3030 for my $stepparent (@stepparents) {
3031 $req->user_can(edit_stepkid_save => $stepparent->{parentId})
3033 for my $name (qw/release expire/) {
3034 my $date = $cgi->param($name.'_'.$stepparent->{parentId});
3035 if (defined $date) {
3037 $date = $datedefs{$name};
3039 elsif (dh_parse_date($date)) {
3040 use BSE::Util::SQL qw/date_to_sql/;
3041 $date = date_to_sql($date);
3044 return $self->refresh($article, $cgi, "Invalid date '$date'");
3046 $stepparent->{$name} = $date;
3050 $stepparent->save();
3052 $@ and return $self->refresh($article, $cgi, '', $@);
3055 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
3057 return $self->refresh($article, $cgi, 'stepparents',
3058 'Stepparent information saved');
3062 my ($self, $article, $cgi, $name, $message, $extras) = @_;
3064 my $url = $cgi->param('r');
3066 if ($url !~ /[?&](m|message)=/ && $message) {
3067 # add in messages if none in the provided refresh
3068 my @msgs = ref $message ? @$message : $message;
3069 my $sep = $url =~ /\?/ ? "&" : "?";
3070 for my $msg (@msgs) {
3071 $url .= $sep . "m=" . CGI::escape($msg);
3076 my $urlbase = admin_base_url($self->{cfg});
3077 $url = "$urlbase$ENV{SCRIPT_NAME}?id=$article->{id}";
3079 my @msgs = ref $message ? @$message : $message;
3080 for my $msg (@msgs) {
3081 $url .= "&m=" . CGI::escape($msg);
3084 if ($cgi->param('_t')) {
3085 $url .= "&_t=".CGI::escape($cgi->param('_t'));
3087 $url .= $extras if defined $extras;
3088 my $cgiextras = $cgi->param('e');
3089 $url .= "#$name" if $name;
3096 my ($self, $article, $cgi, $name, $message, $extras) = @_;
3098 my $url = $self->refresh_url($article, $cgi, $name, $message, $extras);
3100 return BSE::Template->get_refresh($url, $self->{cfg});
3104 my ($self, $req, $article, $articles, $msg, $errors) = @_;
3107 %acts = $self->low_edit_tags(\%acts, $req, $article, $articles, $msg, $errors);
3108 my $template = 'admin/article_img';
3110 return $req->dyn_response($template, \%acts);
3113 sub save_image_changes {
3114 my ($self, $req, $article, $articles) = @_;
3116 $req->check_csrf("admin_save_images")
3117 or return $self->csrf_error($req, $article, "admin_save_images", "Save Images");
3119 $req->user_can(edit_images_save => $article)
3120 or return $self->edit_form($req, $article, $articles,
3121 "You don't have access to save image information for this article");
3123 my $image_dir = cfg_image_dir($req->cfg);
3125 my $cgi = $req->cgi;
3126 my $image_pos = $cgi->param('imagePos');
3128 && $image_pos =~ /^(?:tl|tr|bl|br|xx)$/
3129 && $image_pos ne $article->{imagePos}) {
3130 $article->{imagePos} = $image_pos;
3133 my @images = $self->get_images($article);
3136 return $self->refresh($article, $cgi, undef, 'No images to save information for');
3143 for my $image (@images) {
3144 my $id = $image->{id};
3146 my $alt = $cgi->param("alt$id");
3147 if ($alt ne $image->{alt}) {
3148 $changes{$id}{alt} = $alt;
3151 my $url = $cgi->param("url$id");
3152 if (defined $url && $url ne $image->{url}) {
3153 $changes{$id}{url} = $url;
3156 my $name = $cgi->param("name$id");
3157 if (defined $name && $name ne $image->{name}) {
3159 $changes{$id}{name} = '';
3161 elsif ($name =~ /^[a-z_]\w*$/i) {
3163 if ($self->validate_image_name($name, \$msg)) {
3164 # check for duplicates after the loop
3165 push @{$names{lc $name}}, $image->{id}
3167 $changes{$id}{name} = $name;
3170 $errors{"name$id"} = $msg;
3174 $errors{"name$id"} = 'Image name must be empty or alphanumeric and unique to the article';
3178 push @{$names{lc $image->{name}}}, $image->{id}
3179 if length $image->{name};
3182 if ($cgi->param("_save_image_tags$image->{id}")) {
3183 my @tags = $cgi->param("tags$image->{id}");
3186 for my $tag (@tags) {
3189 && !BSE::TB::Tags->valid_name($tag, \$error)) {
3190 $errors[$index] = "msg:bse/admin/edit/tags/invalid/$error";
3191 $errors{"tags$image->{id}"} = \@errors;
3196 $changes{$id}{tags} = [ grep /\S/, @tags ];
3200 my $filename = $cgi->param("image$id");
3201 if (defined $filename && length $filename) {
3202 my $in_fh = $cgi->upload("image$id");
3206 my ($width, $height, $type) = $self->_validate_image
3207 ($filename, $in_fh, \$basename, \$image_error);
3210 $errors{"image$id"} = $image_error;
3213 unless ($errors{"image$id"}) {
3214 # work out where to put it
3215 require DevHelp::FileUpload;
3217 my ($image_name, $out_fh) = DevHelp::FileUpload->make_img_filename
3218 ($image_dir, $basename, \$msg);
3222 while ($data = <$in_fh>) {
3223 print $out_fh $data;
3227 my $full_filename = "$image_dir/$image_name";
3231 image => $image->{image},
3232 storage => $image->{storage}
3234 push @new_images, $image_name;
3236 $changes{$id}{image} = $image_name;
3237 $changes{$id}{storage} = 'local';
3238 $changes{$id}{src} = cfg_image_uri() . "/" . $image_name;
3239 $changes{$id}{width} = $width;
3240 $changes{$id}{height} = $height;
3241 $changes{$id}{ftype} = $self->_image_ftype($type);
3245 $errors{"image$id"} = $msg;
3251 $errors{"image$id"} = "No image file received";
3255 # look for duplicate names
3256 for my $name (keys %names) {
3257 if (@{$names{$name}} > 1) {
3258 for my $id (@{$names{$name}}) {
3259 $errors{"name$id"} = 'Image name must be unique to the article';
3264 # remove files that won't be stored because validation failed
3265 unlink map "$image_dir/$_", @new_images;
3267 return $self->edit_form($req, $article, $articles, undef,
3271 my $mgr = $self->_image_manager($req->cfg);
3272 $req->flash('Image information saved');
3273 my $changes_found = 0;
3274 my $auto_store = $cgi->param('auto_storage');
3275 for my $image (@images) {
3276 my $id = $image->{id};
3278 if ($changes{$id}) {
3279 my $changes = $changes{$id};
3282 for my $field (keys %$changes) {
3283 my $tags = delete $changes->{$field};
3286 $image->set_tags($tags, \$error);
3288 $image->{$field} = $changes->{$field};
3293 my $old_storage = $image->{storage};
3294 my $new_storage = $auto_store ? '' : $cgi->param("storage$id");
3295 defined $new_storage or $new_storage = $image->{storage};
3296 $new_storage = $mgr->select_store($image->{image}, $new_storage, $image);
3297 if ($new_storage ne $old_storage) {
3299 $image->{src} = $mgr->store($image->{image}, $new_storage, $image);
3300 $image->{storage} = $new_storage;
3304 if ($old_storage ne 'local') {
3305 $mgr->unstore($image->{image}, $old_storage);
3310 # delete any image files that were replaced
3311 for my $old_image (values %old_images) {
3312 my ($image, $storage) = @$old_image{qw/image storage/};
3313 if ($storage ne 'local') {
3314 $mgr->unstore($image->{image}, $storage);
3316 unlink "$image_dir/$image";
3319 if ($changes_found) {
3320 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
3323 return $self->refresh($article, $cgi);
3326 =item _service_error
3328 This function is called on various errors.
3330 If a _service parameter was supplied, returns text like:
3336 Field-Error: I<field-name1> - I<message1>
3338 Field-Error: I<field-name2> - I<message2>
3342 If the request is detected as an ajax request or a _ parameter is
3343 supplied, return JSON like:
3345 { error: I<message> }
3347 Otherwise display the normal edit page with the error.
3351 sub _service_error {
3352 my ($self, $req, $article, $articles, $msg, $error, $code, $method) = @_;
3356 $article = $self->_dummy_article($req, $articles, \$mymsg);
3359 map $_ => '', BSE::TB::Article->columns
3363 if ($req->cgi->param('_service')) {
3365 $body .= "Result: failure\n";
3367 for my $field (keys %$error) {
3368 my $text = $error->{$field};
3370 $body .= "Field-Error: $field - $text\n";
3372 my $text = join ('/', values %$error);
3374 $body .= "Error: $text\n";
3377 $body .= "Error: $msg\n";
3380 $body .= "Error: $error\n";
3384 type => 'text/plain',
3388 elsif ((() = $req->cgi->param('_')) ||
3389 (exists $ENV{HTTP_X_REQUESTED_WITH}
3390 && $ENV{HTTP_X_REQUESTED_WITH} =~ /XMLHttpRequest/)) {
3397 $msg and $result->{message} = $msg;
3398 $code and $result->{error_code} = $code;
3399 my $json_result = $req->json_content($result);
3401 if (!exists $ENV{HTTP_X_REQUESTED_WITH}
3402 || $ENV{HTTP_X_REQUESTED_WITH} !~ /XMLHttpRequest/) {
3403 $json_result->{type} = "text/plain";
3406 return $json_result;
3409 $method ||= "edit_form";
3410 return $self->$method($req, $article, $articles, $msg, $error);
3414 sub _service_success {
3415 my ($self, $results) = @_;
3417 my $body = "Result: success\n";
3418 for my $field (keys %$results) {
3419 $body .= "$field: $results->{$field}\n";
3423 type => 'text/plain',
3428 # FIXME: eliminate this method and call get_ftype directly
3430 my ($self, $type) = @_;
3432 require BSE::TB::Images;
3433 return BSE::TB::Images->get_ftype($type);
3440 pnm => "pbm,pgm,ppm",
3443 sub _validate_image {
3444 my ($self, $filename, $fh, $rbasename, $error) = @_;
3448 $$error = 'Image file is empty';
3453 $$error = 'Please enter an image filename';
3456 my $imagename = $filename;
3457 $imagename .= ''; # force it into a string
3458 (my $basename = $imagename) =~ tr/A-Za-z0-9_./-/cs;
3460 require BSE::ImageSize;
3462 my ($width,$height, $type) = BSE::ImageSize::imgsize($fh);
3464 unless (defined $width) {
3465 $$error = "Unknown image file type";
3469 my $lctype = lc $type;
3470 my @valid_exts = split /,/,
3471 BSE::Cfg->single->entry("valid image extensions", $lctype,
3472 $valid_exts{$lctype} || $lctype);
3474 my ($ext) = $basename =~ /\.(\w+)\z/;
3475 if (!$ext || !grep $_ eq lc $ext, @valid_exts) {
3476 $basename .= ".$valid_exts[0]";
3478 $$rbasename = $basename;
3480 return ($width, $height, $type);
3483 my $last_display_order = 0;
3486 my ($self, $cfg, $article, $image, %opts) = @_;
3488 my $errors = $opts{errors}
3489 or die "No errors parameter";
3491 my $imageref = $opts{name};
3492 if (defined $imageref && $imageref ne '') {
3493 if ($imageref =~ /^[a-z_]\w+$/i) {
3494 # make sure it's unique
3495 my @images = $self->get_images($article);
3496 for my $img (@images) {
3497 if (defined $img->{name} && lc $img->{name} eq lc $imageref) {
3498 $errors->{name} = 'Image name must be unique to the article';
3504 $errors->{name} = 'Image name must be empty or alphanumeric beginning with an alpha character';
3510 unless ($errors->{name}) {
3512 $self->validate_image_name($imageref, \$workmsg)
3513 or $errors->{name} = $workmsg;
3518 my ($width, $height, $type) =
3519 $self->_validate_image($opts{filename} || $image, $image, \$basename,
3522 $errors->{image} = $image_error;
3528 # for the sysopen() constants
3531 my $imagedir = cfg_image_dir($cfg);
3533 require DevHelp::FileUpload;
3535 my ($filename, $fh) =
3536 DevHelp::FileUpload->make_img_filename($imagedir, $basename, \$msg);
3537 unless ($filename) {
3538 $errors->{image} = $msg;
3548 # read the image in from the browser and output it to our output filehandle
3549 print $fh $buffer while read $image, $buffer, 1024;
3553 or die "Could not close image file $filename: $!";
3555 my $display_order = time;
3556 if ($display_order <= $last_display_order) {
3557 $display_order = $last_display_order + 1;
3559 $last_display_order = $display_order;
3561 my $alt = $opts{alt};
3562 defined $alt or $alt = '';
3563 my $url = $opts{url};
3564 defined $url or $url = '';
3567 articleId => $article->{id},
3573 displayOrder => $display_order,
3576 src => cfg_image_uri() . '/' . $filename,
3577 ftype => $self->_image_ftype($type),
3579 require BSE::TB::Images;
3580 my @cols = BSE::TB::Image->columns;
3582 my $imageobj = BSE::TB::Images->add(@image{@cols});
3584 my $storage = $opts{storage};
3585 defined $storage or $storage = 'local';
3586 my $image_manager = $self->_image_manager($cfg);
3587 local $SIG{__DIE__};
3590 $storage = $image_manager->select_store($filename, $storage, $imageobj);
3591 $src = $image_manager->store($filename, $storage, $imageobj);
3594 $imageobj->{src} = $src;
3595 $imageobj->{storage} = $storage;
3600 $errors->{flash} = $@;
3607 my ($self, $cfg, $image) = @_;
3609 my $data = $image->data_only;
3610 $data->{src} = $image->image_url($cfg);
3616 my ($self, $req, $article, $articles) = @_;
3618 $req->check_csrf("admin_add_image")
3619 or return $self->csrf_error($req, $article, "admin_add_image", "Add Image");
3620 $req->user_can(edit_images_add => $article)
3621 or return $self->_service_error($req, $article, $articles,
3622 "You don't have access to add new images to this article");
3624 my $cgi = $req->cgi;
3628 my $save_tags = $cgi->param("_save_tags");
3631 @tags = $cgi->param("tags");
3632 $self->_validate_tags(\@tags, \%errors);
3640 scalar($cgi->upload('image')),
3641 name => scalar($cgi->param('name')),
3642 alt => scalar($cgi->param('altIn')),
3643 url => scalar($cgi->param('url')),
3644 storage => scalar($cgi->param('storage')),
3646 filename => scalar($cgi->param("image")),
3650 or return $self->_service_error($req, $article, $articles, undef, \%errors);
3654 $imageobj->set_tags([ grep /\S/, @tags ], \$error);
3657 # typically a soft failure from the storage
3659 and $req->flash($errors{flash});
3661 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
3663 if ($cgi->param('_service')) {
3664 return $self->_service_success
3667 image => $imageobj->{id},
3671 elsif ($cgi->param("_") || $req->is_ajax) {
3672 my $resp = $req->json_content
3675 image => $self->_image_data($req->cfg, $imageobj),
3678 # the browser handles this directly, tell it that it's text
3679 $resp->{type} = "text/plain";
3684 return $self->refresh($article, $cgi, undef, 'New image added');
3688 sub _image_manager {
3691 require BSE::TB::Images;
3692 return BSE::TB::Images->storage_manager;
3697 my ($self, $req, $article, $articles, $imageid) = @_;
3699 $req->check_csrf("admin_remove_image")
3700 or return $self->csrf_error($req, $article, "admin_remove_image", "Remove Image");
3702 $req->user_can(edit_images_delete => $article)
3703 or return $self->_service_error($req, $article, $articles,
3704 "You don't have access to delete images from this article", {}, "ACCESS");
3708 my @images = $self->get_images($article);
3709 my ($image) = grep $_->{id} == $imageid, @images;
3711 if ($req->want_json_response) {
3712 return $self->_service_error($req, $article, $articles, "No such image", {}, "NOTFOUND");
3715 return $self->show_images($req, $article, $articles, "No such image");
3719 if ($image->{storage} ne 'local') {
3720 my $mgr = $self->_image_manager($req->cfg);
3721 $mgr->unstore($image->{image}, $image->{storage});
3724 my $imagedir = cfg_image_dir($req->cfg);
3727 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
3729 if ($req->want_json_response) {
3730 return $req->json_content
3736 return $self->refresh($article, $req->cgi, undef, 'Image removed');
3740 my ($self, $req, $article, $articles) = @_;
3742 $req->check_csrf("admin_move_image")
3743 or return $self->csrf_error($req, $article, "admin_move_image", "Move Image");
3744 $req->user_can(edit_images_reorder => $article)
3745 or return $self->edit_form($req, $article, $articles,
3746 "You don't have access to reorder images in this article");
3748 my $imageid = $req->cgi->param('imageid');
3749 my @images = $self->get_images($article);
3750 my ($imgindex) = grep $images[$_]{id} == $imageid, 0..$#images
3751 or return $self->edit_form($req, $article, $articles, "No such image");
3753 or return $self->edit_form($req, $article, $articles, "Image is already at the top");
3754 my ($to, $from) = @images[$imgindex-1, $imgindex];
3755 ($to->{displayOrder}, $from->{displayOrder}) =
3756 ($from->{displayOrder}, $to->{displayOrder});
3760 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
3762 return $self->refresh($article, $req->cgi, undef, 'Image moved');
3766 my ($self, $req, $article, $articles) = @_;
3768 $req->check_csrf("admin_move_image")
3769 or return $self->csrf_error($req, $article, "admin_move_image", "Move Image");
3770 $req->user_can(edit_images_reorder => $article)
3771 or return $self->edit_form($req, $article, $articles,
3772 "You don't have access to reorder images in this article");
3774 my $imageid = $req->cgi->param('imageid');
3775 my @images = $self->get_images($article);
3776 my ($imgindex) = grep $images[$_]{id} == $imageid, 0..$#images
3777 or return $self->edit_form($req, $article, $articles, "No such image");
3778 $imgindex < $#images
3779 or return $self->edit_form($req, $article, $articles, "Image is already at the end");
3780 my ($to, $from) = @images[$imgindex+1, $imgindex];
3781 ($to->{displayOrder}, $from->{displayOrder}) =
3782 ($from->{displayOrder}, $to->{displayOrder});
3786 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
3788 return $self->refresh($article, $req->cgi, undef, 'Image moved');
3792 my ($self, $req, $article) = @_;
3794 my $cgi = $req->cgi;
3795 my $cfg = $req->cfg;
3796 my $im_id = $cgi->param('im');
3798 if (defined $im_id && $im_id =~ /^\d+$/) {
3799 ($image) = grep $_->{id} == $im_id, $self->get_images($article);
3801 my $thumb_obj = $self->_get_thumbs_class();
3803 if ($image && $thumb_obj) {
3804 my $geometry_id = $cgi->param('g');
3805 defined $geometry_id or $geometry_id = 'editor';
3806 my $geometry = $cfg->entry('thumb geometries', $geometry_id, 'scale(200x200)');
3807 my $imagedir = cfg_image_dir();
3810 ($data, $type) = $thumb_obj->thumb_data
3812 filename => "$imagedir/$image->{image}",
3813 geometry => $geometry,
3818 type => 'text/plain',
3819 content => 'Error: '.$error
3823 if ($type && $data) {
3830 "Content-Length: ".length($data),
3831 "Cache-Control: max-age=3600",
3836 # grab the nothumb image
3837 my $uri = $cfg->entry('editor', 'default_thumbnail', cfg_dist_image_uri() . '/admin/nothumb.png');
3838 my $filebase = $cfg->content_base_path;
3839 if (open IMG, "<$filebase/$uri") {
3841 my $data = do { local $/; <IMG> };
3843 my $type = $uri =~ /\.(\w+)$/ ? $1 : 'png';
3846 type => "image/$type",
3848 headers => [ "Content-Length: ".length($data) ],
3855 content => "<html><body>Cannot make thumb or default image</body></html>",
3863 Display a form to allow editing an image.
3871 eimage - the image being edited
3875 normal article edit tags.
3881 eimage - the image being edited.
3885 sub req_edit_image {
3886 my ($self, $req, $article, $articles, $errors) = @_;
3888 my $cgi = $req->cgi;
3890 my $id = $cgi->param('image_id');
3892 my ($image) = grep $_->{id} == $id, $self->get_images($article)
3893 or return $self->edit_form($req, $article, $articles,
3895 $req->user_can(edit_images_save => $article)
3896 or return $self->edit_form($req, $article, $articles,
3897 "You don't have access to save image information for this article");
3899 $req->set_variable(eimage => $image);
3904 $self->low_edit_tags(\%acts, $req, $article, $articles, undef,
3906 eimage => [ \&tag_hash, $image ],
3907 error_img => [ \&tag_error_img, $req->cfg, $errors ],
3910 return $req->response('admin/image_edit', \%acts);
3915 Save changes to an image.
3931 alt, url, name - text fields to update
3935 image - replacement image data (if any)
3941 sub req_save_image {
3942 my ($self, $req, $article, $articles) = @_;
3944 $req->check_csrf("admin_save_image")
3945 or return $self->csrf_error($req, $article, "admin_save_image", "Save Image");
3946 my $cgi = $req->cgi;
3948 my $id = $cgi->param('image_id');
3950 my @images = $self->get_images($article);
3951 my ($image) = grep $_->{id} == $id, @images
3952 or return $self->_service_error($req, $article, $articles, "No such image",
3954 $req->user_can(edit_images_save => $article)
3955 or return $self->_service_error($req, $article, $articles,
3956 "You don't have access to save image information for this article", {}, "ACCESS");
3958 my $image_dir = cfg_image_dir($req->cfg);
3960 my $old_storage = $image->{storage};
3964 my $alt = $cgi->param('alt');
3965 defined $alt and $image->{alt} = $alt;
3966 my $url = $cgi->param('url');
3967 defined $url and $image->{url} = $url;
3968 my @other_images = grep $_->{id} != $id, @images;
3969 my $name = $cgi->param('name');
3970 if (defined $name) {
3972 if ($name !~ /^[a-z_]\w*$/i) {
3973 $errors{name} = 'Image name must be empty or alphanumeric and unique to the article';
3975 elsif (grep $name eq $_->{name}, @other_images) {
3976 $errors{name} = 'Image name must be unique to the article';
3979 $image->{name} = $name;
3983 $image->{name} = '';
3986 my $filename = $cgi->param('image');
3987 if (defined $filename && length $filename) {
3988 my $in_fh = $cgi->upload('image');
3992 my ($width, $height, $type) = $self->_validate_image
3993 ($filename, $in_fh, \$basename, \$image_error);
3995 require DevHelp::FileUpload;
3997 my ($image_name, $out_fh) = DevHelp::FileUpload->make_img_filename
3998 ($image_dir, $basename, \$msg);
4003 while ($data = <$in_fh>) {
4004 print $out_fh $data;
4009 my $full_filename = "$image_dir/$image_name";
4010 $delete_file = $image->{image};
4011 $image->{image} = $image_name;
4012 $image->{width} = $width;
4013 $image->{height} = $height;
4014 $image->{storage} = 'local'; # not on the remote store yet
4015 $image->{src} = cfg_image_uri() . '/' . $image_name;
4016 $image->{ftype} = $self->_image_ftype($type);
4019 $errors{image} = $msg;
4023 $errors{image} = $image_error;
4027 $errors{image} = "No image file received";
4030 my $save_tags = $cgi->param("_save_tags");
4033 @tags = $cgi->param("tags");
4034 $self->_validate_tags(\@tags, \%errors);
4037 if ($req->want_json_response) {
4038 return $self->_service_error($req, $article, $articles, undef,
4042 return $self->req_edit_image($req, $article, $articles, \%errors);
4046 my $new_storage = $cgi->param('storage');
4047 defined $new_storage or $new_storage = $image->{storage};
4051 $image->set_tags([ grep /\S/, @tags ], \$error);
4053 my $mgr = $self->_image_manager($req->cfg);
4055 if ($old_storage ne 'local') {
4056 $mgr->unstore($delete_file, $old_storage);
4058 unlink "$image_dir/$delete_file";
4060 $req->flash("Image saved");
4063 $mgr->select_store($image->{image}, $new_storage);
4064 if ($image->{storage} ne $new_storage) {
4065 # handles both new images (which sets storage to local) and changing
4066 # the storage for old images
4067 my $old_storage = $image->{storage};
4068 my $src = $mgr->store($image->{image}, $new_storage, $image);
4069 $image->{src} = $src;
4070 $image->{storage} = $new_storage;
4074 $@ and $req->flash("There was a problem adding it to the new storage: $@");
4075 if ($image->{storage} ne $old_storage && $old_storage ne 'local') {
4077 $mgr->unstore($image->{image}, $old_storage);
4079 $@ and $req->flash("There was a problem removing if from the old storage: $@");
4082 if ($req->want_json_response) {
4083 return $req->json_content
4086 image => $self->_image_data($req->cfg, $image),
4090 return $self->refresh($article, $cgi);
4093 =item a_order_images
4095 Change the order of images for an article (or global images).
4103 id - id of the article to change the image order for (-1 for global
4108 order - comma-separated list of image ids in the new order.
4114 sub req_order_images {
4115 my ($self, $req, $article, $articles) = @_;
4118 or return $self->_service_error($req, $article, $articles, "The function only permitted from Ajax", {}, "AJAXONLY");
4120 my $order = $req->cgi->param("order");
4122 or return $self->_service_error($req, $article, $articles, "order not supplied", {}, "NOORDER");
4123 $order =~ /^\d+(,\d+)*$/
4124 or return $self->_service_error($req, $article, $articles, "order not supplied", {}, "BADORDER");
4126 my @order = split /,/, $order;
4128 my @images = $article->set_image_order(\@order);
4130 return $req->json_content
4135 map $self->_image_data($req->cfg, $_), @images
4141 my ($self, $articles, $article) = @_;
4147 my ($self, $articles) = @_;
4152 sub _refresh_filelist {
4153 my ($self, $req, $article, $msg) = @_;
4155 return $self->refresh($article, $req->cgi, undef, $msg);
4159 my ($self, $req, $article, $articles, $msg, $errors) = @_;
4162 %acts = $self->low_edit_tags(\%acts, $req, $article, $articles, $msg, $errors);
4163 my $template = 'admin/filelist';
4165 return BSE::Template->get_response($template, $req->cfg, \%acts);
4172 maxlength => MAX_FILE_DISPLAYNAME_LENGTH,
4173 description => 'Filename'
4177 rules => 'dh_one_line',
4179 description => 'Description',
4183 description => 'Identifier',
4188 description => "Category",
4194 my ($self, $req, $article, $articles) = @_;
4196 $req->check_csrf("admin_add_file")
4197 or return $self->csrf_error($req, $article, "admin_add_file", "Add File");
4198 $req->user_can(edit_files_add => $article)
4199 or return $self->_service_error($req, $article, $articles,
4200 "You don't have access to add files to this article");
4203 my $cgi = $req->cgi;
4204 require BSE::TB::ArticleFiles;
4205 my @cols = BSE::TB::ArticleFile->columns;
4207 for my $col (@cols) {
4208 if (defined $cgi->param($col)) {
4209 $file{$col} = $cgi->param($col);
4215 $req->validate(errors => \%errors,
4216 fields => \%file_fields,
4217 section => $article->{id} == -1 ? 'Global File Validation' : 'Article File Validation');
4220 my $file = $cgi->upload('file');
4221 my $filename = $cgi->param("file");
4223 $errors{file} = 'Please enter a filename';
4225 if ($file && -z $file) {
4226 $errors{file} = 'File is empty';
4229 $file{forSale} = 0 + exists $file{forSale};
4230 $file{articleId} = $article->{id};
4231 $file{download} = 0 + exists $file{download};
4232 $file{requireUser} = 0 + exists $file{requireUser};
4233 $file{hide_from_list} = 0 + exists $file{hide_from_list};
4234 $file{category} ||= '';
4236 defined $file{name} or $file{name} = '';
4237 if (!$errors{name} && length $file{name} && $file{name} !~/^\w+$/) {
4238 $errors{name} = "Identifier must be a single word";
4240 if (!$errors{name} && length $file{name}) {
4241 my @files = $self->get_files($article);
4242 if (grep lc $_->{name} eq lc $file{name}, @files) {
4243 $errors{name} = "Duplicate file identifier $file{name}";
4248 and return $self->_service_error($req, $article, $articles, undef, \%errors);
4251 my $workfile = $filename;
4252 $workfile =~ s![^\w.:/\\-]+!_!g;
4253 $workfile =~ tr/_/_/s;
4254 $workfile =~ /([ \w.-]+)$/ and $basename = $1;
4255 $basename =~ tr/ /_/;
4256 $file{displayName} = $basename;
4257 $file{file} = $file;
4259 local $SIG{__DIE__};
4262 $article->add_file($self->cfg, %file);
4266 or return $self->_service_error($req, $article, $articles, $@);
4268 unless ($req->is_ajax) {
4269 $req->flash("New file added");
4275 file => $fileobj->data_only,
4278 my $storage = $cgi->param("storage") || "";
4282 $article->apply_storage($self->cfg, $fileobj, $storage, \$msg);
4285 if ($req->is_ajax) {
4286 push @{$json->{warnings}}, $msg;
4289 $req->flash_error($msg);
4294 if ($req->is_ajax) {
4295 push @{$json->{warnings}}, $@;
4298 $req->flash_error($@);
4302 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
4304 if ($req->is_ajax) {
4305 return $req->json_content($json);
4308 $self->_refresh_filelist($req, $article);
4313 my ($self, $req, $article, $articles) = @_;
4315 $req->check_csrf("admin_move_file")
4316 or return $self->csrf_error($req, $article, "admin_move_file", "Move File");
4318 $req->user_can('edit_files_reorder', $article)
4319 or return $self->edit_form($req, $article, $articles,
4320 "You don't have access to reorder files in this article");
4322 my $cgi = $req->cgi;
4323 my $id1 = $cgi->param('file1');
4324 my $id2 = $cgi->param('file2');
4327 my @files = $self->get_files($article);
4329 my ($file1) = grep $_->{id} == $id1, @files;
4330 my ($file2) = grep $_->{id} == $id2, @files;
4332 if ($file1 && $file2) {
4333 ($file1->{displayOrder}, $file2->{displayOrder})
4334 = ($file2->{displayOrder}, $file1->{displayOrder});
4340 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
4342 $self->refresh($article, $req->cgi, undef, 'File moved');
4346 my ($self, $req, $article, $articles) = @_;
4348 $req->check_csrf("admin_remove_file")
4349 or return $self->csrf_error($req, $article, "admin_remove_file", "Delete File");
4350 $req->user_can('edit_files_delete', $article)
4351 or return $self->edit_form($req, $article, $articles,
4352 "You don't have access to delete files from this article");
4354 my $cgi = $req->cgi;
4355 my $fileid = $cgi->param('file');
4357 my @files = $self->get_files($article);
4359 my ($file) = grep $_->{id} == $fileid, @files;
4362 if ($file->{storage} ne 'local') {
4363 my $mgr = $self->_file_manager($self->cfg);
4364 $mgr->unstore($file->{filename}, $file->{storage});
4367 $file->remove($req->cfg);
4371 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
4373 $self->_refresh_filelist($req, $article, 'File deleted');
4377 my ($self, $req, $article, $articles) = @_;
4379 $req->check_csrf("admin_save_files")
4380 or return $self->csrf_error($req, $article, "admin_save_files", "Save Files");
4382 $req->user_can('edit_files_save', $article)
4383 or return $self->edit_form($req, $article, $articles,
4384 "You don't have access to save file information for this article");
4385 my @files = $self->get_files($article);
4387 my $download_path = BSE::TB::ArticleFiles->download_path($self->{cfg});
4389 my $cgi = $req->cgi;
4395 my $change_count = 0;
4396 my @content_changed;
4397 for my $file (@files) {
4398 my $id = $file->{id};
4399 my $orig = $file->data_only;
4400 my $desc = $cgi->param("description_$id");
4401 defined $desc and $file->{description} = $desc;
4402 my $type = $cgi->param("contentType_$id");
4403 if (defined $type and $type ne $file->{contentType}) {
4404 ++$store_anyway{$id};
4405 $file->{contentType} = $type;
4407 my $notes = $cgi->param("notes_$id");
4408 defined $notes and $file->{notes} = $notes;
4409 my $category = $cgi->param("category_$id");
4410 defined $category and $file->{category} = $category;
4411 my $name = $cgi->param("name_$id");
4412 if (defined $name) {
4413 $file->{name} = $name;
4415 if ($name =~ /^\w+$/) {
4416 push @{$names{$name}}, $id;
4419 $errors{"name_$id"} = "Invalid file identifier $name";
4424 push @{$names{$file->{name}}}, $id
4425 if length $file->{name};
4427 if ($cgi->param('save_file_flags')) {
4428 my $download = 0 + defined $cgi->param("download_$id");
4429 if ($download != $file->{download}) {
4430 ++$store_anyway{$file->{id}};
4431 $file->{download} = $download;
4433 $file->{forSale} = 0 + defined $cgi->param("forSale_$id");
4434 $file->{requireUser} = 0 + defined $cgi->param("requireUser_$id");
4435 $file->{hide_from_list} = 0 + defined $cgi->param("hide_from_list_$id");
4438 my $filex = $cgi->param("file_$id");
4439 my $in_fh = $cgi->upload("file_$id");
4440 if (defined $filex && length $filex) {
4441 if (length $filex <= MAX_FILE_DISPLAYNAME_LENGTH) {
4444 require DevHelp::FileUpload;
4446 my ($file_name, $out_fh) = DevHelp::FileUpload->make_img_filename
4447 ($download_path, $filex . '', \$msg);
4452 while ($data = <$in_fh>) {
4453 print $out_fh $data;
4457 my $display_name = $filex;
4458 $display_name =~ s!.*[\\:/]!!;
4459 $display_name =~ s/[^\w._-]+/_/g;
4460 my $full_name = "$download_path/$file_name";
4461 push @old_files, [ $file->{filename}, $file->{storage} ];
4462 push @new_files, $file_name;
4464 $file->{filename} = $file_name;
4465 $file->{storage} = 'local';
4466 $file->{sizeInBytes} = -s $full_name;
4467 $file->{whenUploaded} = now_sqldatetime();
4468 $file->{displayName} = $display_name;
4469 push @content_changed, $file;
4472 $errors{"file_$id"} = $msg;
4476 $errors{"file_$id"} = "File is empty";
4480 $errors{"file_$id"} = "No file data received";
4484 $errors{"file_$id"} = "Filename too long";
4488 my $new = $file->data_only;
4490 for my $col ($file->columns) {
4491 if ($new->{$col} ne $orig->{$col}) {
4497 for my $name (keys %names) {
4498 if (@{$names{$name}} > 1) {
4499 for my $id (@{$names{$name}}) {
4500 $errors{"name_$id"} = 'File identifier must be unique to the article';
4505 # remove the uploaded replacements
4506 unlink map "$download_path/$_", @new_files;
4508 return $self->edit_form($req, $article, $articles, undef, \%errors);
4510 if ($change_count) {
4511 $req->flash("msg:bse/admin/edit/file/save/success_count", [ $change_count ]);
4514 $req->flash("msg:bse/admin/edit/file/save/success_none");
4516 my $mgr = $self->_file_manager($self->cfg);
4517 for my $file (@files) {
4520 my $storage = $cgi->param("storage_$file->{id}");
4521 defined $storage or $storage = 'local';
4523 $storage = $article->select_filestore($mgr, $file, $storage, \$msg);
4524 $msg and $req->flash($msg);
4525 if ($storage ne $file->{storage} || $store_anyway{$file->{id}}) {
4526 my $old_storage = $file->{storage};
4528 $file->{src} = $mgr->store($file->{filename}, $storage, $file);
4529 $file->{storage} = $storage;
4532 if ($old_storage ne $storage) {
4533 $mgr->unstore($file->{filename}, $old_storage);
4537 and $req->flash("Could not move $file->{displayName} to $storage: $@");
4541 # remove the replaced files
4542 for my $file (@old_files) {
4543 my ($filename, $storage) = @$file;
4546 $mgr->unstore($filename, $storage);
4549 and $req->flash("Error removing $filename from $storage: $@");
4551 unlink "$download_path/$filename";
4554 # update file type metadatas
4555 for my $file (@content_changed) {
4556 $file->set_handler($self->{cfg});
4560 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
4562 $self->_refresh_filelist($req, $article);
4566 my ($self, $req, $article, $articles, $errors) = @_;
4568 my $cgi = $req->cgi;
4570 my $id = $cgi->param('file_id');
4572 my ($file) = grep $_->{id} == $id, $self->get_files($article)
4573 or return $self->edit_form($req, $article, $articles,
4575 $req->user_can(edit_files_save => $article)
4576 or return $self->edit_form($req, $article, $articles,
4577 "You don't have access to save file information for this article");
4579 my $name = $cgi->param('name');
4580 $name && $name =~ /^\w+$/
4581 or return $self->edit_form($req, $article, $articles,