1 package BSE::Edit::Article;
3 use base qw(BSE::Edit::Base);
4 use BSE::Util::Tags qw(tag_error_img);
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);
10 use BSE::Util::Iterate;
12 use BSE::Util::ContentType qw(content_type);
13 use DevHelp::Date qw(dh_parse_date dh_parse_sql_date);
14 use constant MAX_FILE_DISPLAYNAME_LENGTH => 255;
16 our $VERSION = "1.009";
20 BSE::Edit::Article - editing functionality for BSE articles
24 Provides the base article editing functionality.
26 This is badly organized and documented.
35 my ($self, $req) = @_;
38 # AJAX/Prototype request
39 return $req->json_content
43 message => "Access forbidden: user not logged on",
45 error_code => "LOGON",
49 elsif ($req->cgi->param('_service')) {
52 content => 'Access Forbidden: login timed out',
54 "Status: 403", # forbidden
59 BSE::Template->get_refresh($req->url('logon'), $req->cfg);
63 sub article_dispatch {
64 my ($self, $req, $article, $articles) = @_;
66 BSE::Permissions->check_logon($req)
67 or return $self->not_logged_on($req);
71 my %actions = $self->article_actions;
72 for my $check (keys %actions) {
73 if ($cgi->param($check) || $cgi->param("$check.x")) {
80 ($action, @extraargs) = $self->other_article_actions($cgi);
83 my $method = $actions{$action};
84 return $self->$method($req, $article, $articles, @extraargs);
87 sub noarticle_dispatch {
88 my ($self, $req, $articles) = @_;
90 BSE::Permissions->check_logon($req)
91 or return BSE::Template->get_refresh($req->url('logon'), $req->cfg);
94 my $article = $self->_dummy_article($req, $articles, \$mymsg);
96 require BSE::Edit::Site;
97 my $site = BSE::Edit::Site->new(cfg=>$req->cfg, db=> BSE::DB->single);
98 return $site->edit_sections($req, $articles, $mymsg);
103 my %actions = $self->noarticle_actions;
104 for my $check (keys %actions) {
105 if ($cgi->param($check) || $cgi->param("$check.x")) {
110 my $method = $actions{$action};
111 return $self->$method($req, $article, $articles);
114 sub article_actions {
121 add_stepkid => 'add_stepkid',
122 del_stepkid => 'del_stepkid',
123 save_stepkids => 'save_stepkids',
124 add_stepparent => 'add_stepparent',
125 del_stepparent => 'del_stepparent',
126 save_stepparents => 'save_stepparents',
127 artimg => 'save_image_changes',
128 addimg => 'add_image',
129 a_edit_image => 'req_edit_image',
130 a_save_image => 'req_save_image',
131 a_order_images => 'req_order_images',
133 showimages => 'show_images',
134 process => 'save_image_changes',
135 removeimg => 'remove_img',
136 moveimgup => 'move_img_up',
137 moveimgdown => 'move_img_down',
138 filelist => 'filelist',
139 fileadd => 'fileadd',
140 fileswap => 'fileswap',
141 filedel => 'filedel',
142 filesave => 'filesave',
143 a_edit_file => 'req_edit_file',
144 a_save_file => 'req_save_file',
147 a_thumb => 'req_thumb',
148 a_ajax_get => 'req_ajax_get',
149 a_ajax_save_body => 'req_ajax_save_body',
150 a_ajax_set => 'req_ajax_set',
151 a_filemeta => 'req_filemeta',
152 a_csrfp => 'req_csrfp',
153 a_tree => 'req_tree',
154 a_article => 'req_article',
155 a_config => 'req_config',
156 a_restepkid => 'req_restepkid',
160 sub other_article_actions {
161 my ($self, $cgi) = @_;
163 for my $param ($cgi->param) {
164 if ($param =~ /^removeimg_(\d+)(\.x)?$/) {
165 return ('removeimg', $1 );
172 sub noarticle_actions {
177 a_csrfp => 'req_csrfp',
178 a_config => 'req_config',
183 my ($self, $parentid, $articles) = @_;
185 if ($parentid == -1) {
189 title=>'All Sections',
196 return $articles->getByPkey($parentid);
201 my ($object, $args) = @_;
203 my $value = $object->{$args};
204 defined $value or $value = '';
205 if ($value =~ /\cJ/ && $value =~ /\cM/) {
212 my ($object, $args) = @_;
214 my $value = $object->{$args};
215 defined $value or $value = '';
216 if ($value =~ /\cJ/ && $value =~ /\cM/) {
219 escape_html($value, '<>&"');
223 my ($level, $cfg) = @_;
225 escape_html($cfg->entry('level names', $level, 'Article'));
234 sub reparent_updown {
238 sub should_be_catalog {
239 my ($self, $article, $parent, $articles) = @_;
241 if ($article->{parentid} && (!$parent || $parent->{id} != $article->{parentid})) {
242 $parent = $articles->getByPkey($article->{id});
245 my $shopid = $self->cfg->entryErr('articles', 'shop');
247 return $article->{parentid} && $parent &&
248 ($article->{parentid} == $shopid ||
249 $parent->{generator} eq 'Generate::Catalog');
252 sub possible_parents {
253 my ($self, $article, $articles, $req) = @_;
258 my $shopid = $self->cfg->entryErr('articles', 'shop');
259 my @parents = $articles->getBy('level', $article->{level}-1);
260 @parents = grep { $_->{generator} eq 'Generate::Article'
261 && $_->{id} != $shopid } @parents;
263 # user can only select parent they can add to
264 @parents = grep $req->user_can('edit_add_child', $_), @parents;
266 @values = ( map {$_->{id}} @parents );
267 %labels = ( map { $_->{id} => "$_->{title} ($_->{id})" } @parents );
269 if ($article->{level} == 1 && $req->user_can('edit_add_child')) {
271 $labels{-1} = "No parent - this is a section";
274 if ($article->{id} && $self->reparent_updown($article)) {
275 # we also list the siblings and grandparent (if any)
276 my @siblings = grep $_->{id} != $article->{id} && $_->{id} != $shopid,
277 $articles->getBy(parentid => $article->{parentid});
278 @siblings = grep $req->user_can('edit_add_child', $_), @siblings;
279 push @values, map $_->{id}, @siblings;
280 @labels{map $_->{id}, @siblings} =
281 map { "-- move down a level -- $_->{title} ($_->{id})" } @siblings;
283 if ($article->{parentid} != -1) {
284 my $parent = $articles->getByPkey($article->{parentid});
285 if ($parent->{parentid} != -1) {
286 my $gparent = $articles->getByPkey($parent->{parentid});
287 if ($req->user_can('edit_add_child', $gparent)) {
288 push @values, $gparent->{id};
289 $labels{$gparent->{id}} =
290 "-- move up a level -- $gparent->{title} ($gparent->{id})";
294 if ($req->user_can('edit_add_child')) {
296 $labels{-1} = $req->catmsg("bse/admin/edit/uplabelsect");
302 return (\@values, \%labels);
306 my ($self, $article, $articles, $cgi, $req, $what) = @_;
308 if ($what eq 'listed') {
309 my @values = qw(0 1);
310 my %labels = ( 0=>"No", 1=>"Yes");
311 if ($article->{level} <= 2) {
312 $labels{2} = "In Sections, but not menu";
316 $labels{2} = "In content, but not menus";
319 return popup_menu(-name=>'listed',
322 -default=>$article->{listed});
325 my ($values, $labels) = $self->possible_parents($article, $articles, $req);
327 if (defined $article->{parentid}) {
328 $html = popup_menu(-name=>'parentid',
331 -default => $article->{parentid},
335 $html = popup_menu(-name=>'parentid',
341 # munge the html - we display a default value, so we need to wrap the
342 # default <select /> around this one
343 $html =~ s!^<select[^>]+>|</select>!!gi;
349 my ($arg, $acts, $funcname, $templater) = @_;
350 my ($func, $args) = split ' ', $arg, 2;
351 return $templater->perform($acts, $func, $args) ? 'checked' : '';
354 sub iter_get_images {
355 my ($self, $article) = @_;
357 $article->{id} or return;
358 $self->get_images($article);
362 my ($article, $articles) = @_;
365 $article->{id} or return;
366 if (UNIVERSAL::isa($article, 'Article')) {
367 @children = $article->children;
369 elsif ($article->{id}) {
370 @children = $articles->children($article->{id});
373 return sort { $b->{displayOrder} <=> $a->{displayOrder} } @children;
376 sub tag_if_have_child_type {
377 my ($level, $cfg) = @_;
379 defined $cfg->entry("level names", $level+1);
383 my ($args, $acts, $isname, $templater) = @_;
385 my ($func, $funcargs) = split ' ', $args, 2;
386 return $templater->perform($acts, $func, $funcargs) ? 'Yes' : 'No';
389 sub default_template {
390 my ($self, $article, $cfg, $templates) = @_;
392 if ($article->{parentid}) {
393 my $template = $cfg->entry("children of $article->{parentid}", "template");
395 if $template && grep $_ eq $template, @$templates;
397 if ($article->{level}) {
398 my $template = $cfg->entry("level $article->{level}", "template");
400 if $template && grep $_ eq $template, @$templates;
402 return $templates->[0];
406 my ($self, $article, $cfg, $cgi) = @_;
408 my @templates = sort { $a->{name} cmp $b->{name} } $self->templates_long($article);
410 if ($article->{template} && grep $_->{name} eq $article->{template}, @templates) {
411 $default = $article->{template};
414 my @template_names = map $_->{name}, @templates;
415 $default = $self->default_template($article, $cfg, \@template_names);
422 $_->{name} eq $_->{description}
424 : "$_->{description} ($_->{name})"
427 return popup_menu(-name => 'template',
428 -values => [ map $_->{name}, @templates ],
430 -default => $default,
435 my ($self, $article) = @_;
438 my $imagedir = cfg_image_dir($self->cfg);
439 if (opendir TITLE_IMAGES, "$imagedir/titles") {
441 grep -f "$imagedir/titles/$_" && /\.(gif|jpeg|jpg|png)$/i,
442 readdir TITLE_IMAGES;
443 closedir TITLE_IMAGES;
449 sub tag_title_images {
450 my ($self, $article, $cfg, $cgi) = @_;
452 my @images = $self->title_images($article);
453 my @values = ( '', @images );
454 my %labels = ( '' => 'None', map { $_ => $_ } @images );
456 popup_menu(-name=>'titleImage',
459 -default=>$article->{id} ? $article->{titleImage} : '',
463 sub base_template_dirs {
468 my ($self, $article) = @_;
470 my @dirs = $self->base_template_dirs;
471 if (my $parentid = $article->{parentid}) {
472 my $section = "children of $parentid";
473 if (my $dirs = $self->cfg->entry($section, 'template_dirs')) {
474 push @dirs, split /,/, $dirs;
477 if (my $id = $article->{id}) {
478 my $section = "article $id";
479 if (my $dirs = $self->{cfg}->entry($section, 'template_dirs')) {
480 push @dirs, split /,/, $dirs;
483 if ($article->{level}) {
484 push @dirs, $article->{level};
485 my $dirs = $self->{cfg}->entry("level $article->{level}", 'template_dirs');
486 push @dirs, split /,/, $dirs if $dirs;
493 my ($self, $article) = @_;
495 my @dirs = $self->template_dirs($article);
497 my @basedirs = BSE::Template->template_dirs($self->{cfg});
498 for my $basedir (@basedirs) {
499 for my $dir (@dirs) {
500 my $path = File::Spec->catdir($basedir, $dir);
502 if (opendir TEMPLATE_DIR, $path) {
503 push(@templates, sort map "$dir/$_",
504 grep -f "$path/$_" && /\.(tmpl|html)$/i, readdir TEMPLATE_DIR);
505 closedir TEMPLATE_DIR;
511 # eliminate any dups, and order it nicely
513 @templates = sort { lc($a) cmp lc($b) }
514 grep !$seen{$_}++, @templates;
516 return (@templates, $self->extra_templates($article));
519 sub extra_templates {
520 my ($self, $article) = @_;
522 my $basedir = $self->{cfg}->entryVar('paths', 'templates');
524 if (my $id = $article->{id}) {
525 push @templates, 'index.tmpl'
526 if $id == 1 && -f "$basedir/index.html";
527 push @templates, 'index2.tmpl'
528 if $id == 2 && -f "$basedir/index2.html";
529 my $shopid = $self->{cfg}->entryErr('articles', 'shop');
530 push @templates, "shop_sect.tmpl"
531 if $id == $shopid && -f "$basedir/shop_sect.tmpl";
532 my $section = "article $id";
533 my $extras = $self->{cfg}->entry($section, 'extra_templates');
534 push @templates, grep /\.(tmpl|html)$/i, split /,/, $extras
544 return '' unless $article->{id} && $article->{id} != -1;
546 <a href="$ENV{SCRIPT_NAME}?id=$article->{parentid}">Edit parent</a> |
553 return unless $article->{id} && $article->{id} > 0;
557 sub _load_step_kids {
558 my ($article, $step_kids) = @_;
560 my @stepkids = OtherParents->getBy(parentId=>$article->{id}) if $article->{id};
561 %$step_kids = map { $_->{childId} => $_ } @stepkids;
562 $step_kids->{loaded} = 1;
565 sub tag_if_step_kid {
566 my ($article, $allkids, $rallkid_index, $step_kids) = @_;
568 _load_step_kids($article, $step_kids) unless $step_kids->{loaded};
570 my $kid = $allkids->[$$rallkid_index]
572 exists $step_kids->{$kid->{id}};
576 my ($article, $allkids, $rallkid_index, $step_kids, $arg) = @_;
578 _load_step_kids($article, $step_kids) unless $step_kids->{loaded};
580 my $kid = $allkids->[$$rallkid_index]
582 my $step_kid = $step_kids->{$kid->{id}}
585 #print STDERR "found kid (want $arg): ", Dumper($kid), Dumper($step_kid);
586 escape_html($step_kid->{$arg});
589 sub tag_move_stepkid {
590 my ($self, $cgi, $req, $article, $allkids, $rallkids_index, $arg,
591 $acts, $funcname, $templater) = @_;
593 $req->user_can(edit_reorder_children => $article)
596 @$allkids > 1 or return '';
598 my ($img_prefix, $urladd) = DevHelp::Tags->get_parms($arg, $acts, $templater);
599 $img_prefix = '' unless defined $img_prefix;
600 $urladd = '' unless defined $urladd;
602 my $cgi_uri = $self->{cfg}->entry('uri', 'cgi', '/cgi-bin');
603 my $url = $ENV{SCRIPT_NAME} . "?id=$article->{id}";
604 if ($cgi->param('_t')) {
605 $url .= "&_t=".$cgi->param('_t');
610 if ($$rallkids_index < $#$allkids) {
611 $down_url = "$cgi_uri/admin/move.pl?stepparent=$article->{id}&d=swap&id=$allkids->[$$rallkids_index]{id}&other=$allkids->[$$rallkids_index+1]{id}";
614 if ($$rallkids_index > 0) {
615 $up_url = "$cgi_uri/admin/move.pl?stepparent=$article->{id}&d=swap&id=$allkids->[$$rallkids_index]{id}&other=$allkids->[$$rallkids_index-1]{id}";
618 return make_arrows($req->cfg, $down_url, $up_url, $url, $img_prefix);
621 sub possible_stepkids {
622 my ($req, $article, $articles, $stepkids) = @_;
624 $req->user_can(edit_stepkid_add => $article)
630 my @possible = sort { lc $a->{title} cmp lc $b->{title} }
631 $article->possible_stepchildren;
632 if ($req->access_control && $req->cfg->entry('basic', 'access_filter_steps', 0)) {
633 @possible = grep $req->user_can(edit_stepparent_add => $_->{id}), @possible;
638 sub tag_possible_stepkids {
639 my ($step_kids, $req, $article, $possstepkids, $articles, $cgi) = @_;
641 _load_step_kids($article, $step_kids) unless $step_kids->{loaded};
642 @$possstepkids = possible_stepkids($req, $article, $articles, $step_kids)
643 unless @$possstepkids;
644 my %labels = map { $_->{id} => "$_->{title} ($_->{id})" } @$possstepkids;
646 popup_menu(-name=>'stepkid',
647 -values=> [ map $_->{id}, @$possstepkids ],
648 -labels => \%labels);
651 sub tag_if_possible_stepkids {
652 my ($step_kids, $req, $article, $possstepkids, $articles, $cgi) = @_;
654 _load_step_kids($article, $step_kids) unless $step_kids->{loaded};
655 @$possstepkids = possible_stepkids($req, $article, $articles, $step_kids)
656 unless @$possstepkids;
661 sub iter_get_stepparents {
664 return unless $article->{id} && $article->{id} > 0;
666 OtherParents->getBy(childId=>$article->{id});
669 sub tag_ifStepParents {
670 my ($args, $acts, $funcname, $templater) = @_;
672 return $templater->perform($acts, 'ifStepparents', '');
675 sub tag_stepparent_targ {
676 my ($article, $targs, $rindex, $arg) = @_;
678 if ($article->{id} && $article->{id} > 0 && !@$targs) {
679 @$targs = $article->step_parents;
681 escape_html($targs->[$$rindex]{$arg});
684 sub tag_move_stepparent {
685 my ($self, $cgi, $req, $article, $stepparents, $rindex, $arg,
686 $acts, $funcname, $templater) = @_;
688 $req->user_can(edit_reorder_stepparents => $article)
691 @$stepparents > 1 or return '';
693 my ($img_prefix, $urladd) = DevHelp::Tags->get_parms($arg, $acts, $templater);
694 $img_prefix = '' unless defined $img_prefix;
695 $urladd = '' unless defined $urladd;
697 my $cgi_uri = $self->{cfg}->entry('uri', 'cgi', '/cgi-bin');
698 my $images_uri = $self->{cfg}->entry('uri', 'images', '/images');
700 my $url = $ENV{SCRIPT_NAME} . "?id=$article->{id}";
701 if ($cgi->param('_t')) {
702 $url .= "&_t=".$cgi->param('_t');
705 $url .= "#stepparents";
706 my $blank = qq!<img src="$images_uri/trans_pixel.gif" width="17" height="13" border="0" align="absbottom" alt="" />!;
708 if ($$rindex < $#$stepparents) {
709 $down_url = "$cgi_uri/admin/move.pl?stepchild=$article->{id}&id=$stepparents->[$$rindex]{parentId}&d=swap&other=$stepparents->[$$rindex+1]{parentId}";
713 $up_url = "$cgi_uri/admin/move.pl?stepchild=$article->{id}&id=$stepparents->[$$rindex]{parentId}&d=swap&other=$stepparents->[$$rindex-1]{parentId}";
716 return make_arrows($req->cfg, $down_url, $up_url, $url, $img_prefix);
719 sub _stepparent_possibles {
720 my ($req, $article, $articles, $targs) = @_;
722 $req->user_can(edit_stepparent_add => $article)
728 @$targs = $article->step_parents unless @$targs;
729 my %targs = map { $_->{id}, 1 } @$targs;
730 my @possibles = $article->possible_stepparents;
731 if ($req->access_control && $req->cfg->entry('basic', 'access_filter_steps', 0)) {
732 @possibles = grep $req->user_can(edit_stepkid_add => $_->{id}), @possibles;
734 @possibles = sort { lc $a->{title} cmp lc $b->{title} } @possibles;
739 sub tag_if_stepparent_possibles {
740 my ($req, $article, $articles, $targs, $possibles) = @_;
742 if ($article->{id} && $article->{id} > 0 && !@$possibles) {
743 @$possibles = _stepparent_possibles($req, $article, $articles, $targs);
748 sub tag_stepparent_possibles {
749 my ($cgi, $req, $article, $articles, $targs, $possibles) = @_;
751 if ($article->{id} && $article->{id} > 0 && !@$possibles) {
752 @$possibles = _stepparent_possibles($req, $article, $articles, $targs);
754 popup_menu(-name=>'stepparent',
755 -values => [ map $_->{id}, @$possibles ],
756 -labels => { map { $_->{id}, "$_->{title} ($_->{id})" }
761 my ($self, $article) = @_;
763 return $self->get_files($article);
767 my ($self, $article) = @_;
769 return unless $article->{id} && $article->{id} > 0;
771 return $article->files;
774 sub tag_edit_parent {
777 return '' unless $article->{id} && $article->{id} != -1;
780 <a href="$ENV{SCRIPT_NAME}?id=$article->{parentid}">Edit parent</a> |
784 sub tag_if_children {
785 my ($args, $acts, $funcname, $templater) = @_;
787 return $templater->perform($acts, 'ifChildren', '');
791 my ($self, $req, $article, $kids, $rindex, $arg,
792 $acts, $funcname, $templater) = @_;
794 $req->user_can('edit_reorder_children', $article)
797 @$kids > 1 or return '';
799 $$rindex >=0 && $$rindex < @$kids
800 or return '** movechild can only be used in the children iterator **';
802 my ($img_prefix, $urladd) = DevHelp::Tags->get_parms($arg, $acts, $templater);
803 $img_prefix = '' unless defined $img_prefix;
804 $urladd = '' unless defined $urladd;
806 my $cgi_uri = $self->{cfg}->entry('uri', 'cgi', '/cgi-bin');
807 my $images_uri = $self->{cfg}->entry('uri', 'images', '/images');
808 my $urlbase = admin_base_url($req->cfg);
809 my $refresh_url = "$urlbase$ENV{SCRIPT_NAME}?id=$article->{id}";
810 my $t = $req->cgi->param('_t');
811 if ($t && $t =~ /^\w+$/) {
812 $refresh_url .= "&_t=$t";
815 $refresh_url .= $urladd;
817 my $id = $kids->[$$rindex]{id};
819 if ($$rindex < $#$kids) {
820 $down_url = "$cgi_uri/admin/move.pl?id=$id&d=down&edit=1&all=1";
824 $up_url = "$cgi_uri/admin/move.pl?id=$id&d=up&edit=1&all=1"
827 return make_arrows($req->cfg, $down_url, $up_url, $refresh_url, $img_prefix);
831 my ($cfg, $article, $args, $acts, $funcname, $templater) = @_;
832 my ($which, $name) = split / /, $args, 2;
836 && ($gen_class = $templater->perform($acts, $which, 'generator'))) {
837 eval "use $gen_class";
839 my $gen = $gen_class->new(top => $article, cfg => $cfg);
840 my $link = $gen->edit_link($templater->perform($acts, $which, 'id'));
841 return qq!<a href="$link">$name</a>!;
848 my ($req, $article, $rindex, $images, $arg,
849 $acts, $funcname, $templater) = @_;
851 $req->user_can(edit_images_reorder => $article)
854 @$images > 1 or return '';
856 $$rindex >= 0 && $$rindex < @$images
857 or return '** imgmove can only be used in image iterator **';
859 my ($img_prefix, $urladd) = DevHelp::Tags->get_parms($arg, $acts, $templater);
860 $img_prefix = '' unless defined $img_prefix;
861 $urladd = '' unless defined $urladd;
863 my $urlbase = admin_base_url($req->cfg);
864 my $url = "$urlbase$ENV{SCRIPT_NAME}?id=$article->{id}";
865 my $t = $req->cgi->param('_t');
866 if ($t && $t =~ /^\w+$/) {
871 my $image = $images->[$$rindex];
872 my $csrfp = $req->get_csrf_token("admin_move_image");
873 my $baseurl = "$ENV{SCRIPT_NAME}?id=$article->{id}&imageid=$image->{id}&";
874 $baseurl .= "_csrfp=$csrfp&";
876 if ($$rindex < $#$images) {
877 $down_url = $baseurl . "moveimgdown=1";
881 $up_url = $baseurl . "moveimgup=1";
883 return make_arrows($req->cfg, $down_url, $up_url, $url, $img_prefix);
887 my ($self, $req, $article, $files, $rindex, $arg,
888 $acts, $funcname, $templater) = @_;
890 $req->user_can('edit_files_reorder', $article)
893 @$files > 1 or return '';
895 my ($img_prefix, $urladd) = DevHelp::Tags->get_parms($arg, $acts, $templater);
896 $img_prefix = '' unless defined $img_prefix;
897 $urladd = '' unless defined $urladd;
899 $$rindex >= 0 && $$rindex < @$files
900 or return '** movefiles can only be used in the files iterator **';
902 my $urlbase = admin_base_url($req->cfg);
903 my $url = "$urlbase$ENV{SCRIPT_NAME}?id=$article->{id}$urladd";
904 my $t = $req->cgi->param('_t');
905 if ($t && $t =~ /^\w+$/) {
910 my $csrfp = $req->get_csrf_token("admin_move_file");
911 my $baseurl = "$ENV{SCRIPT_NAME}?fileswap=1&id=$article->{id}&";
912 $baseurl .= "_csrfp=$csrfp&";
913 if ($$rindex < $#$files) {
914 $down_url = $baseurl . "file1=$files->[$$rindex]{id}&file2=$files->[$$rindex+1]{id}";
918 $up_url = $baseurl . "file1=$files->[$$rindex]{id}&file2=$files->[$$rindex-1]{id}";
921 return make_arrows($req->cfg, $down_url, $up_url, $url, $img_prefix);
925 my ($article, $cgi, $args, $acts, $funcname, $templater) = @_;
927 my ($col, $func, $funcargs) = split ' ', $args, 3;
928 my $value = $cgi->param($col);
929 if (defined $value) {
930 return escape_html($value);
934 return $templater->perform($acts, $func, $funcargs);
937 $value = $article->{$args};
938 defined $value or $value = '';
939 return escape_html($value);
944 sub iter_admin_users {
945 require BSE::TB::AdminUsers;
947 BSE::TB::AdminUsers->all;
950 sub iter_admin_groups {
951 require BSE::TB::AdminGroups;
953 BSE::TB::AdminGroups->all;
956 sub tag_if_field_perm {
957 my ($req, $article, $field) = @_;
959 unless ($field =~ /^\w+$/) {
960 print STDERR "Bad fieldname '$field'\n";
963 if ($article->{id}) {
964 return $req->user_can("edit_field_edit_$field", $article);
967 #print STDERR "adding, always successful\n";
973 my ($self, $req, $article, $args, $acts, $funcname, $templater) = @_;
975 my ($col, $func, $funcargs) = split ' ', $args, 3;
976 if ($article->{id}) {
978 return $templater->perform($acts, $func, $funcargs);
981 my $value = $article->{$args};
982 defined $value or $value = '';
983 return escape_html($value, '<>&"');
987 my $value = $self->default_value($req, $article, $col);
988 defined $value or $value = '';
989 return escape_html($value, '<>&"');
999 sub tag_if_flag_set {
1000 my ($article, $arg, $acts, $funcname, $templater) = @_;
1002 my @args = DevHelp::Tags->get_parms($arg, $acts, $templater);
1005 return index($article->{flags}, $args[0]) >= 0;
1009 my ($article, $articles) = @_;
1012 my $temp = $article;
1013 defined($temp->{parentid}) or return;
1014 while ($temp->{parentid} > 0
1015 and my $crumb = $articles->getByPkey($temp->{parentid})) {
1016 unshift @crumbs, $crumb;
1024 my ($args, $acts, $funcname, $templater) = @_;
1026 exists $acts->{$args} or return "** need an article name **";
1027 my $generator = $templater->perform($acts, $args, 'generator');
1029 $generator =~ /^(?:BSE::)?Generate::(\w+)$/
1030 or return "** invalid generator $generator **";
1035 sub _get_thumbs_class {
1038 $self->{cfg}->entry('editor', 'allow_thumb', 0)
1041 my $class = $self->{cfg}->entry('editor', 'thumbs_class')
1044 (my $filename = "$class.pm") =~ s!::!/!g;
1045 eval { require $filename; };
1047 print STDERR "** Error loading thumbs_class $class ($filename): $@\n";
1051 eval { $obj = $class->new($self->{cfg}) };
1053 print STDERR "** Error creating thumbs objects $class: $@\n";
1060 sub tag_thumbimage {
1061 my ($cfg, $thumbs_obj, $current_image, $args) = @_;
1063 $thumbs_obj or return '';
1065 $$current_image or return '** no current image **';
1067 my $imagedir = cfg_image_dir($cfg);
1069 my $filename = "$imagedir/$$current_image->{image}";
1070 -e $filename or return "** image file missing **";
1072 defined $args && $args =~ /\S/
1073 or $args = "editor";
1075 my $image = $$current_image;
1076 return $image->thumb
1083 sub tag_file_display {
1084 my ($self, $files, $file_index) = @_;
1086 $$file_index >= 0 && $$file_index < @$files
1087 or return "* file_display only usable inside a files iterator *";
1088 my $file = $files->[$$file_index];
1090 my $disp_type = $self->cfg->entry("editor", "file_display", "");
1092 return $file->inline
1095 field => $disp_type,
1100 my ($self, $cfg, $rcurrent, $args) = @_;
1105 my ($align, $rest) = split ' ', $args, 2;
1107 if ($align && exists $im->{$align}) {
1108 if ($align eq 'src') {
1109 return escape_html($im->image_url($self->{cfg}));
1112 return escape_html($im->{$align});
1116 return $im->formatted
1126 my ($self, $article) = @_;
1131 return $article->tag_objects;
1135 my ($self, $acts, $request, $article, $articles, $msg, $errors) = @_;
1137 my $cgi = $request->cgi;
1138 my $show_full = $cgi->param('f_showfull');
1139 my $if_error = $msg || ($errors && keys %$errors) || $request->cgi->param("_e");
1140 $msg ||= join "\n", map escape_html($_), $cgi->param('message'), $cgi->param('m');
1141 $msg ||= $request->message($errors);
1143 if ($article->{id}) {
1144 if ($article->{parentid} > 0) {
1145 $parent = $article->parent;
1148 $parent = { title=>"No parent - this is a section", id=>-1 };
1152 $parent = { title=>"How did we get here?", id=>0 };
1154 my $cfg = $self->{cfg};
1155 my $mbcs = $cfg->entry('html', 'mbcs', 0);
1156 my $tag_hash = $mbcs ? \&tag_hash_mbcs : \&tag_hash;
1157 my $thumbs_obj_real = $self->_get_thumbs_class();
1158 my $thumbs_obj = $show_full ? undef : $thumbs_obj_real;
1169 my $stepparent_index;
1170 my @stepparent_targs;
1171 my @stepparentpossibles;
1176 my $it = BSE::Util::Iterate->new;
1177 my $ito = BSE::Util::Iterate::Objects->new;
1180 $request->admin_tags,
1181 article => [ $tag_hash, $article ],
1182 old => [ \&tag_old, $article, $cgi ],
1183 default => [ \&tag_default, $self, $request, $article ],
1184 articleType => [ \&tag_art_type, $article->{level}, $cfg ],
1185 parentType => [ \&tag_art_type, $article->{level}-1, $cfg ],
1186 ifNew => [ \&tag_if_new, $article ],
1187 list => [ \&tag_list, $self, $article, $articles, $cgi, $request ],
1188 script => $ENV{SCRIPT_NAME},
1189 level => $article->{level},
1190 checked => \&tag_checked,
1192 ([ \&iter_get_images, $self, $article ], 'image', 'images', \@images,
1193 \$image_index, undef, \$current_image),
1194 image => [ tag_image => $self, $cfg, \$current_image ],
1195 thumbimage => [ \&tag_thumbimage, $cfg, $thumbs_obj, \$current_image ],
1196 ifThumbs => defined($thumbs_obj),
1197 ifCanThumbs => defined($thumbs_obj_real),
1198 imgmove => [ \&tag_imgmove, $request, $article, \$image_index, \@images ],
1200 ifError => $if_error,
1201 DevHelp::Tags->make_iterator2
1202 ([ \&iter_get_kids, $article, $articles ],
1203 'child', 'children', \@children, \$child_index),
1204 ifchildren => \&tag_if_children,
1205 childtype => [ \&tag_art_type, $article->{level}+1, $cfg ],
1206 ifHaveChildType => [ \&tag_if_have_child_type, $article->{level}, $cfg ],
1207 movechild => [ \&tag_movechild, $self, $request, $article, \@children,
1210 templates => [ \&tag_templates, $self, $article, $cfg, $cgi ],
1211 titleImages => [ \&tag_title_images, $self, $article, $cfg, $cgi ],
1212 editParent => [ \&tag_edit_parent, $article ],
1213 DevHelp::Tags->make_iterator2
1214 ([ \&iter_allkids, $article ], 'kid', 'kids', \@allkids, \$allkid_index),
1216 [ \&tag_if_step_kid, $article, \@allkids, \$allkid_index, \%stepkids ],
1217 stepkid => [ \&tag_step_kid, $article, \@allkids, \$allkid_index,
1220 [ \&tag_move_stepkid, $self, $cgi, $request, $article, \@allkids,
1222 possible_stepkids =>
1223 [ \&tag_possible_stepkids, \%stepkids, $request, $article,
1224 \@possstepkids, $articles, $cgi ],
1226 [ \&tag_if_possible_stepkids, \%stepkids, $request, $article,
1227 \@possstepkids, $articles, $cgi ],
1228 DevHelp::Tags->make_iterator2
1229 ( [ \&iter_get_stepparents, $article ], 'stepparent', 'stepparents',
1230 \@stepparents, \$stepparent_index),
1231 ifStepParents => \&tag_ifStepParents,
1233 [ \&tag_stepparent_targ, $article, \@stepparent_targs,
1234 \$stepparent_index ],
1236 [ \&tag_move_stepparent, $self, $cgi, $request, $article, \@stepparents,
1237 \$stepparent_index ],
1238 ifStepparentPossibles =>
1239 [ \&tag_if_stepparent_possibles, $request, $article, $articles,
1240 \@stepparent_targs, \@stepparentpossibles, ],
1241 stepparent_possibles =>
1242 [ \&tag_stepparent_possibles, $cgi, $request, $article, $articles,
1243 \@stepparent_targs, \@stepparentpossibles, ],
1244 DevHelp::Tags->make_iterator2
1245 ([ iter_files => $self, $article ], 'file', 'files', \@files, \$file_index ),
1247 [ \&tag_movefiles, $self, $request, $article, \@files, \$file_index ],
1250 code => [ iter_file_metas => $self, \@files, \$file_index ],
1251 plural => "file_metas",
1252 single => "file_meta",
1255 file_display => [ tag_file_display => $self, \@files, \$file_index ],
1256 DevHelp::Tags->make_iterator2
1257 (\&iter_admin_users, 'iadminuser', 'adminusers'),
1258 DevHelp::Tags->make_iterator2
1259 (\&iter_admin_groups, 'iadmingroup', 'admingroups'),
1260 edit => [ \&tag_edit_link, $cfg, $article ],
1261 error => [ $tag_hash, $errors ],
1262 error_img => [ \&tag_error_img, $cfg, $errors ],
1263 ifFieldPerm => [ \&tag_if_field_perm, $request, $article ],
1264 parent => [ $tag_hash, $parent ],
1265 DevHelp::Tags->make_iterator2
1266 ([ \&iter_flags, $self ], 'flag', 'flags' ),
1267 ifFlagSet => [ \&tag_if_flag_set, $article ],
1268 DevHelp::Tags->make_iterator2
1269 ([ \&iter_crumbs, $article, $articles ], 'crumb', 'crumbs' ),
1270 typename => \&tag_typename,
1271 $it->make_iterator([ \&iter_groups, $request ],
1272 'group', 'groups', \@groups, undef, undef,
1274 $it->make_iterator([ iter_image_stores => $self],
1275 'image_store', 'image_stores'),
1276 $it->make_iterator([ iter_file_stores => $self],
1277 'file_store', 'file_stores'),
1278 ifGroupRequired => [ \&tag_ifGroupRequired, $article, \$current_group ],
1283 code => [ iter_tags => $self, $article ],
1288 sub iter_image_stores {
1291 my $mgr = $self->_image_manager;
1293 return map +{ name => $_->name, description => $_->description },
1300 require BSE::TB::ArticleFiles;
1302 return BSE::TB::ArticleFiles->file_manager($self->cfg);
1305 sub iter_file_stores {
1308 require BSE::TB::ArticleFiles;
1309 my $mgr = $self->_file_manager($self->cfg);
1311 return map +{ name => $_->name, description => $_->description },
1318 require BSE::TB::SiteUserGroups;
1319 BSE::TB::SiteUserGroups->admin_and_query_groups($req->cfg);
1322 sub tag_ifGroupRequired {
1323 my ($article, $rgroup) = @_;
1325 $$rgroup or return 0;
1327 $article->is_accessible_to($$rgroup);
1331 my ($self, $article, $cgi) = @_;
1333 my $base = $article->{level};
1334 my $t = $cgi->param('_t');
1335 if ($t && $t =~ /^\w+$/) {
1338 return $self->{cfg}->entry('admin templates', $base,
1339 "admin/edit_$base");
1343 my ($self, $article, $cgi) = @_;
1345 $self->edit_template($article, $cgi);
1349 my ($self, $request, $article, $articles, $msg, $errors) = @_;
1351 my $cgi = $request->cgi;
1353 %acts = $self->low_edit_tags(\%acts, $request, $article, $articles, $msg,
1355 my $template = $article->{id} ?
1356 $self->edit_template($article, $cgi) : $self->add_template($article, $cgi);
1358 return $request->response($template, \%acts);
1362 my ($self, $request, $article, $articles, $msg, $errors) = @_;
1364 return $self->low_edit_form($request, $article, $articles, $msg, $errors);
1367 sub _dummy_article {
1368 my ($self, $req, $articles, $rmsg) = @_;
1371 my $cgi = $req->cgi;
1372 my $parentid = $cgi->param('parentid');
1374 if ($parentid =~ /^\d+$/) {
1375 if (my $parent = $self->get_parent($parentid, $articles)) {
1376 $level = $parent->{level}+1;
1382 elsif ($parentid eq "-1") {
1386 unless (defined $level) {
1387 $level = $cgi->param('level');
1388 undef $level unless defined $level && $level =~ /^\d+$/
1389 && $level > 0 && $level < 100;
1390 defined $level or $level = 3;
1394 my @cols = Article->columns;
1395 @article{@cols} = ('') x @cols;
1397 $article{parentid} = $parentid;
1398 $article{level} = $level;
1399 $article{body} = '<maximum of 64Kb>';
1400 $article{listed} = 1;
1401 $article{generator} = $self->generator;
1403 my ($values, $labels) = $self->possible_parents(\%article, $articles, $req);
1405 $$rmsg = "You can't add children to any article at that level";
1413 my ($self, $req, $article, $articles, $msg, $errors) = @_;
1415 return $self->low_edit_form($req, $article, $articles, $msg, $errors);
1418 sub generator { 'Generate::Article' }
1423 my $gen = $self->generator;
1425 ($gen =~ /(\w+)$/)[0] || 'Article';
1428 sub _validate_common {
1429 my ($self, $data, $articles, $errors, $article) = @_;
1431 # if (defined $data->{parentid} && $data->{parentid} =~ /^(?:-1|\d+)$/) {
1432 # unless ($data->{parentid} == -1 or
1433 # $articles->getByPkey($data->{parentid})) {
1434 # $errors->{parentid} = "Selected parent article doesn't exist";
1438 # $errors->{parentid} = "You need to select a valid parent";
1440 if (exists $data->{title} && $data->{title} !~ /\S/) {
1441 $errors->{title} = "Please enter a title";
1444 if (exists $data->{template} && $data->{template} =~ /\.\./) {
1445 $errors->{template} = "Please only select templates from the list provided";
1447 if (exists $data->{linkAlias}
1448 && length $data->{linkAlias}) {
1449 unless ($data->{linkAlias} =~ /\A[a-zA-Z0-9-_]+\z/
1450 && $data->{linkAlias} =~ /[A-Za-z]/) {
1451 $errors->{linkAlias} = "Link alias must contain only alphanumerics and contain at least one letter";
1457 my ($self, $data, $articles, $errors) = @_;
1459 $self->_validate_common($data, $articles, $errors);
1460 if (!$errors->{linkAlias} && defined $data->{linkAlias} && length $data->{linkAlias}) {
1461 my $other = $articles->getBy(linkAlias => $data->{linkAlias});
1463 and $errors->{linkAlias} =
1464 "Duplicate link alias - already used by article $other->{id}";
1466 custom_class($self->{cfg})
1467 ->article_validate($data, undef, $self->typename, $errors);
1469 return !keys %$errors;
1473 my ($self, $article, $data, $articles, $errors, $ajax) = @_;
1475 $self->_validate_common($data, $articles, $errors, $article);
1476 custom_class($self->{cfg})
1477 ->article_validate($data, $article, $self->typename, $errors);
1479 if (exists $data->{release}) {
1480 if ($ajax && !dh_parse_sql_date($data->{release})
1481 || !$ajax && !dh_parse_date($data->{release})) {
1482 $errors->{release} = "Invalid release date";
1486 if (!$errors->{linkAlias}
1487 && defined $data->{linkAlias}
1488 && length $data->{linkAlias}
1489 && $data->{linkAlias} ne $article->{linkAlias}) {
1490 my $other = $articles->getBy(linkAlias => $data->{linkAlias});
1491 $other && $other->{id} != $article->{id}
1492 and $errors->{linkAlias} = "Duplicate link alias - already used by article $other->{id}";
1495 return !keys %$errors;
1498 sub validate_parent {
1503 my ($self, $req, $data, $articles) = @_;
1505 custom_class($self->{cfg})
1506 ->article_fill_new($data, $self->typename);
1512 my ($self, $article) = @_;
1514 # check the config for the article and any of its ancestors
1515 my $work_article = $article;
1516 my $path = $self->{cfg}->entry('article uris', $work_article->{id});
1518 last if $work_article->{parentid} == -1;
1519 $work_article = $work_article->parent;
1520 $path = $self->{cfg}->entry('article uris', $work_article->{id});
1522 return $path if $path;
1524 $self->default_link_path($article);
1527 sub default_link_path {
1528 my ($self, $article) = @_;
1530 $self->{cfg}->entry('uri', 'articles', '/a');
1534 my ($self, $article) = @_;
1539 my $title = $article->title;
1540 if ($article->is_dynamic) {
1541 return "/cgi-bin/page.pl?page=$article->{id}&title=".escape_uri($title);
1544 my $article_uri = $self->link_path($article);
1545 my $link = "$article_uri/$article->{id}.html";
1546 my $link_titles = $self->{cfg}->entryBool('basic', 'link_titles', 0);
1548 (my $extra = $title) =~ tr/a-z0-9/_/sc;
1549 $link .= "/" . $extra . "_html";
1556 my ($self, $table_object) = @_;
1558 return $table_object->rowClass->columns;
1562 my ($self, $req, $article, $articles) = @_;
1564 $req->check_csrf("admin_add_article")
1565 or return $self->csrf_error($req, undef, "admin_add_article", "Add Article");
1567 my $cgi = $req->cgi;
1569 my $table_object = $self->table_object($articles);
1570 my @columns = $self->save_columns($table_object);
1571 $self->save_thumbnail($cgi, undef, \%data);
1572 for my $name (@columns) {
1573 $data{$name} = $cgi->param($name)
1574 if defined $cgi->param($name);
1576 $data{flags} = join '', sort $cgi->param('flags');
1580 if (!defined $data{parentid} || $data{parentid} eq '') {
1581 $errors{parentid} = "Please select a parent";
1583 elsif ($data{parentid} !~ /^(?:-1|\d+)$/) {
1584 $errors{parentid} = "Invalid parent selection (template bug)";
1586 $self->validate(\%data, $articles, \%errors);
1588 my $save_tags = $cgi->param("_save_tags");
1591 @tags = grep /\S/, $cgi->param("tags");
1593 for my $tag (@tags) {
1594 BSE::TB::Tags->valid_name($tag, \$error)
1598 $errors{tags} = "msg:bse/admin/edit/badtag/$error";
1603 if ($req->is_ajax) {
1604 return $req->json_content
1608 error_code => "FIELD",
1609 message => $req->message(\%errors),
1613 return $self->add_form($req, $article, $articles, $msg, \%errors);
1620 if ($data{parentid} > 0) {
1621 $parent = $articles->getByPkey($data{parentid}) or die;
1622 if ($req->user_can('edit_add_child', $parent)) {
1623 for my $name (@columns) {
1624 if (exists $data{$name} &&
1625 !$req->user_can("edit_add_field_$name", $parent)) {
1626 delete $data{$name};
1631 $parent_msg = "You cannot add a child to that article";
1632 $parent_code = "ACCESS";
1636 if ($req->user_can('edit_add_child')) {
1637 for my $name (@columns) {
1638 if (exists $data{$name} &&
1639 !$req->user_can("edit_add_field_$name")) {
1640 delete $data{$name};
1645 $parent_msg = "You cannot create a top-level article";
1646 $parent_code = "ACCESS";
1650 $self->validate_parent(\%data, $articles, $parent, \$parent_msg)
1651 or $parent_code = "PARENT";
1654 if ($req->is_ajax) {
1655 return $req->json_content
1658 message => $parent_msg,
1659 error_code => $parent_code,
1664 return $self->add_form($req, $article, $articles, $parent_msg);
1668 my $level = $parent ? $parent->{level}+1 : 1;
1669 $data{level} = $level;
1670 $data{displayOrder} = time;
1672 $data{admin} ||= '';
1673 $data{generator} = $self->generator;
1674 $data{lastModified} = now_sqldatetime();
1675 $data{listed} = 1 unless defined $data{listed};
1678 $data{pageTitle} = '' unless defined $data{pageTitle};
1679 my $user = $req->getuser;
1680 $data{createdBy} = $user ? $user->{logon} : '';
1681 $data{lastModifiedBy} = $user ? $user->{logon} : '';
1682 $data{created} = now_sqldatetime();
1685 $data{force_dynamic} = 0;
1686 $data{cached_dynamic} = 0;
1687 $data{inherit_siteuser_rights} = 1;
1690 $data{metaDescription} = '' unless defined $data{metaDescription};
1691 $data{metaKeywords} = '' unless defined $data{metaKeywords};
1694 $self->fill_new_data($req, \%data, $articles);
1695 for my $col (qw(titleImage imagePos template keyword menu titleAlias linkAlias body author summary)) {
1697 or $data{$col} = $self->default_value($req, \%data, $col);
1700 for my $col (qw/force_dynamic inherit_siteuser_rights/) {
1701 if ($req->user_can("edit_add_field_$col", $parent)
1702 && $cgi->param("save_$col")) {
1703 $data{$col} = $cgi->param($col) ? 1 : 0;
1706 $data{$col} = $self->default_value($req, \%data, $col);
1710 unless ($req->is_ajax) {
1711 for my $col (qw(release expire)) {
1712 $data{$col} = sql_date($data{$col});
1716 # these columns are handled a little differently
1717 for my $col (qw(release expire threshold summaryLength )) {
1719 or $data{$col} = $self->default_value($req, \%data, $col);
1722 my @cols = $table_object->rowClass->columns;
1724 $article = $table_object->add(@data{@cols});
1726 $self->save_new_more($req, $article, \%data);
1728 # we now have an id - generate the links
1730 $article->update_dynamic($self->{cfg});
1731 my $cgi_uri = $self->{cfg}->entry('uri', 'cgi', '/cgi-bin');
1732 $article->setAdmin("$cgi_uri/admin/admin.pl?id=$article->{id}");
1733 $article->setLink($self->make_link($article));
1736 my ($after_id) = $cgi->param("_after");
1737 if (defined $after_id) {
1738 Articles->reorder_child($article->{parentid}, $article->{id}, $after_id);
1739 # reload, the displayOrder probably changed
1740 $article = $articles->getByPkey($article->{id});
1745 $article->set_tags(\@tags, \$error);
1748 use Util 'generate_article';
1749 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
1751 if ($req->is_ajax) {
1752 return $req->json_content
1756 article => $self->_article_data($req, $article),
1761 my $r = $cgi->param('r');
1763 $r .= ($r =~ /\?/) ? '&' : '?';
1764 $r .= "id=$article->{id}";
1767 $r = admin_base_url($req->cfg) . $article->{admin};
1769 return BSE::Template->get_refresh($r, $self->{cfg});
1773 my ($self, $req, $article, $data) = @_;
1775 if (exists $data->{body}) {
1776 $data->{body} =~ s/\x0D\x0A/\n/g;
1777 $data->{body} =~ tr/\r/\n/;
1779 for my $col (Article->columns) {
1780 next if $col =~ /^custom/;
1781 $article->{$col} = $data->{$col}
1782 if exists $data->{$col} && $col ne 'id' && $col ne 'parentid';
1784 custom_class($self->{cfg})
1785 ->article_fill_old($article, $data, $self->typename);
1791 my ($self, $req, $article) = @_;
1793 my $article_data = $article->data_only;
1794 $article_data->{link} = $article->link($req->cfg);
1795 $article_data->{images} =
1797 map $self->_image_data($req->cfg, $_), $article->images
1799 $article_data->{files} =
1801 map $_->data_only, $article->files,
1803 $article_data->{tags} =
1805 $article->tags, # just the names
1808 return $article_data;
1812 my ($self, $req, $article, $data) = @_;
1813 # nothing to do here
1817 my ($self, $req, $article, $data) = @_;
1818 # nothing to do here
1829 ACCESS - user doesn't have access to this article.
1833 LASTMOD - lastModified value doesn't match that in the article
1837 PARENT - invalid parentid specified
1844 my ($self, $req, $article, $articles) = @_;
1846 $req->check_csrf("admin_save_article")
1847 or return $self->csrf_error($req, $article, "admin_save_article", "Save Article");
1849 $req->user_can(edit_save => $article)
1850 or return $self->_service_error
1851 ($req, $article, $articles, "You don't have access to save this article",
1854 my $old_dynamic = $article->is_dynamic;
1855 my $cgi = $req->cgi;
1857 my $table_object = $self->table_object($articles);
1858 my @save_cols = $self->save_columns($table_object);
1859 for my $name (@save_cols) {
1860 $data{$name} = $cgi->param($name)
1861 if defined($cgi->param($name)) and $name ne 'id' && $name ne 'parentid'
1862 && $req->user_can("edit_field_edit_$name", $article);
1866 # checks editor lastModified against record lastModified
1867 if ($self->{cfg}->entry('editor', 'check_modified')) {
1868 if ($article->{lastModified} ne $cgi->param('lastModified')) {
1869 my $whoModified = '';
1870 my $timeModified = ampm_time($article->{lastModified});
1871 if ($article->{lastModifiedBy}) {
1872 $whoModified = "by '$article->{lastModifiedBy}'";
1874 print STDERR "non-matching lastModified, article not saved\n";
1875 my $msg = "Article changes not saved, this article was modified $whoModified at $timeModified since this editor was loaded";
1876 return $self->_service_error($req, $article, $articles, $msg, {}, "LASTMOD");
1881 # possibly this needs tighter error checking
1882 $data{flags} = join '', sort $cgi->param('flags')
1883 if $req->user_can("edit_field_edit_flags", $article);
1885 if (exists $article->{template} &&
1886 $article->{template} =~ m|\.\.|) {
1887 $errors{template} = "Please only select templates from the list provided";
1890 my $save_tags = $cgi->param("_save_tags");
1893 @tags = grep /\S/, $cgi->param("tags");
1895 for my $tag (@tags) {
1896 BSE::TB::Tags->valid_name($tag, \$error)
1900 $errors{tags} = "msg:bse/admin/edit/badtag/$error";
1903 $self->validate_old($article, \%data, $articles, \%errors, scalar $req->is_ajax)
1904 or return $self->_service_error($req, $article, $articles, undef, \%errors, "FIELD");
1905 $self->save_thumbnail($cgi, $article, \%data)
1906 if $req->user_can('edit_field_edit_thumbImage', $article);
1907 if (exists $data{flags} && $data{flags} =~ /D/) {
1908 $article->remove_html;
1910 $self->fill_old_data($req, $article, \%data);
1913 my $newparentid = $cgi->param('parentid');
1915 && $req->user_can('edit_field_edit_parentid', $article)
1916 && $newparentid != $article->{parentid}) {
1919 if ($newparentid == -1) {
1920 require BSE::Edit::Site;
1921 $newparent = BSE::TB::Site->new;
1922 $parent_editor = BSE::Edit::Site->new(cfg => $req->cfg);
1925 $newparent = $articles->getByPkey($newparentid);
1926 ($parent_editor, $newparent) = $self->article_class($newparent, $articles, $req->cfg);
1930 if ($self->can_reparent_to($article, $newparent, $parent_editor, $articles, \$msg)
1931 && $self->reparent($article, $newparentid, $articles, \$msg)) {
1932 # nothing to do here
1935 return $self->_service_error($req, $article, $articles, $msg, {}, "PARENT");
1939 return $self->_service_error($req, $article, $articles, "No such parent article", {}, "PARENT");
1943 $article->{listed} = $cgi->param('listed')
1944 if defined $cgi->param('listed') &&
1945 $req->user_can('edit_field_edit_listed', $article);
1947 if ($req->user_can('edit_field_edit_release', $article)) {
1948 my $release = $cgi->param("release");
1949 if (defined $release && $release =~ /\S/) {
1950 if ($req->is_ajax) {
1951 $article->{release} = $release;
1954 $article->{release} = sql_date($release)
1959 $article->{expire} = sql_date($cgi->param('expire')) || $Constants::D_99
1960 if defined $cgi->param('expire') &&
1961 $req->user_can('edit_field_edit_expire', $article);
1962 $article->{lastModified} = now_sqldatetime();
1963 for my $col (qw/force_dynamic inherit_siteuser_rights/) {
1964 if ($req->user_can("edit_field_edit_$col", $article)
1965 && $cgi->param("save_$col")) {
1966 $article->{$col} = $cgi->param($col) ? 1 : 0;
1971 my $user = $req->getuser;
1972 $article->{lastModifiedBy} = $user ? $user->{logon} : '';
1975 my @save_group_ids = $cgi->param('save_group_id');
1976 if ($req->user_can('edit_field_edit_group_id')
1977 && @save_group_ids) {
1978 require BSE::TB::SiteUserGroups;
1979 my %groups = map { $_->{id} => $_ }
1980 BSE::TB::SiteUserGroups->admin_and_query_groups($self->{cfg});
1981 my %set = map { $_ => 1 } $cgi->param('group_id');
1982 my %current = map { $_ => 1 } $article->group_ids;
1984 for my $group_id (@save_group_ids) {
1985 $groups{$group_id} or next;
1986 if ($current{$group_id} && !$set{$group_id}) {
1987 $article->remove_group_id($group_id);
1989 elsif (!$current{$group_id} && $set{$group_id}) {
1990 $article->add_group_id($group_id);
1995 my $old_link = $article->{link};
1996 # this need to go last
1997 $article->update_dynamic($self->{cfg});
1998 if (!$self->{cfg}->entry('protect link', $article->{id})) {
1999 my $article_uri = $self->make_link($article);
2000 $article->setLink($article_uri);
2007 $article->set_tags(\@tags, \$error);
2012 @extra_regen = $self->update_child_dynamic($article, $articles, $req);
2014 if ($article->is_dynamic || $old_dynamic) {
2015 if (!$old_dynamic and $old_link) {
2016 unlink $article->link_to_filename($self->{cfg}, $old_link);
2018 elsif (!$article->is_dynamic) {
2019 unlink $article->cached_filename($self->{cfg});
2023 my ($after_id) = $cgi->param("_after");
2024 if (defined $after_id) {
2025 Articles->reorder_child($article->{parentid}, $article->{id}, $after_id);
2026 # reload, the displayOrder probably changed
2027 $article = $articles->getByPkey($article->{id});
2030 use Util 'generate_article';
2031 if ($Constants::AUTO_GENERATE) {
2032 generate_article($articles, $article);
2033 for my $regen_id (@extra_regen) {
2034 my $regen = $articles->getByPkey($regen_id);
2035 Util::generate_low($articles, $regen, $self->{cfg});
2039 $self->save_more($req, $article, \%data);
2041 if ($req->is_ajax) {
2042 return $req->json_content
2046 article => $self->_article_data($req, $article),
2051 return $self->refresh($article, $cgi, undef, 'Article saved');
2054 sub can_reparent_to {
2055 my ($self, $article, $newparent, $parent_editor, $articles, $rmsg) = @_;
2057 my @child_types = $parent_editor->child_types;
2058 if (!grep $_ eq ref $self, @child_types) {
2059 my ($child_type) = (ref $self) =~ /(\w+)$/;
2060 my ($parent_type) = (ref $parent_editor) =~ /(\w+)$/;
2062 $$rmsg = "A $child_type cannot be a child of a $parent_type";
2066 # the article cannot become a child of itself or one of it's
2068 if ($article->{id} == $newparent->id
2069 || $self->is_descendant($article->id, $newparent->id, $articles)) {
2070 $$rmsg = "Cannot become a child of itself or of a descendant";
2074 my $shopid = $self->{cfg}->entryErr('articles', 'shop');
2075 if ($self->shop_article) { # if this article belongs in the shop
2076 unless ($newparent->id == $shopid
2077 || $self->is_descendant($shopid, $newparent->{id}, $articles)) {
2078 $$rmsg = "This article belongs in the shop";
2083 if ($newparent->id == $shopid
2084 || $self->is_descendant($shopid, $newparent->id, $articles)) {
2085 $$rmsg = "This article doesn't belong in the shop";
2093 sub shop_article { 0 }
2095 sub update_child_dynamic {
2096 my ($self, $article, $articles, $req) = @_;
2098 my $cfg = $req->cfg;
2099 my @stack = $article->children;
2102 my $workart = pop @stack;
2103 my $old_dynamic = $workart->is_dynamic; # before update
2104 my $old_link = $workart->{link};
2106 ($editor, $workart) = $self->article_class($workart, $articles, $cfg);
2108 $workart->update_dynamic($cfg);
2109 if ($old_dynamic != $workart->is_dynamic) {
2111 if ($article->{link} && !$cfg->entry('protect link', $workart->{id})) {
2112 my $uri = $editor->make_link($workart);
2113 $workart->setLink($uri);
2115 !$old_dynamic && $old_link
2116 and unlink $workart->link_to_filename($cfg, $old_link);
2117 $workart->is_dynamic
2118 or unlink $workart->cached_filename($cfg);
2121 # save dynamic cache change and link if that changed
2124 push @stack, $workart->children;
2125 push @regen, $workart->{id};
2133 my ($year, $month, $day);
2136 if (($day, $month, $year) = ($str =~ m!(\d+)/(\d+)/(\d+)!)) {
2137 $year += 2000 if $year < 100;
2139 return sprintf("%04d-%02d-%02d", $year, $month, $day);
2145 # Converts 24hr time to 12hr AM/PM time
2148 my ($hour, $minute, $second, $ampm);
2151 if (($hour, $minute, $second) = ($str =~ m!(\d+):(\d+):(\d+)!)) {
2157 $hour = 12 if $hour == 0;
2160 return sprintf("%02d:%02d:%02d $ampm", $hour, $minute, $second);
2167 my ($self, $article, $newparentid, $articles, $rmsg) = @_;
2170 if ($newparentid == -1) {
2174 my $parent = $articles->getByPkey($newparentid);
2176 $$rmsg = "Cannot get new parent article";
2179 $newlevel = $parent->{level} + 1;
2181 # the caller will save this one
2182 $article->{parentid} = $newparentid;
2183 $article->{level} = $newlevel;
2184 $article->{displayOrder} = time;
2186 my @change = ( [ $article->{id}, $newlevel ] );
2188 my $this = shift @change;
2189 my ($art, $level) = @$this;
2191 my @kids = $articles->getBy(parentid=>$art);
2192 push @change, map { [ $_->{id}, $level+1 ] } @kids;
2194 for my $kid (@kids) {
2195 $kid->{level} = $level+1;
2203 # tests if $desc is a descendant of $art
2204 # where both are article ids
2206 my ($self, $art, $desc, $articles) = @_;
2210 my $parent = shift @check;
2211 $parent == $desc and return 1;
2212 my @kids = $articles->getBy(parentid=>$parent);
2213 push @check, map $_->{id}, @kids;
2219 sub save_thumbnail {
2220 my ($self, $cgi, $original, $newdata) = @_;
2222 unless ($original) {
2223 @$newdata{qw/thumbImage thumbWidth thumbHeight/} = ('', 0, 0);
2225 my $imagedir = cfg_image_dir($self->{cfg});
2226 if ($cgi->param('remove_thumb') && $original && $original->{thumbImage}) {
2227 unlink("$imagedir/$original->{thumbImage}");
2228 @$newdata{qw/thumbImage thumbWidth thumbHeight/} = ('', 0, 0);
2230 my $image_name = $cgi->param('thumbnail');
2231 my $image = $cgi->upload('thumbnail');
2232 if ($image_name && -s $image) {
2233 # where to put it...
2235 $image_name =~ /([\w.-]+)$/ and $name = $1;
2236 my $filename = time . "_" . $name;
2240 $filename = time . '_' . $counter . '_' . $name
2241 until sysopen( OUTPUT, "$imagedir/$filename",
2242 O_WRONLY| O_CREAT| O_EXCL)
2243 || ++$counter > 100;
2245 fileno(OUTPUT) or die "Could not open image file: $!";
2251 # read the image in from the browser and output it to our
2253 print STDERR "\$image ",ref $image,"\n";
2255 print OUTPUT $buffer while sysread $image, $buffer, 1024;
2258 or die "Could not close image output file: $!";
2262 if ($original && $original->{thumbImage}) {
2263 #unlink("$imagedir/$original->{thumbImage}");
2265 @$newdata{qw/thumbWidth thumbHeight/} = imgsize("$imagedir/$filename");
2266 $newdata->{thumbImage} = $filename;
2271 my ($self, $article) = @_;
2273 my $shopid = $self->{cfg}->entryErr('articles', 'shop');
2274 if ($article && $article->{id} && $article->{id} == $shopid) {
2275 return ( 'BSE::Edit::Catalog' );
2277 return ( 'BSE::Edit::Article' );
2282 Add a step child to an article.
2290 id - parent article id (required)
2294 stepkid - child article id (required)
2298 _after - id of the allkid of id to position the stepkid after
2303 Returns a FIELD error for an invalid stepkid.
2305 Returns an ACCESS error for insufficient access.
2307 Return an ADD error for a general add failure.
2313 relationship: { childId: I<childid>, parentId: I<parentid> }
2321 my ($self, $req, $article, $articles) = @_;
2323 $req->check_csrf("admin_add_stepkid")
2324 or return $self->csrf_error($req, $article, "admin_add_stepkid", "Add Stepkid");
2326 $req->user_can(edit_stepkid_add => $article)
2327 or return $self->_service_error($req, $article, $articles,
2328 "You don't have access to add step children to this article", {}, "ACCESS");
2330 my $cgi = $req->cgi;
2331 require BSE::Admin::StepParents;
2334 my $childId = $cgi->param('stepkid');
2336 or $errors{stepkid} = "No stepkid supplied to add_stepkid";
2337 unless ($errors{stepkid}) {
2339 or $errors{stepkid} = "Invalid stepkid supplied to add_stepkid";
2342 unless ($errors{stepkid}) {
2343 $child = $articles->getByPkey($childId)
2344 or $errors{stepkid} = "Article $childId not found";
2347 and return $self->_service_error
2348 ($req, $article, $articles, $errors{stepkid}, \%errors, "FIELD");
2350 $req->user_can(edit_stepparent_add => $child)
2351 or return $self->_service_error($req, $article, $articles, "You don't have access to add a stepparent to that article", {}, "ACCESS");
2356 my $release = $cgi->param('release');
2357 dh_parse_date($release) or $release = undef;
2358 my $expire = $cgi->param('expire');
2359 dh_parse_date($expire) or $expire = undef;
2362 BSE::Admin::StepParents->add($article, $child, $release, $expire);
2365 return $self->_service_error($req, $article, $articles, $@, {}, "ADD");
2368 my $after_id = $cgi->param("_after");
2369 if (defined $after_id) {
2370 Articles->reorder_child($article->id, $child->id, $after_id);
2373 use Util 'generate_article';
2374 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
2376 if ($req->is_ajax) {
2377 return $req->json_content
2380 relationship => $new_entry->data_only,
2384 $self->refresh($article, $cgi, 'step', 'Stepchild added');
2390 Remove a stepkid relationship.
2398 id - parent article id (required)
2402 stepkid - child article id (required)
2406 Returns a FIELD error for an invalid stepkid.
2408 Returns an ACCESS error for insufficient access.
2410 Return a DELETE error for a general delete failure.
2415 my ($self, $req, $article, $articles) = @_;
2417 $req->check_csrf("admin_remove_stepkid")
2418 or return $self->csrf_error($req, $article, "admin_del_stepkid", "Delete Stepkid");
2419 $req->user_can(edit_stepkid_delete => $article)
2420 or return $self->_service_error($req, $article, $articles,
2421 "You don't have access to delete stepchildren from this article", {}, "ACCESS");
2423 my $cgi = $req->cgi;
2426 my $childId = $cgi->param('stepkid');
2428 or $errors{stepkid} = "No stepkid supplied to add_stepkid";
2429 unless ($errors{stepkid}) {
2431 or $errors{stepkid} = "Invalid stepkid supplied to add_stepkid";
2434 unless ($errors{stepkid}) {
2435 $child = $articles->getByPkey($childId)
2436 or $errors{stepkid} = "Article $childId not found";
2439 and return $self->_service_error
2440 ($req, $article, $articles, $errors{stepkid}, \%errors, "FIELD");
2442 $req->user_can(edit_stepparent_delete => $child)
2443 or return _service_error($req, $article, $article, "You cannot remove stepparents from that article", {}, "ACCESS");
2446 require BSE::Admin::StepParents;
2448 BSE::Admin::StepParents->del($article, $child);
2452 return $self->_service_error($req, $article, $articles, $@, {}, "DELETE");
2454 use Util 'generate_article';
2455 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
2457 if ($req->is_ajax) {
2458 return $req->json_content(success => 1);
2461 return $self->refresh($article, $cgi, 'step', 'Stepchild deleted');
2466 my ($self, $req, $article, $articles) = @_;
2468 $req->check_csrf("admin_save_stepkids")
2469 or return $self->csrf_error($req, $article, "admin_save_stepkids", "Save Stepkids");
2471 $req->user_can(edit_stepkid_save => $article)
2472 or return $self->edit_form($req, $article, $articles,
2473 "No access to save stepkid data for this article");
2475 my $cgi = $req->cgi;
2476 require 'BSE/Admin/StepParents.pm';
2477 my @stepcats = OtherParents->getBy(parentId=>$article->{id});
2478 my %stepcats = map { $_->{parentId}, $_ } @stepcats;
2479 my %datedefs = ( release => '2000-01-01', expire=>'2999-12-31' );
2480 for my $stepcat (@stepcats) {
2481 $req->user_can(edit_stepparent_save => $stepcat->{childId})
2483 for my $name (qw/release expire/) {
2484 my $date = $cgi->param($name.'_'.$stepcat->{childId});
2485 if (defined $date) {
2487 $date = $datedefs{$name};
2489 elsif (dh_parse_date($date)) {
2490 use BSE::Util::SQL qw/date_to_sql/;
2491 $date = date_to_sql($date);
2494 return $self->refresh($article, $cgi, '', "Invalid date '$date'");
2496 $stepcat->{$name} = $date;
2502 $@ and return $self->refresh($article, $cgi, '', $@);
2504 use Util 'generate_article';
2505 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
2507 return $self->refresh($article, $cgi, 'step', 'Stepchild information saved');
2512 Moves a stepkid from one parent to another, and sets the order within
2513 that new stepparent.
2521 id - id of the step kid to move (required)
2525 parentid - id of the parent in the stepkid relationship (required)
2529 newparentid - the new parent for the stepkid relationship (optional)
2533 _after - id of the allkid under newparentid (or parentid if
2534 newparentid isn't supplied) to place the stepkid after (0 to place at
2545 NOPARENTID - parentid parameter not supplied
2549 BADPARENTID - non-numeric parentid supplied
2553 NOTFOUND - no stepkid relationship from parentid was found
2557 BADNEWPARENT - newparentid is non-numeric
2561 UNKNOWNNEWPARENT - no article id newparentid found
2565 NEWPARENTDUP - there's already a stepkid relationship between
2573 my ($self, $req, $article, $articles) = @_;
2575 # first, identify the stepkid link
2576 my $cgi = $req->cgi;
2577 require OtherParents;
2578 my $parentid = $cgi->param("parentid");
2580 or return $self->_service_error($req, $article, $articles, "Missing parentid", {}, "NOPARENTID");
2581 $parentid =~ /^\d+$/
2582 or return $self->_service_error($req, $article, $articles, "Invalid parentid", {}, "BADPARENTID");
2584 my ($step) = OtherParents->getBy(parentId => $parentid, childId => $article->id)
2585 or return $self->_service_error($req, $article, $articles, "Unknown relationship", {}, "NOTFOUND");
2587 my $newparentid = $cgi->param("newparentid");
2589 $newparentid =~ /^\d+$/
2590 or return $self->_service_error($req, $article, $articles, "Bad new parent id", {}, "BADNEWPARENT");
2591 my $new_parent = Articles->getByPkey($newparentid)
2592 or return $self->_service_error($req, $article, $articles, "Unknown new parent id", {}, "UNKNOWNNEWPARENT");
2594 OtherParents->getBy(parentId=>$newparentid, childId=>$article->id)
2595 and return $self->_service_error($req, $article, $articles, "New parent is duplicate", {}, "NEWPARENTDUP");
2597 $step->{parentId} = $newparentid;
2601 my $after_id = $cgi->param("_after");
2602 if (defined $after_id) {
2603 Articles->reorder_child($step->{parentId}, $article->id, $after_id);
2606 if ($req->is_ajax) {
2607 return $req->json_content
2610 relationshop => $step->data_only,
2614 return $self->refresh($article, $cgi, 'step', "Stepchild moved");
2618 sub add_stepparent {
2619 my ($self, $req, $article, $articles) = @_;
2621 $req->check_csrf("admin_add_stepparent")
2622 or return $self->csrf_error($req, $article, "admin_add_stepparent", "Add Stepparent");
2624 $req->user_can(edit_stepparent_add => $article)
2625 or return $self->edit_form($req, $article, $articles,
2626 "You don't have access to add stepparents to this article");
2628 my $cgi = $req->cgi;
2629 require 'BSE/Admin/StepParents.pm';
2631 my $step_parent_id = $cgi->param('stepparent');
2632 defined($step_parent_id)
2633 or die "No stepparent supplied to add_stepparent";
2634 int($step_parent_id) eq $step_parent_id
2635 or die "Invalid stepcat supplied to add_stepcat";
2636 my $step_parent = $articles->getByPkey($step_parent_id)
2637 or die "Parent $step_parent_id not found\n";
2639 $req->user_can(edit_stepkid_add => $step_parent)
2640 or die "You don't have access to add a stepkid to that article\n";
2642 my $release = $cgi->param('release');
2644 or $release = "01/01/2000";
2645 $release eq '' or dh_parse_date($release)
2646 or die "Invalid release date";
2647 my $expire = $cgi->param('expire');
2649 or $expire = '31/12/2999';
2650 $expire eq '' or dh_parse_date($expire)
2651 or die "Invalid expire data";
2654 BSE::Admin::StepParents->add($step_parent, $article, $release, $expire);
2656 $@ and return $self->refresh($article, $cgi, 'step', $@);
2658 use Util 'generate_article';
2659 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
2661 return $self->refresh($article, $cgi, 'stepparents', 'Stepparent added');
2664 sub del_stepparent {
2665 my ($self, $req, $article, $articles) = @_;
2667 $req->check_csrf("admin_remove_stepparent")
2668 or return $self->csrf_error($req, $article, "admin_del_stepparent", "Delete Stepparent");
2670 $req->user_can(edit_stepparent_delete => $article)
2671 or return $self->edit_form($req, $article, $articles,
2672 "You cannot remove stepparents from that article");
2674 my $cgi = $req->cgi;
2675 require 'BSE/Admin/StepParents.pm';
2676 my $step_parent_id = $cgi->param('stepparent');
2677 defined($step_parent_id)
2678 or return $self->refresh($article, $cgi, 'stepparents',
2679 "No stepparent supplied to add_stepcat");
2680 int($step_parent_id) eq $step_parent_id
2681 or return $self->refresh($article, $cgi, 'stepparents',
2682 "Invalid stepparent supplied to add_stepparent");
2683 my $step_parent = $articles->getByPkey($step_parent_id)
2684 or return $self->refresh($article, $cgi, 'stepparent',
2685 "Stepparent $step_parent_id not found");
2687 $req->user_can(edit_stepkid_delete => $step_parent)
2688 or die "You don't have access to remove the stepkid from that article\n";
2691 BSE::Admin::StepParents->del($step_parent, $article);
2693 $@ and return $self->refresh($article, $cgi, 'stepparents', $@);
2695 use Util 'generate_article';
2696 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
2698 return $self->refresh($article, $cgi, 'stepparents', 'Stepparent deleted');
2701 sub save_stepparents {
2702 my ($self, $req, $article, $articles) = @_;
2704 $req->check_csrf("admin_save_stepparents")
2705 or return $self->csrf_error($req, $article, "admin_save_stepparents", "Save Stepparents");
2706 $req->user_can(edit_stepparent_save => $article)
2707 or return $self->edit_form($req, $article, $articles,
2708 "No access to save stepparent data for this artice");
2710 my $cgi = $req->cgi;
2712 require 'BSE/Admin/StepParents.pm';
2713 my @stepparents = OtherParents->getBy(childId=>$article->{id});
2714 my %stepparents = map { $_->{parentId}, $_ } @stepparents;
2715 my %datedefs = ( release => '2000-01-01', expire=>'2999-12-31' );
2716 for my $stepparent (@stepparents) {
2717 $req->user_can(edit_stepkid_save => $stepparent->{parentId})
2719 for my $name (qw/release expire/) {
2720 my $date = $cgi->param($name.'_'.$stepparent->{parentId});
2721 if (defined $date) {
2723 $date = $datedefs{$name};
2725 elsif (dh_parse_date($date)) {
2726 use BSE::Util::SQL qw/date_to_sql/;
2727 $date = date_to_sql($date);
2730 return $self->refresh($article, $cgi, "Invalid date '$date'");
2732 $stepparent->{$name} = $date;
2736 $stepparent->save();
2738 $@ and return $self->refresh($article, $cgi, '', $@);
2741 use Util 'generate_article';
2742 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
2744 return $self->refresh($article, $cgi, 'stepparents',
2745 'Stepparent information saved');
2749 my ($self, $article, $cgi, $name, $message, $extras) = @_;
2751 my $url = $cgi->param('r');
2753 if ($url !~ /[?&](m|message)=/ && $message) {
2754 # add in messages if none in the provided refresh
2755 my @msgs = ref $message ? @$message : $message;
2756 my $sep = $url =~ /\?/ ? "&" : "?";
2757 for my $msg (@msgs) {
2758 $url .= $sep . "m=" . CGI::escape($msg);
2763 my $urlbase = admin_base_url($self->{cfg});
2764 $url = "$urlbase$ENV{SCRIPT_NAME}?id=$article->{id}";
2766 my @msgs = ref $message ? @$message : $message;
2767 for my $msg (@msgs) {
2768 $url .= "&m=" . CGI::escape($msg);
2771 if ($cgi->param('_t')) {
2772 $url .= "&_t=".CGI::escape($cgi->param('_t'));
2774 $url .= $extras if defined $extras;
2775 my $cgiextras = $cgi->param('e');
2776 $url .= "#$name" if $name;
2783 my ($self, $article, $cgi, $name, $message, $extras) = @_;
2785 my $url = $self->refresh_url($article, $cgi, $name, $message, $extras);
2787 return BSE::Template->get_refresh($url, $self->{cfg});
2791 my ($self, $req, $article, $articles, $msg, $errors) = @_;
2794 %acts = $self->low_edit_tags(\%acts, $req, $article, $articles, $msg, $errors);
2795 my $template = 'admin/article_img';
2797 return $req->dyn_response($template, \%acts);
2800 sub save_image_changes {
2801 my ($self, $req, $article, $articles) = @_;
2803 $req->check_csrf("admin_save_images")
2804 or return $self->csrf_error($req, $article, "admin_save_images", "Save Images");
2806 $req->user_can(edit_images_save => $article)
2807 or return $self->edit_form($req, $article, $articles,
2808 "You don't have access to save image information for this article");
2810 my $image_dir = cfg_image_dir($req->cfg);
2812 my $cgi = $req->cgi;
2813 my $image_pos = $cgi->param('imagePos');
2815 && $image_pos =~ /^(?:tl|tr|bl|br)$/
2816 && $image_pos ne $article->{imagePos}) {
2817 $article->{imagePos} = $image_pos;
2820 my @images = $self->get_images($article);
2823 return $self->refresh($article, $cgi, undef, 'No images to save information for');
2830 for my $image (@images) {
2831 my $id = $image->{id};
2833 my $alt = $cgi->param("alt$id");
2834 if ($alt ne $image->{alt}) {
2835 $changes{$id}{alt} = $alt;
2838 my $url = $cgi->param("url$id");
2839 if (defined $url && $url ne $image->{url}) {
2840 $changes{$id}{url} = $url;
2843 my $name = $cgi->param("name$id");
2844 if (defined $name && $name ne $image->{name}) {
2846 if ($article->{id} > 0) {
2847 $changes{$id}{name} = '';
2850 $errors{"name$id"} = "Identifiers are required for global images";
2853 elsif ($name =~ /^[a-z_]\w*$/i) {
2855 if ($self->validate_image_name($name, \$msg)) {
2856 # check for duplicates after the loop
2857 push @{$names{lc $name}}, $image->{id}
2859 $changes{$id}{name} = $name;
2862 $errors{"name$id"} = $msg;
2866 $errors{"name$id"} = 'Image name must be empty or alphanumeric and unique to the article';
2870 push @{$names{lc $image->{name}}}, $image->{id}
2871 if length $image->{name};
2874 my $filename = $cgi->param("image$id");
2875 if (defined $filename && length $filename) {
2876 my $in_fh = $cgi->upload("image$id");
2878 # work out where to put it
2879 require DevHelp::FileUpload;
2881 my ($image_name, $out_fh) = DevHelp::FileUpload->make_img_filename
2882 ($image_dir, $filename . '', \$msg);
2886 while ($data = <$in_fh>) {
2887 print $out_fh $data;
2891 my $full_filename = "$image_dir/$image_name";
2892 require Image::Size;
2893 my ($width, $height, $type) = Image::Size::imgsize($full_filename);
2897 image => $image->{image},
2898 storage => $image->{storage}
2900 push @new_images, $image_name;
2902 $changes{$id}{image} = $image_name;
2903 $changes{$id}{storage} = 'local';
2904 $changes{$id}{src} = "/images/$image_name";
2905 $changes{$id}{width} = $width;
2906 $changes{$id}{height} = $height;
2907 $changes{$id}{ftype} = $self->_image_ftype($type);
2910 $errors{"image$id"} = $type;
2914 $errors{"image$id"} = $msg;
2919 $errors{"image$id"} = "No image file received";
2923 # look for duplicate names
2924 for my $name (keys %names) {
2925 if (@{$names{$name}} > 1) {
2926 for my $id (@{$names{$name}}) {
2927 $errors{"name$id"} = 'Image name must be unique to the article';
2932 # remove files that won't be stored because validation failed
2933 unlink map "$image_dir/$_", @new_images;
2935 return $self->edit_form($req, $article, $articles, undef,
2939 my $mgr = $self->_image_manager($req->cfg);
2940 $req->flash('Image information saved');
2941 my $changes_found = 0;
2942 my $auto_store = $cgi->param('auto_storage');
2943 for my $image (@images) {
2944 my $id = $image->{id};
2946 if ($changes{$id}) {
2947 my $changes = $changes{$id};
2950 for my $field (keys %$changes) {
2951 $image->{$field} = $changes->{$field};
2956 my $old_storage = $image->{storage};
2957 my $new_storage = $auto_store ? '' : $cgi->param("storage$id");
2958 defined $new_storage or $new_storage = $image->{storage};
2959 $new_storage = $mgr->select_store($image->{image}, $new_storage, $image);
2960 if ($new_storage ne $old_storage) {
2962 $image->{src} = $mgr->store($image->{image}, $new_storage, $image);
2963 $image->{storage} = $new_storage;
2967 if ($old_storage ne 'local') {
2968 $mgr->unstore($image->{image}, $old_storage);
2973 # delete any image files that were replaced
2974 for my $old_image (values %old_images) {
2975 my ($image, $storage) = @$old_image{qw/image storage/};
2976 if ($storage ne 'local') {
2977 $mgr->unstore($image->{image}, $storage);
2979 unlink "$image_dir/$image";
2982 if ($changes_found) {
2983 use Util 'generate_article';
2984 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
2987 return $self->refresh($article, $cgi);
2990 =item _service_error
2992 This function is called on various errors.
2994 If a _service parameter was supplied, returns text like:
3000 Field-Error: I<field-name1> - I<message1>
3002 Field-Error: I<field-name2> - I<message2>
3006 If the request is detected as an ajax request or a _ parameter is
3007 supplied, return JSON like:
3009 { error: I<message> }
3011 Otherwise display the normal edit page with the error.
3015 sub _service_error {
3016 my ($self, $req, $article, $articles, $msg, $error, $code) = @_;
3020 $article = $self->_dummy_article($req, $articles, \$mymsg);
3023 map $_ => '', Article->columns
3027 if ($req->cgi->param('_service')) {
3029 $body .= "Result: failure\n";
3031 for my $field (keys %$error) {
3032 my $text = $error->{$field};
3034 $body .= "Field-Error: $field - $text\n";
3036 my $text = join ('/', values %$error);
3038 $body .= "Error: $text\n";
3041 $body .= "Error: $msg\n";
3044 $body .= "Error: $error\n";
3048 type => 'text/plain',
3052 elsif ((() = $req->cgi->param('_')) ||
3053 (exists $ENV{HTTP_X_REQUESTED_WITH}
3054 && $ENV{HTTP_X_REQUESTED_WITH} =~ /XMLHttpRequest/)) {
3061 $msg and $result->{message} = $msg;
3062 $code and $result->{error_code} = $code;
3063 my $json_result = $req->json_content($result);
3065 if (!exists $ENV{HTTP_X_REQUESTED_WITH}
3066 || $ENV{HTTP_X_REQUESTED_WITH} !~ /XMLHttpRequest/) {
3067 $json_result->{type} = "text/plain";
3070 return $json_result;
3073 return $self->edit_form($req, $article, $articles, $msg, $error);
3077 sub _service_success {
3078 my ($self, $results) = @_;
3080 my $body = "Result: success\n";
3081 for my $field (keys %$results) {
3082 $body .= "$field: $results->{$field}\n";
3086 type => 'text/plain',
3092 my ($self, $type) = @_;
3094 if ($type eq 'CWS' || $type eq 'SWF') {
3102 my ($self, $cfg, $article, $image, %opts) = @_;
3104 my $errors = $opts{errors}
3105 or die "No errors parameter";
3107 my $imageref = $opts{name};
3108 if (defined $imageref && $imageref ne '') {
3109 if ($imageref =~ /^[a-z_]\w+$/i) {
3110 # make sure it's unique
3111 my @images = $self->get_images($article);
3112 for my $img (@images) {
3113 if (defined $img->{name} && lc $img->{name} eq lc $imageref) {
3114 $errors->{name} = 'Image name must be unique to the article';
3120 $errors->{name} = 'Image name must be empty or alphanumeric beginning with an alpha character';
3126 unless ($errors->{name}) {
3128 $self->validate_image_name($imageref, \$workmsg)
3129 or $errors->{name} = $workmsg;
3134 $errors->{image} = 'Image file is empty';
3138 $errors->{image} = 'Please enter an image filename';
3143 my $imagename = $opts{filename} || $image;
3144 $imagename .= ''; # force it into a string
3146 $imagename =~ tr/ //d;
3147 $imagename =~ /([\w.-]+)$/ and $basename = $1;
3149 # for the sysopen() constants
3152 my $imagedir = cfg_image_dir($cfg);
3154 require DevHelp::FileUpload;
3156 my ($filename, $fh) =
3157 DevHelp::FileUpload->make_img_filename($imagedir, $basename, \$msg);
3158 unless ($filename) {
3159 $errors->{image} = $msg;
3162 print STDERR "Gen filename '$filename'\n";
3163 # for OSs with special text line endings
3170 # read the image in from the browser and output it to our output filehandle
3171 print $fh $buffer while read $image, $buffer, 1024;
3175 or die "Could not close image file $filename: $!";
3180 my($width,$height, $type) = imgsize("$imagedir/$filename");
3182 my $alt = $opts{alt};
3183 defined $alt or $alt = '';
3184 my $url = $opts{url};
3185 defined $url or $url = '';
3188 articleId => $article->{id},
3197 src => '/images/' . $filename,
3198 ftype => $self->_image_ftype($type),
3200 require BSE::TB::Images;
3201 my @cols = BSE::TB::Image->columns;
3203 my $imageobj = BSE::TB::Images->add(@image{@cols});
3205 my $storage = $opts{storage};
3206 defined $storage or $storage = 'local';
3207 my $image_manager = $self->_image_manager($cfg);
3208 local $SIG{__DIE__};
3211 $storage = $image_manager->select_store($filename, $storage, $imageobj);
3212 $src = $image_manager->store($filename, $storage, $imageobj);
3215 $imageobj->{src} = $src;
3216 $imageobj->{storage} = $storage;
3221 $errors->{flash} = $@;
3228 my ($self, $cfg, $image) = @_;
3230 my $data = $image->data_only;
3231 $data->{src} = $image->image_url($cfg);
3237 my ($self, $req, $article, $articles) = @_;
3239 $req->check_csrf("admin_add_image")
3240 or return $self->csrf_error($req, $article, "admin_add_image", "Add Image");
3241 $req->user_can(edit_images_add => $article)
3242 or return $self->_service_error($req, $article, $articles,
3243 "You don't have access to add new images to this article");
3245 my $cgi = $req->cgi;
3252 scalar($cgi->upload('image')),
3253 name => scalar($cgi->param('name')),
3254 alt => scalar($cgi->param('altIn')),
3255 url => scalar($cgi->param('url')),
3256 storage => scalar($cgi->param('storage')),
3258 filename => scalar($cgi->param("image")),
3262 or return $self->_service_error($req, $article, $articles, undef, \%errors);
3264 # typically a soft failure from the storage
3266 and $req->flash($errors{flash});
3268 use Util 'generate_article';
3269 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
3271 if ($cgi->param('_service')) {
3272 return $self->_service_success
3275 image => $imageobj->{id},
3279 elsif ($cgi->param("_") || $req->is_ajax) {
3280 my $resp = $req->json_content
3283 image => $self->_image_data($req->cfg, $imageobj),
3286 # the browser handles this directly, tell it that it's text
3287 $resp->{type} = "text/plain";
3292 return $self->refresh($article, $cgi, undef, 'New image added');
3296 sub _image_manager {
3299 require BSE::StorageMgr::Images;
3301 return BSE::StorageMgr::Images->new(cfg => $self->cfg);
3306 my ($self, $req, $article, $articles, $imageid) = @_;
3308 $req->check_csrf("admin_remove_image")
3309 or return $self->csrf_error($req, $article, "admin_remove_image", "Remove Image");
3311 $req->user_can(edit_images_delete => $article)
3312 or return $self->_service_error($req, $article, $articles,
3313 "You don't have access to delete images from this article", {}, "ACCESS");
3317 my @images = $self->get_images($article);
3318 my ($image) = grep $_->{id} == $imageid, @images;
3320 if ($req->want_json_response) {
3321 return $self->_service_error($req, $article, $articles, "No such image", {}, "NOTFOUND");
3324 return $self->show_images($req, $article, $articles, "No such image");
3328 if ($image->{storage} ne 'local') {
3329 my $mgr = $self->_image_manager($req->cfg);
3330 $mgr->unstore($image->{image}, $image->{storage});
3333 my $imagedir = cfg_image_dir($req->cfg);
3334 unlink "$imagedir$image->{image}";
3337 use Util 'generate_article';
3338 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
3340 if ($req->want_json_response) {
3341 return $req->json_content
3347 return $self->refresh($article, $req->cgi, undef, 'Image removed');
3351 my ($self, $req, $article, $articles) = @_;
3353 $req->check_csrf("admin_move_image")
3354 or return $self->csrf_error($req, $article, "admin_move_image", "Move Image");
3355 $req->user_can(edit_images_reorder => $article)
3356 or return $self->edit_form($req, $article, $articles,
3357 "You don't have access to reorder images in this article");
3359 my $imageid = $req->cgi->param('imageid');
3360 my @images = $self->get_images($article);
3361 my ($imgindex) = grep $images[$_]{id} == $imageid, 0..$#images
3362 or return $self->edit_form($req, $article, $articles, "No such image");
3364 or return $self->edit_form($req, $article, $articles, "Image is already at the top");
3365 my ($to, $from) = @images[$imgindex-1, $imgindex];
3366 ($to->{displayOrder}, $from->{displayOrder}) =
3367 ($from->{displayOrder}, $to->{displayOrder});
3371 use Util 'generate_article';
3372 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
3374 return $self->refresh($article, $req->cgi, undef, 'Image moved');
3378 my ($self, $req, $article, $articles) = @_;
3380 $req->check_csrf("admin_move_image")
3381 or return $self->csrf_error($req, $article, "admin_move_image", "Move Image");
3382 $req->user_can(edit_images_reorder => $article)
3383 or return $self->edit_form($req, $article, $articles,
3384 "You don't have access to reorder images in this article");
3386 my $imageid = $req->cgi->param('imageid');
3387 my @images = $self->get_images($article);
3388 my ($imgindex) = grep $images[$_]{id} == $imageid, 0..$#images
3389 or return $self->edit_form($req, $article, $articles, "No such image");
3390 $imgindex < $#images
3391 or return $self->edit_form($req, $article, $articles, "Image is already at the end");
3392 my ($to, $from) = @images[$imgindex+1, $imgindex];
3393 ($to->{displayOrder}, $from->{displayOrder}) =
3394 ($from->{displayOrder}, $to->{displayOrder});
3398 use Util 'generate_article';
3399 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
3401 return $self->refresh($article, $req->cgi, undef, 'Image moved');
3405 my ($self, $req, $article) = @_;
3407 my $cgi = $req->cgi;
3408 my $cfg = $req->cfg;
3409 my $im_id = $cgi->param('im');
3411 if (defined $im_id && $im_id =~ /^\d+$/) {
3412 ($image) = grep $_->{id} == $im_id, $self->get_images($article);
3414 my $thumb_obj = $self->_get_thumbs_class();
3416 if ($image && $thumb_obj) {
3417 my $geometry_id = $cgi->param('g');
3418 defined $geometry_id or $geometry_id = 'editor';
3419 my $geometry = $cfg->entry('thumb geometries', $geometry_id, 'scale(200x200)');
3420 my $imagedir = $cfg->entry('paths', 'images', $Constants::IMAGEDIR);
3423 ($data, $type) = $thumb_obj->thumb_data
3425 filename => "$imagedir/$image->{image}",
3426 geometry => $geometry,
3431 type => 'text/plain',
3432 content => 'Error: '.$error
3436 if ($type && $data) {
3443 "Content-Length: ".length($data),
3444 "Cache-Control: max-age=3600",
3449 # grab the nothumb image
3450 my $uri = $cfg->entry('editor', 'default_thumbnail', '/images/admin/nothumb.png');
3451 my $filebase = $cfg->content_base_path;
3452 if (open IMG, "<$filebase/$uri") {
3454 my $data = do { local $/; <IMG> };
3456 my $type = $uri =~ /\.(\w+)$/ ? $1 : 'png';
3459 type => "image/$type",
3461 headers => [ "Content-Length: ".length($data) ],
3468 content => "<html><body>Cannot make thumb or default image</body></html>",
3474 sub req_edit_image {
3475 my ($self, $req, $article, $articles, $errors) = @_;
3477 my $cgi = $req->cgi;
3479 my $id = $cgi->param('image_id');
3481 my ($image) = grep $_->{id} == $id, $self->get_images($article)
3482 or return $self->edit_form($req, $article, $articles,
3484 $req->user_can(edit_images_save => $article)
3485 or return $self->edit_form($req, $article, $articles,
3486 "You don't have access to save image information for this article");
3491 $self->low_edit_tags(\%acts, $req, $article, $articles, undef,
3493 eimage => [ \&tag_hash, $image ],
3494 error_img => [ \&tag_error_img, $req->cfg, $errors ],
3497 return $req->response('admin/image_edit', \%acts);
3502 Save changes to an image.
3518 alt, url, name - text fields to update
3522 image - replacement image data (if any)
3528 sub req_save_image {
3529 my ($self, $req, $article, $articles) = @_;
3531 $req->check_csrf("admin_save_image")
3532 or return $self->csrf_error($req, $article, "admin_save_image", "Save Image");
3533 my $cgi = $req->cgi;
3535 my $id = $cgi->param('image_id');
3537 my @images = $self->get_images($article);
3538 my ($image) = grep $_->{id} == $id, @images
3539 or return $self->_service_error($req, $article, $articles, "No such image",
3541 $req->user_can(edit_images_save => $article)
3542 or return $self->_service_error($req, $article, $articles,
3543 "You don't have access to save image information for this article", {}, "ACCESS");
3545 my $image_dir = cfg_image_dir($req->cfg);
3547 my $old_storage = $image->{storage};
3551 my $alt = $cgi->param('alt');
3552 defined $alt and $image->{alt} = $alt;
3553 my $url = $cgi->param('url');
3554 defined $url and $image->{url} = $url;
3555 my @other_images = grep $_->{id} != $id, @images;
3556 my $name = $cgi->param('name');
3557 if (defined $name) {
3559 if ($name !~ /^[a-z_]\w*$/i) {
3560 $errors{name} = 'Image name must be empty or alphanumeric and unique to the article';
3562 elsif (grep $name eq $_->{name}, @other_images) {
3563 $errors{name} = 'Image name must be unique to the article';
3566 $image->{name} = $name;
3570 if ($article->{id} == -1) {
3571 $errors{name} = "Identifiers are required for global images";
3574 $image->{name} = '';
3578 my $filename = $cgi->param('image');
3579 if (defined $filename && length $filename) {
3580 my $in_fh = $cgi->upload('image');
3582 require DevHelp::FileUpload;
3584 my ($image_name, $out_fh) = DevHelp::FileUpload->make_img_filename
3585 ($image_dir, $filename . '', \$msg);
3590 while ($data = <$in_fh>) {
3591 print $out_fh $data;
3596 my $full_filename = "$image_dir/$image_name";
3597 require Image::Size;
3598 my ($width, $height, $type) = Image::Size::imgsize($full_filename);
3600 $delete_file = $image->{image};
3601 $image->{image} = $image_name;
3602 $image->{width} = $width;
3603 $image->{height} = $height;
3604 $image->{storage} = 'local'; # not on the remote store yet
3605 $image->{src} = '/images/' . $image_name;
3606 $image->{ftype} = $self->_image_ftype($type);
3609 $errors{image} = $type;
3613 $errors{image} = $msg;
3617 $errors{image} = "No image file received";
3621 if ($req->want_json_response) {
3622 return $self->_service_error($req, $article, $articles, undef,
3626 return $self->req_edit_image($req, $article, $articles, \%errors);
3630 my $new_storage = $cgi->param('storage');
3631 defined $new_storage or $new_storage = $image->{storage};
3633 my $mgr = $self->_image_manager($req->cfg);
3635 if ($old_storage ne 'local') {
3636 $mgr->unstore($delete_file, $old_storage);
3638 unlink "$image_dir/$delete_file";
3640 $req->flash("Image saved");
3643 $mgr->select_store($image->{image}, $new_storage);
3644 if ($image->{storage} ne $new_storage) {
3645 # handles both new images (which sets storage to local) and changing
3646 # the storage for old images
3647 my $old_storage = $image->{storage};
3648 my $src = $mgr->store($image->{image}, $new_storage, $image);
3649 $image->{src} = $src;
3650 $image->{storage} = $new_storage;
3654 $@ and $req->flash("There was a problem adding it to the new storage: $@");
3655 if ($image->{storage} ne $old_storage && $old_storage ne 'local') {
3657 $mgr->unstore($image->{image}, $old_storage);
3659 $@ and $req->flash("There was a problem removing if from the old storage: $@");
3662 if ($req->want_json_response) {
3663 return $req->json_content
3666 image => $self->_image_data($req->cfg, $image),
3670 return $self->refresh($article, $cgi);
3673 =item a_order_images
3675 Change the order of images for an article (or global images).
3683 id - id of the article to change the image order for (-1 for global
3688 order - comma-separated list of image ids in the new order.
3694 sub req_order_images {
3695 my ($self, $req, $article, $articles) = @_;
3698 or return $self->_service_error($req, $article, $articles, "The function only permitted from Ajax", {}, "AJAXONLY");
3700 my $order = $req->cgi->param("order");
3702 or return $self->_service_error($req, $article, $articles, "order not supplied", {}, "NOORDER");
3703 $order =~ /^\d+(,\d+)*$/
3704 or return $self->_service_error($req, $article, $articles, "order not supplied", {}, "BADORDER");
3706 my @order = split /,/, $order;
3708 my @images = $article->set_image_order(\@order);
3710 return $req->json_content
3715 map $self->_image_data($req->cfg, $_), @images
3721 my ($self, $articles, $article) = @_;
3727 my ($self, $articles) = @_;
3732 sub _refresh_filelist {
3733 my ($self, $req, $article, $msg) = @_;
3735 return $self->refresh($article, $req->cgi, undef, $msg);
3739 my ($self, $req, $article, $articles, $msg, $errors) = @_;
3742 %acts = $self->low_edit_tags(\%acts, $req, $article, $articles, $msg, $errors);
3743 my $template = 'admin/filelist';
3745 return BSE::Template->get_response($template, $req->cfg, \%acts);
3752 maxlength => MAX_FILE_DISPLAYNAME_LENGTH,
3753 description => 'Filename'
3757 rules => 'dh_one_line',
3759 description => 'Description',
3763 description => 'Identifier',
3768 description => "Category",
3774 my ($self, $req, $article, $articles) = @_;
3776 $req->check_csrf("admin_add_file")
3777 or return $self->csrf_error($req, $article, "admin_add_file", "Add File");
3778 $req->user_can(edit_files_add => $article)
3779 or return $self->edit_form($req, $article, $articles,
3780 "You don't have access to add files to this article");
3783 my $cgi = $req->cgi;
3784 require BSE::TB::ArticleFiles;
3785 my @cols = BSE::TB::ArticleFile->columns;
3787 for my $col (@cols) {
3788 if (defined $cgi->param($col)) {
3789 $file{$col} = $cgi->param($col);
3795 $req->validate(errors => \%errors,
3796 fields => \%file_fields,
3797 section => $article->{id} == -1 ? 'Global File Validation' : 'Article File Validation');
3800 my $file = $cgi->upload('file');
3801 my $filename = $cgi->param("file");
3803 $errors{file} = 'Please enter a filename';
3805 if ($file && -z $file) {
3806 $errors{file} = 'File is empty';
3809 $file{forSale} = 0 + exists $file{forSale};
3810 $file{articleId} = $article->{id};
3811 $file{download} = 0 + exists $file{download};
3812 $file{requireUser} = 0 + exists $file{requireUser};
3813 $file{hide_from_list} = 0 + exists $file{hide_from_list};
3814 $file{category} ||= '';
3816 defined $file{name} or $file{name} = '';
3817 if ($article->{id} == -1 && $file{name} eq '') {
3818 $errors{name} = 'Identifier is required for global files';
3820 if (!$errors{name} && length $file{name} && $file{name} !~/^\w+$/) {
3821 $errors{name} = "Identifier must be a single word";
3823 if (!$errors{name} && length $file{name}) {
3824 my @files = $self->get_files($article);
3825 if (grep lc $_->{name} eq lc $file{name}, @files) {
3826 $errors{name} = "Duplicate file identifier $file{name}";
3831 and return $self->edit_form($req, $article, $articles, undef, \%errors);
3834 my $workfile = $filename;
3835 $workfile =~ s![^\w.:/\\-]+!_!g;
3836 $workfile =~ tr/_/_/s;
3837 $workfile =~ /([ \w.-]+)$/ and $basename = $1;
3838 $basename =~ tr/ /_/;
3839 $file{displayName} = $basename;
3840 $file{file} = $file;
3842 local $SIG{__DIE__};
3845 $article->add_file($self->cfg, %file);
3849 or return $self->edit_form($req, $article, $articles, $@);
3851 $req->flash("New file added");
3853 my $storage = $cgi->param("storage") || "";
3857 $article->apply_storage($self->cfg, $fileobj, $storage, \$msg);
3859 $msg and $req->flash($msg);
3862 and $req->flash($@);
3864 use Util 'generate_article';
3865 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
3867 $self->_refresh_filelist($req, $article);
3871 my ($self, $req, $article, $articles) = @_;
3873 $req->check_csrf("admin_move_file")
3874 or return $self->csrf_error($req, $article, "admin_move_file", "Move File");
3876 $req->user_can('edit_files_reorder', $article)
3877 or return $self->edit_form($req, $article, $articles,
3878 "You don't have access to reorder files in this article");
3880 my $cgi = $req->cgi;
3881 my $id1 = $cgi->param('file1');
3882 my $id2 = $cgi->param('file2');
3885 my @files = $self->get_files($article);
3887 my ($file1) = grep $_->{id} == $id1, @files;
3888 my ($file2) = grep $_->{id} == $id2, @files;
3890 if ($file1 && $file2) {
3891 ($file1->{displayOrder}, $file2->{displayOrder})
3892 = ($file2->{displayOrder}, $file1->{displayOrder});
3898 use Util 'generate_article';
3899 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
3901 $self->refresh($article, $req->cgi, undef, 'File moved');
3905 my ($self, $req, $article, $articles) = @_;
3907 $req->check_csrf("admin_remove_file")
3908 or return $self->csrf_error($req, $article, "admin_remove_file", "Delete File");
3909 $req->user_can('edit_files_delete', $article)
3910 or return $self->edit_form($req, $article, $articles,
3911 "You don't have access to delete files from this article");
3913 my $cgi = $req->cgi;
3914 my $fileid = $cgi->param('file');
3916 my @files = $self->get_files($article);
3918 my ($file) = grep $_->{id} == $fileid, @files;
3921 if ($file->{storage} ne 'local') {
3922 my $mgr = $self->_file_manager($self->cfg);
3923 $mgr->unstore($file->{filename}, $file->{storage});
3926 $file->remove($req->cfg);
3930 use Util 'generate_article';
3931 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
3933 $self->_refresh_filelist($req, $article, 'File deleted');
3937 my ($self, $req, $article, $articles) = @_;
3939 $req->check_csrf("admin_save_files")
3940 or return $self->csrf_error($req, $article, "admin_save_files", "Save Files");
3942 $req->user_can('edit_files_save', $article)
3943 or return $self->edit_form($req, $article, $articles,
3944 "You don't have access to save file information for this article");
3945 my @files = $self->get_files($article);
3947 my $download_path = BSE::TB::ArticleFiles->download_path($self->{cfg});
3949 my $cgi = $req->cgi;
3955 my @content_changed;
3956 for my $file (@files) {
3957 my $id = $file->{id};
3958 my $desc = $cgi->param("description_$id");
3959 defined $desc and $file->{description} = $desc;
3960 my $type = $cgi->param("contentType_$id");
3961 if (defined $type and $type ne $file->{contentType}) {
3962 ++$store_anyway{$id};
3963 $file->{contentType} = $type;
3965 my $notes = $cgi->param("notes_$id");
3966 defined $notes and $file->{notes} = $notes;
3967 my $category = $cgi->param("category_$id");
3968 defined $category and $file->{category} = $category;
3969 my $name = $cgi->param("name_$id");
3970 if (defined $name) {
3971 $file->{name} = $name;
3973 if ($name =~ /^\w+$/) {
3974 push @{$names{$name}}, $id;
3977 $errors{"name_$id"} = "Invalid file identifier $name";
3980 elsif ($article->{id} == -1) {
3981 $errors{"name_$id"} = "Identifier is required for global files";
3985 push @{$names{$file->{name}}}, $id
3986 if length $file->{name};
3988 if ($cgi->param('save_file_flags')) {
3989 my $download = 0 + defined $cgi->param("download_$id");
3990 if ($download != $file->{download}) {
3991 ++$store_anyway{$file->{id}};
3992 $file->{download} = $download;
3994 $file->{forSale} = 0 + defined $cgi->param("forSale_$id");
3995 $file->{requireUser} = 0 + defined $cgi->param("requireUser_$id");
3996 $file->{hide_from_list} = 0 + defined $cgi->param("hide_from_list_$id");
3999 my $filex = $cgi->param("file_$id");
4000 my $in_fh = $cgi->upload("file_$id");
4001 if (defined $filex && length $filex) {
4002 if (length $filex <= MAX_FILE_DISPLAYNAME_LENGTH) {
4005 require DevHelp::FileUpload;
4007 my ($file_name, $out_fh) = DevHelp::FileUpload->make_img_filename
4008 ($download_path, $filex . '', \$msg);
4013 while ($data = <$in_fh>) {
4014 print $out_fh $data;
4018 my $display_name = $filex;
4019 $display_name =~ s!.*[\\:/]!!;
4020 $display_name =~ s/[^\w._-]+/_/g;
4021 my $full_name = "$download_path/$file_name";
4022 push @old_files, [ $file->{filename}, $file->{storage} ];
4023 push @new_files, $file_name;
4025 $file->{filename} = $file_name;
4026 $file->{storage} = 'local';
4027 $file->{sizeInBytes} = -s $full_name;
4028 $file->{whenUploaded} = now_datetime();
4029 $file->{displayName} = $display_name;
4030 push @content_changed, $file;
4033 $errors{"file_$id"} = $msg;
4037 $errors{"file_$id"} = "File is empty";
4041 $errors{"file_$id"} = "No file data received";
4045 $errors{"file_$id"} = "Filename too long";
4049 for my $name (keys %names) {
4050 if (@{$names{$name}} > 1) {
4051 for my $id (@{$names{$name}}) {
4052 $errors{"name_$id"} = 'File identifier must be unique to the article';
4057 # remove the uploaded replacements
4058 unlink map "$download_path/$_", @new_files;
4060 return $self->edit_form($req, $article, $articles, undef, \%errors);
4062 $req->flash('File information saved');
4063 my $mgr = $self->_file_manager($self->cfg);
4064 for my $file (@files) {
4067 my $storage = $cgi->param("storage_$file->{id}");
4068 defined $storage or $storage = 'local';
4070 $storage = $article->select_filestore($mgr, $file, $storage, \$msg);
4071 $msg and $req->flash($msg);
4072 if ($storage ne $file->{storage} || $store_anyway{$file->{id}}) {
4073 my $old_storage = $file->{storage};
4075 $file->{src} = $mgr->store($file->{filename}, $storage, $file);
4076 $file->{storage} = $storage;
4079 if ($old_storage ne $storage) {
4080 $mgr->unstore($file->{filename}, $old_storage);
4084 and $req->flash("Could not move $file->{displayName} to $storage: $@");
4088 # remove the replaced files
4089 for my $file (@old_files) {
4090 my ($filename, $storage) = @$file;
4093 $mgr->unstore($filename, $storage);
4096 and $req->flash("Error removing $filename from $storage: $@");
4098 unlink "$download_path/$filename";
4101 # update file type metadatas
4102 for my $file (@content_changed) {
4103 $file->set_handler($self->{cfg});
4107 use Util 'generate_article';
4108 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
4110 $self->_refresh_filelist($req, $article);
4114 my ($self, $req, $article, $articles, $errors) = @_;
4116 my $cgi = $req->cgi;
4118 my $id = $cgi->param('file_id');
4120 my ($file) = grep $_->{id} == $id, $self->get_files($article)
4121 or return $self->edit_form($req, $article, $articles,
4123 $req->user_can(edit_files_save => $article)
4124 or return $self->edit_form($req, $article, $articles,
4125 "You don't have access to save file information for this article");
4127 my $name = $cgi->param('name');
4128 $name && $name =~ /^\w+$/
4129 or return $self->edit_form($req, $article, $articles,
4130 "Missing or invalid metadata name");
4132 my $meta = $file->meta_by_name($name)
4133 or return $self->edit_form($req, $article, $articles,
4134 "Metadata $name not defined for this file");
4138 type => $meta->content_type,
4139 content => $meta->value,
4143 sub tag_old_checked {
4144 my ($errors, $cgi, $file, $key) = @_;
4146 return $errors ? $cgi->param($key) : $file->{$key};
4149 sub tag_filemeta_value {
4150 my ($file, $args, $acts, $funcname, $templater) = @_;
4152 my ($name) = DevHelp::Tags->get_parms($args, $acts, $templater)
4153 or return "* no meta name supplied *";
4155 my $meta = $file->meta_by_name($name)
4158 $meta->content_type eq "text/plain"
4159 or return "* $name has type " . $meta->content_type . " and cannot be displayed inline *";
4161 return escape_html($meta->value);
4164 sub tag_ifFilemeta_set {
4165 my ($file, $args, $acts, $funcname, $templater) = @_;
4167 my ($name) = DevHelp::Tags->get_parms($args, $acts, $templater)
4168 or return "* no meta name supplied *";
4170 my $meta = $file->meta_by_name($name)
4176 sub tag_filemeta_source {
4177 my ($file, $args, $acts, $funcname, $templater) = @_;
4179 my ($name) = DevHelp::Tags->get_parms($args, $acts, $templater)
4180 or return "* no meta name supplied *";
4182 return "$ENV{SCRIPT_NAME}?a_filemeta=1&id=$file->{articleId}&file_id=$file->{id}&name=$name";
4185 sub tag_filemeta_select {
4186 my ($cgi, $allmeta, $rcurr_meta, $file, $args, $acts, $funcname, $templater) = @_;
4189 if ($args =~ /\S/) {
4190 my ($name) = DevHelp::Tags->get_parms($args, $acts, $templater)
4191 or return "* cannot parse *";
4192 ($meta) = grep $_->name eq $name, @$allmeta
4193 or return "* cannot find meta field *";
4195 elsif ($$rcurr_meta) {
4196 $meta = $$rcurr_meta;
4199 return "* use in filemeta iterator or supply a name *";
4202 $meta->type eq "enum"
4203 or return "* can only use filemeta_select on enum metafields *";
4206 my @values = $meta->values;
4207 @labels{@values} = $meta->labels;
4209 my $field_name = "meta_" . $meta->name;
4210 my ($def) = $cgi->param($field_name);
4211 unless (defined $def) {
4212 my $value = $file->meta_by_name($meta->name);
4213 if ($value && $value->is_text) {
4214 $def = $value->value;
4217 defined $def or $def = $values[0];
4221 -name => $field_name,
4222 -values => \@values,
4223 -labels => \%labels,
4228 sub tag_filemeta_select_label {
4229 my ($allmeta, $rcurr_meta, $file, $args, $acts, $funcname, $templater) = @_;
4232 if ($args =~ /\S/) {
4233 my ($name) = DevHelp::Tags->get_parms($args, $acts, $templater)
4234 or return "* cannot parse *";
4235 ($meta) = grep $_->name eq $name, @$allmeta
4236 or return "* cannot find meta field *";
4238 elsif ($$rcurr_meta) {
4239 $meta = $$rcurr_meta;
4242 return "* use in filemeta iterator or supply a name *";
4245 $meta->type eq "enum"
4246 or return "* can only use filemeta_select_label on enum metafields *";
4249 my @values = $meta->values;
4250 @labels{@values} = $meta->labels;
4252 my $field_name = "meta_" . $meta->name;
4253 my $value = $file->meta_by_name($meta->name);
4255 if ($value->is_text) {
4256 if (exists $labels{$value->value}) {
4257 return escape_html($labels{$value->value});
4260 return escape_html($value->value);
4264 return "* cannot display type " . $value->content_type . " inline *";
4268 return "* " . $meta->name . " not set *";
4273 my ($self, $req, $article, $articles, $errors) = @_;
4275 my $cgi = $req->cgi;
4277 my $id = $cgi->param('file_id');
4279 my ($file) = grep $_->{id} == $id, $self->get_files($article)
4280 or return $self->edit_form($req, $article, $articles,
4282 $req->user_can(edit_files_save => $article)
4283 or return $self->edit_form($req, $article, $articles,
4284 "You don't have access to save file information for this article");
4286 my @metafields = $file->metafields($self->cfg);
4288 my $it = BSE::Util::Iterate->new;
4293 $self->low_edit_tags(\%acts, $req, $article, $articles, undef,
4295 efile => [ \&tag_hash, $file ],
4296 error_img => [ \&tag_error_img, $req->cfg, $errors ],
4298 [ \&tag_old_checked, $errors, $cgi, $file ],
4301 plural => "filemetas",
4302 single => "filemeta",
4303 data => \@metafields,
4304 store => \$current_meta,
4307 [ \&tag_filemeta_value, $file ],
4309 [ \&tag_ifFilemeta_set, $file ],
4311 [ \&tag_filemeta_source, $file ],
4313 [ \&tag_filemeta_select, $cgi, \@metafields, \$current_meta, $file ],
4314 filemeta_select_label =>
4315 [ \&tag_filemeta_select_label, \@metafields, \$current_meta, $file ],
4318 return $req->response('admin/file_edit', \%acts);
4322 my ($self, $req, $article, $articles) = @_;
4324 $req->check_csrf("admin_save_file")
4325 or return $self->csrf_error($req, $article, "admin_save_file", "Save File");
4327 my $cgi = $req->cgi;
4329 my @files = $self->get_files($article);
4331 my $id = $cgi->param('file_id');
4333 my ($file) = grep $_->{id} == $id, @files
4334 or return $self->edit_form($req, $article, $articles,
4336 $req->user_can(edit_files_save => $article)
4337 or return $self->edit_form($req, $article, $articles,
4338 "You don't have access to save file information for this article");
4339 my @other_files = grep $_->{id} != $id, @files;
4341 my $download_path = BSE::TB::ArticleFiles->download_path($self->{cfg});
4345 $req->validate(errors => \%errors,
4346 fields => \%file_fields,
4347 section => $article->{id} == -1 ? 'Global File Validation' : 'Article File Validation');
4349 my $store_anyway = 0;
4350 my $desc = $cgi->param("description");
4351 defined $desc and $file->{description} = $desc;
4352 my $type = $cgi->param("contentType");
4353 if (defined $type && $file->{contentType} ne $type) {
4355 $file->{contentType} = $type;
4357 my $notes = $cgi->param("notes");
4358 defined $notes and $file->{notes} = $notes;
4359 my $name = $cgi->param("name");
4360 if (defined $name) {
4361 $file->{name} = $name;
4363 if ($name =~ /^\w+$/) {
4364 if (grep lc $name eq lc $_->{name}, @other_files) {
4365 $errors{name} = 'File identifier must be unique to the article';
4369 $errors{name} = "Invalid file identifier $name";
4372 if (!$errors{name} && $article->{id} == -1) {
4374 or $errors{name} = "Identifier is required for global files";
4380 my @metafields = grep !$_->ro, $file->metafields($self->cfg);
4381 my %current_meta = map { $_ => 1 } $file->metanames;
4382 for my $meta (@metafields) {
4383 my $name = $meta->name;
4384 my $cgi_name = "meta_$name";
4385 if ($cgi->param("delete_$cgi_name")) {
4386 for my $metaname ($meta->metanames) {
4387 push @meta_delete, $metaname
4388 if $current_meta{$metaname};
4393 if ($meta->is_text) {
4394 my ($value) = $cgi->param($cgi_name);
4395 if (defined $value &&
4396 ($value =~ /\S/ || $current_meta{$meta->name})) {
4398 if ($meta->validate(value => $value, error => \$error)) {
4406 $errors{$cgi_name} = $error;
4411 my $im = $cgi->param($cgi_name);
4412 my $up = $cgi->upload($cgi_name);
4413 if (defined $im && $up) {
4414 my $data = do { local $/; <$up> };
4415 my ($width, $height, $type) = imgsize(\$data);
4417 if ($width && $height) {
4421 name => $meta->data_name,
4423 content_type => "image/\L$type",
4426 name => $meta->width_name,
4430 name => $meta->height_name,
4436 $errors{$cgi_name} = $type;
4443 if ($cgi->param('save_file_flags')) {
4444 my $download = 0 + defined $cgi->param("download");
4445 if ($download ne $file->{download}) {
4447 $file->{download} = $download;
4449 $file->{forSale} = 0 + defined $cgi->param("forSale");
4450 $file->{requireUser} = 0 + defined $cgi->param("requireUser");
4451 $file->{hide_from_list} = 0 + defined $cgi->param("hide_from_list");
4456 my $filex = $cgi->param("file");
4457 my $in_fh = $cgi->upload("file");
4458 if (defined $filex && length $filex) {
4461 require DevHelp::FileUpload;
4463 my ($file_name, $out_fh) = DevHelp::FileUpload->make_img_filename
4464 ($download_path, $filex . '', \$msg);
4469 while ($data = <$in_fh>) {
4470 print $out_fh $data;
4474 my $display_name = $filex;
4475 $display_name =~ s!.*[\\:/]!!;
4476 $display_name =~ s/[^\w._-]+/_/g;
4477 my $full_name = "$download_path/$file_name";
4478 @old_file = ( $file->{filename}, $file->{storage} );
4479 push @new_files, $file_name;
4481 $file->{filename} = $file_name;
4482 $file->{sizeInBytes} = -s $full_name;
4483 $file->{whenUploaded} = now_sqldatetime();
4484 $file->{displayName} = $display_name;
4485 $file->{storage} = 'local';
4488 $errors{"file"} = $msg;
4492 $errors{"file"} = "File is empty";
4496 $errors{"file"} = "No file data received";
4501 # remove the uploaded replacements
4502 unlink map "$download_path/$_", @new_files;
4504 return $self->req_edit_file($req, $article, $articles, \%errors);
4508 $file->set_handler($self->cfg);
4511 $req->flash('File information saved');
4512 my $mgr = $self->_file_manager($self->cfg);
4514 my $storage = $cgi->param('storage');
4515 defined $storage or $storage = $file->{storage};
4517 $storage = $article->select_filestore($mgr, $file, $storage, \$msg);
4518 $msg and $req->flash($msg);
4519 if ($storage ne $file->{storage} || $store_anyway) {
4520 my $old_storage = $file->{storage};
4522 $file->{src} = $mgr->store($file->{filename}, $storage, $file);
4523 $file->{storage} = $storage;
4526 $mgr->unstore($file->{filename}, $old_storage)
4527 if $old_storage ne $storage;
4530 and $req->flash("Could not move $file->{displayName} to $storage: $@");
4533 for my $meta_delete (@meta_delete, map $_->{name}, @meta) {
4534 $file->delete_meta_by_name($meta_delete);
4536 for my $meta (@meta) {
4537 $file->add_meta(%$meta, appdata => 1);
4540 # remove the replaced files
4541 if (my ($old_name, $old_storage) = @old_file) {
4542 $mgr->unstore($old_name, $old_storage);
4543 unlink "$download_path/$old_name";
4546 use Util 'generate_article';
4547 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
4549 $self->_refresh_filelist($req, $article);
4553 my ($self, $req, $article, $articles, $rmsg, $rcode) = @_;
4555 unless ($req->user_can('edit_delete_article', $article, $rmsg)) {
4556 $$rmsg ||= "Access denied";
4561 if ($articles->children($article->{id})) {
4562 $$rmsg = "This article has children. You must delete the children first (or change their parents)";
4563 $$rcode = "CHILDREN";
4566 if (grep $_ == $article->{id}, @Constants::NO_DELETE) {
4567 $$rmsg = "Sorry, these pages are essential to the site structure - they cannot be deleted";
4568 $$rcode = "ESSENTIAL";
4571 if ($article->{id} == $Constants::SHOPID) {
4572 $$rmsg = "Sorry, these pages are essential to the store - they cannot be deleted - you may want to hide the store instead.";
4588 ACCESS - access denied
4592 CHILDREN - the article has children
4596 ESSENTIAL - the article is marked essential
4600 SHOP - the article is an essential part of the shop (the shop article
4605 JSON success response: { success: 1, article_id: I<id> }
4610 my ($self, $req, $article, $articles) = @_;
4612 $req->check_csrf("admin_remove_article")
4613 or return $self->csrf_error($req, $article, "admin_remove_article", "Remove Article");
4617 unless ($self->can_remove($req, $article, $articles, \$why_not, \$code)) {
4618 return $self->_service_error($req, $article, $articles, $why_not, {}, $code);
4621 my $id = $article->id;
4623 my $parentid = $article->{parentid};
4624 $article->remove($req->cfg);
4626 if ($req->is_ajax) {
4627 return $req->json_content
4634 my $url = $req->cgi->param('r');
4636 my $urlbase = admin_base_url($req->cfg);
4637 $url = "$urlbase$ENV{SCRIPT_NAME}?id=$parentid";
4638 $url .= "&message=Article+deleted";
4640 return BSE::Template->get_refresh($url, $self->{cfg});
4644 my ($self, $req, $article, $articles) = @_;
4646 $req->check_csrf("admin_save_article")
4647 or return $self->csrf_error($req, $article, "admin_save_article", "Unhide article");
4649 if ($req->user_can(edit_field_edit_listed => $article)
4650 && $req->user_can(edit_save => $article)) {
4651 $article->{listed} = 1;
4654 use Util 'generate_article';
4655 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
4657 return $self->refresh($article, $req->cgi, undef, 'Article unhidden');
4661 my ($self, $req, $article, $articles) = @_;
4663 $req->check_csrf("admin_save_article")
4664 or return $self->csrf_error($req, $article, "admin_save_article", "Hide article");
4666 if ($req->user_can(edit_field_edit_listed => $article)
4667 && $req->user_can(edit_save => $article)) {
4668 $article->{listed} = 0;
4671 use Util 'generate_article';
4672 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
4674 my $r = $req->cgi->param('r');
4676 $r = admin_base_url($req->cfg)
4677 . "/cgi-bin/admin/add.pl?id=" . $article->{parentid};
4679 return $self->refresh($article, $req->cgi, undef, 'Article hidden');
4686 expire => $Constants::D_99,
4689 body => '<maximum of 64Kb>',
4691 inherit_siteuser_rights => 1,
4700 my ($self, $req, $article, $col) = @_;
4702 if ($article->{parentid}) {
4703 my $section = "children of $article->{parentid}";
4704 my $value = $req->cfg->entry($section, $col);
4705 if (defined $value) {
4709 my $section = "level $article->{level}";
4710 my $value = $req->cfg->entry($section, $col);
4711 defined($value) and return $value;
4713 $value = $self->type_default_value($req, $col);
4714 defined $value and return $value;
4716 exists $defaults{$col} and return $defaults{$col};
4718 $col eq 'release' and return now_sqldate();
4720 if ($col eq 'threshold') {
4721 my $parent = defined $article->{parentid} && $article->{parentid} != -1
4722 && Articles->getByPkey($article->{parentid});
4724 $parent and return $parent->{threshold};
4729 if ($col eq 'summaryLength') {
4730 my $parent = defined $article->{parentid} && $article->{parentid} != -1
4731 && Articles->getByPkey($article->{parentid});
4733 $parent and return $parent->{summaryLength};
4741 sub type_default_value {
4742 my ($self, $req, $col) = @_;
4744 return $req->cfg->entry('article defaults', $col);
4748 return ( 'article flags' );
4754 my $cfg = $self->{cfg};
4756 my @sections = $self->flag_sections;
4758 my %flags = map $cfg->entriesCS($_), reverse @sections;
4759 my @valid = grep /^\w$/, keys %flags;
4761 return map +{ id => $_, desc => $flags{$_} },
4762 sort { lc($flags{$a}) cmp lc($flags{$b}) }@valid;
4766 my ($self, $article) = @_;
4771 sub validate_image_name {
4772 my ($self, $name, $rmsg) = @_;
4774 1; # no extra validation
4778 my ($self, $req, $article, $articles, @extras) = @_;
4780 my $field_name = $req->cgi->param('field');
4781 unless ($field_name && exists $article->{$field_name}) {
4782 print STDERR "req_ajax_get: missing or invalid field parameter\n";
4784 content => 'Invalid or missing field name',
4786 "Status: 187" # bad request
4791 my $value = $article->{$field_name};
4792 defined $value or $value = '';
4794 my $charset = $req->cfg->entry('html', 'charset', 'iso-8859-1');
4798 Encode::from_to($value, $charset, 'utf8');
4804 type => 'text/plain; charset=utf-8',
4808 sub req_ajax_save_body {
4809 my ($self, $req, $article, $articles, @extras) = @_;
4811 my $cfg = $req->cfg;
4812 my $cgi = $req->cgi;
4814 unless ($req->user_can("edit_save", $article)
4815 && $req->user_can("edit_field_edit_body", $article)) {
4817 content => "Access denied to body",
4819 "Status: 187" # bad request
4825 # ajax always sends in UTF-8
4826 my $body = Encode::decode(utf8 => $cgi->param('body'));
4828 my $charset = $req->cfg->entry('html', 'charset', 'iso-8859-1');
4830 # convert it to our working charset
4831 # any characters that don't convert are replaced by some
4832 # substitution character, not defined by the documentation
4833 $body = Encode::encode($charset, $body);
4835 $article->{body} = $body;
4836 $article->{lastModified} = now_sqldatetime();
4837 my $user = $req->getuser;
4838 $article->{lastModifiedBy} = $user ? $user->{logon} : '';
4842 @extra_regen = $self->update_child_dynamic($article, $articles, $req);
4844 if ($Constants::AUTO_GENERATE) {
4846 Util::generate_article($articles, $article);
4847 for my $regen_id (@extra_regen) {
4848 my $regen = $articles->getByPkey($regen_id);
4849 Util::generate_low($articles, $regen, $self->{cfg});
4853 # we need the formatted body as the result
4854 my $genname = $article->{generator};
4855 eval "use $genname";
4856 $@ and die "Error on use $genname: $@";
4857 my $gen = $genname->new(article => $articles, cfg => $cfg, top => $article);
4859 %acts = $gen->baseActs($articles, \%acts, $article, 0);
4860 my $template = "<:body:>";
4861 my $formatted = BSE::Template->replace($template, $req->cfg, \%acts);
4865 content => $formatted,
4866 type => BSE::Template->html_type($cfg),
4870 sub iter_file_metas {
4871 my ($self, $files, $rfile_index) = @_;
4873 $$rfile_index < 0 || $$rfile_index >= @$files
4876 my $file = $files->[$$rfile_index];
4878 return $file->text_metadata;
4881 my %settable_fields = qw(title keyword author pageTitle);
4885 my ($self, $req, $article, $articles, @extras) = @_;
4887 my $cfg = $req->cfg;
4888 my $cgi = $req->cgi;
4890 my $field = $cgi->param('field');
4892 unless ($field && $settable_fields{$field}) {
4894 content => 'Invalid or missing field name',
4896 "Status: 187" # bad request
4900 unless ($req->user_can("edit_save", $article)
4901 && $req->user_can("edit_field_edit_$field", $article)) {
4903 content => "Access denied to $field",
4905 "Status: 187" # bad request
4911 # ajax always sends in UTF-8
4912 my $value = Encode::decode(utf8 => $cgi->param('value'));
4914 # hack - validate it if it's the title
4915 if ($field eq 'title') {
4916 if ($value !~ /\S/) {
4918 content => 'Invelid or missing field name',
4920 "Status: 187" # bad request
4926 my $charset = $req->cfg->entry('html', 'charset', 'iso-8859-1');
4928 # convert it to our working charset
4929 # any characters that don't convert are replaced by some
4930 # substitution character, not defined by the documentation
4931 $value = Encode::encode($charset, $value);
4933 $article->{$field} = $value;
4934 $article->{lastModified} = now_sqldatetime();
4935 my $user = $req->getuser;
4936 $article->{lastModifiedBy} = $user ? $user->{logon} : '';
4940 @extra_regen = $self->update_child_dynamic($article, $articles, $req);
4942 if ($Constants::AUTO_GENERATE) {
4944 Util::generate_article($articles, $article);
4945 for my $regen_id (@extra_regen) {
4946 my $regen = $articles->getByPkey($regen_id);
4947 Util::generate_low($articles, $regen, $self->{cfg});
4954 type => BSE::Template->html_type($cfg),
4959 my ($self, $req, $article, $name, $description) = @_;
4962 my $msg = $req->csrf_error;
4963 $errors{_csrfp} = $msg;
4965 $article ||= $self->_dummy_article($req, 'Articles', \$mymsg);
4967 require BSE::Edit::Site;
4968 my $site = BSE::Edit::Site->new(cfg=>$req->cfg, db=> BSE::DB->single);
4969 return $site->edit_sections($req, 'Articles', $mymsg);
4971 return $self->_service_error($req, $article, 'Articles', $msg, \%errors);
4976 Returns the csrf token for a given action.
4978 Must only be callable from Ajax requests.
4980 In general Ajax requests won't require a token, but some types of
4981 requests initiated by an Ajax based client might need a token, in
4982 particular: file uploads.
4987 my ($self, $req, $article, $articles) = @_;
4990 or return $self->_service_error($req, $article, $articles,
4991 "Only usable from Ajax", undef, "NOTAJAX");
4993 $ENV{REQUEST_METHOD} eq 'POST'
4994 or return $self->_service_error($req, $article, "Articles",
4995 "POST required for this action", {}, "NOTPOST");
4998 my (@names) = $req->cgi->param("name");
4999 @names or $errors{name} = "Missing parameter 'name'";
5000 unless ($errors{name}) {
5001 for my $name (@names) {
5003 or $errors{name} = "Invalid name: must be an identifier";
5008 and return $self->_service_error($req, $article, $articles,
5009 "Invalid parameter", \%errors, "FIELD");
5011 return $req->json_content
5017 map { $_ => $req->get_csrf_token($_) } @names,
5023 sub _article_kid_summary {
5024 my ($article_id, $depth) = @_;
5026 my @kids = BSE::DB->query(bseArticleKidSummary => $article_id);
5028 for my $kid (@kids) {
5029 $kid->{children} = [ _article_kid_summary($kid->{id}, $depth) ];
5030 $kid->{allkids} = [ Articles->allkid_summary($kid->{id}) ];
5039 Returns a JSON tree of articles.
5041 Requires an article id (-1 to start from the root).
5043 Takes an optional tree depth. 1 only shows immediate children of the
5049 my ($self, $req, $article, $articles) = @_;
5051 my $depth = $req->cgi->param("depth");
5052 defined $depth && $depth =~ /^\d+$/ and $depth >= 1
5053 or $depth = 10000; # something large
5056 or return $self->_service_error($req, $article, $articles, "Only available to Ajax requests", {}, "NOTAJAX");
5058 return $req->json_content
5063 _article_kid_summary($article->id, $depth),
5067 Articles->allkid_summary($article->id)
5074 Returns the article as JSON.
5076 Populates images with images and files with files.
5078 The article data is in the article member of the returned object.
5083 my ($self, $req, $article, $articles) = @_;
5086 or return $self->_service_error($req, $article, $articles, "Only available to Ajax requests", {}, "NOTAJAX");
5088 return $req->json_content
5091 article => $self->_article_data($req, $article),
5095 sub templates_long {
5096 my ($self, $article) = @_;
5098 my @templates = $self->templates($article);
5100 my $cfg = $self->{cfg};
5104 description => $cfg->entry("template descriptions", $_, $_),
5108 sub _populate_config {
5109 my ($self, $req, $article, $articles, $conf) = @_;
5111 my $cfg = $req->cfg;
5112 my %geos = $cfg->entries("thumb geometries");
5114 my @cols = $self->table_object($articles)->rowClass->columns;
5116 for my $col (@cols) {
5117 my $def = $self->default_value($req, $article, $col);
5118 defined $def and $defaults{$col} = $def;
5120 my @templates = $self->templates($article);
5121 $defaults{template} =
5122 $self->default_template($article, $req->cfg, \@templates);
5124 $conf->{templates} = [ $self->templates_long($article) ];
5125 $conf->{thumb_geometries} =
5131 description => $cfg->entry("thumb geometry $_", "description", $_),
5135 $conf->{defaults} = \%defaults;
5136 $conf->{upload_progress} = $req->_tracking_uploads;
5137 my @child_types = $self->child_types($article);
5138 s/^BSE::Edit::// for @child_types;
5139 $conf->{child_types} = \@child_types;
5140 $conf->{flags} = [ $self->flags ];
5145 Returns configuration information as JSON.
5147 Returns an object of the form:
5155 description: "template.tmpl", // or from [template descriptions]
5163 description: "geoid", // or from [thumb geometry id].description
5171 child_types: [ "Article" ],
5174 { id => "A", desc => "description" },
5177 // possibible custom data
5180 To define custom data add entries to the [extra a_config] section,
5181 keys become the keys in the returned structure pointing at hashes
5182 containing that section from the system configuration. Custom keys
5183 may not conflict with system defined keys.
5188 my ($self, $req, $article, $articles) = @_;
5191 or return $self->_service_error($req, $article, $articles, "Only available to Ajax requests", {}, "NOTAJAX");
5194 $self->_populate_config($req, $article, $articles, \%conf);
5197 my $cfg = $req->cfg;
5198 my %custom = $cfg->entries("extra a_config");
5199 for my $key (keys %custom) {
5200 exists $conf{$key} and next;
5202 my $section = $custom{$key};
5203 $section =~ s/\{(level|generator|parentid|template)\}/$article->{$1}/g;
5205 $section eq "db" and die;
5207 $conf{$key} = { $cfg->entries($section) };
5210 return $req->json_content
5222 Tony Cook <tony@develop-help.com>