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 DevHelp::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;
18 BSE::Edit::Article - editing functionality for BSE articles
22 Provides the base article editing functionality.
24 This is badly organized and documented.
33 my ($self, $req) = @_;
36 # AJAX/Prototype request
37 return $req->json_content
41 message => "Access forbidden: user not logged on",
43 error_code => "LOGON",
47 elsif ($req->cgi->param('_service')) {
50 content => 'Access Forbidden: login timed out',
52 "Status: 403", # forbidden
57 BSE::Template->get_refresh($req->url('logon'), $req->cfg);
61 sub article_dispatch {
62 my ($self, $req, $article, $articles) = @_;
64 BSE::Permissions->check_logon($req)
65 or return $self->not_logged_on($req);
69 my %actions = $self->article_actions;
70 for my $check (keys %actions) {
71 if ($cgi->param($check) || $cgi->param("$check.x")) {
78 ($action, @extraargs) = $self->other_article_actions($cgi);
81 my $method = $actions{$action};
82 return $self->$method($req, $article, $articles, @extraargs);
85 sub noarticle_dispatch {
86 my ($self, $req, $articles) = @_;
88 BSE::Permissions->check_logon($req)
89 or return BSE::Template->get_refresh($req->url('logon'), $req->cfg);
92 my $article = $self->_dummy_article($req, $articles, \$mymsg);
94 require BSE::Edit::Site;
95 my $site = BSE::Edit::Site->new(cfg=>$req->cfg, db=> BSE::DB->single);
96 return $site->edit_sections($req, $articles, $mymsg);
101 my %actions = $self->noarticle_actions;
102 for my $check (keys %actions) {
103 if ($cgi->param($check) || $cgi->param("$check.x")) {
108 my $method = $actions{$action};
109 return $self->$method($req, $article, $articles);
112 sub article_actions {
119 add_stepkid => 'add_stepkid',
120 del_stepkid => 'del_stepkid',
121 save_stepkids => 'save_stepkids',
122 add_stepparent => 'add_stepparent',
123 del_stepparent => 'del_stepparent',
124 save_stepparents => 'save_stepparents',
125 artimg => 'save_image_changes',
126 addimg => 'add_image',
127 a_edit_image => 'req_edit_image',
128 a_save_image => 'req_save_image',
130 showimages => 'show_images',
131 process => 'save_image_changes',
132 removeimg => 'remove_img',
133 moveimgup => 'move_img_up',
134 moveimgdown => 'move_img_down',
135 filelist => 'filelist',
136 fileadd => 'fileadd',
137 fileswap => 'fileswap',
138 filedel => 'filedel',
139 filesave => 'filesave',
140 a_edit_file => 'req_edit_file',
141 a_save_file => 'req_save_file',
144 a_thumb => 'req_thumb',
145 a_ajax_get => 'req_ajax_get',
146 a_ajax_save_body => 'req_ajax_save_body',
147 a_ajax_set => 'req_ajax_set',
148 a_filemeta => 'req_filemeta',
149 a_csrfp => 'req_csrfp',
150 a_tree => 'req_tree',
151 a_article => 'req_article',
152 a_config => 'req_config',
156 sub other_article_actions {
157 my ($self, $cgi) = @_;
159 for my $param ($cgi->param) {
160 if ($param =~ /^removeimg_(\d+)(\.x)?$/) {
161 return ('removeimg', $1 );
168 sub noarticle_actions {
173 a_csrfp => 'req_csrfp',
174 a_config => 'req_config',
179 my ($self, $parentid, $articles) = @_;
181 if ($parentid == -1) {
185 title=>'All Sections',
192 return $articles->getByPkey($parentid);
197 my ($object, $args) = @_;
199 my $value = $object->{$args};
200 defined $value or $value = '';
201 if ($value =~ /\cJ/ && $value =~ /\cM/) {
208 my ($object, $args) = @_;
210 my $value = $object->{$args};
211 defined $value or $value = '';
212 if ($value =~ /\cJ/ && $value =~ /\cM/) {
215 escape_html($value, '<>&"');
219 my ($level, $cfg) = @_;
221 escape_html($cfg->entry('level names', $level, 'Article'));
230 sub reparent_updown {
234 sub should_be_catalog {
235 my ($self, $article, $parent, $articles) = @_;
237 if ($article->{parentid} && (!$parent || $parent->{id} != $article->{parentid})) {
238 $parent = $articles->getByPkey($article->{id});
241 my $shopid = $self->cfg->entryErr('articles', 'shop');
243 return $article->{parentid} && $parent &&
244 ($article->{parentid} == $shopid ||
245 $parent->{generator} eq 'Generate::Catalog');
248 sub possible_parents {
249 my ($self, $article, $articles, $req) = @_;
254 my $shopid = $self->cfg->entryErr('articles', 'shop');
255 my @parents = $articles->getBy('level', $article->{level}-1);
256 @parents = grep { $_->{generator} eq 'Generate::Article'
257 && $_->{id} != $shopid } @parents;
259 # user can only select parent they can add to
260 @parents = grep $req->user_can('edit_add_child', $_), @parents;
262 @values = ( map {$_->{id}} @parents );
263 %labels = ( map { $_->{id} => "$_->{title} ($_->{id})" } @parents );
265 if ($article->{level} == 1 && $req->user_can('edit_add_child')) {
267 $labels{-1} = "No parent - this is a section";
270 if ($article->{id} && $self->reparent_updown($article)) {
271 # we also list the siblings and grandparent (if any)
272 my @siblings = grep $_->{id} != $article->{id} && $_->{id} != $shopid,
273 $articles->getBy(parentid => $article->{parentid});
274 @siblings = grep $req->user_can('edit_add_child', $_), @siblings;
275 push @values, map $_->{id}, @siblings;
276 @labels{map $_->{id}, @siblings} =
277 map { "-- move down a level -- $_->{title} ($_->{id})" } @siblings;
279 if ($article->{parentid} != -1) {
280 my $parent = $articles->getByPkey($article->{parentid});
281 if ($parent->{parentid} != -1) {
282 my $gparent = $articles->getByPkey($parent->{parentid});
283 if ($req->user_can('edit_add_child', $gparent)) {
284 push @values, $gparent->{id};
285 $labels{$gparent->{id}} =
286 "-- move up a level -- $gparent->{title} ($gparent->{id})";
290 if ($req->user_can('edit_add_child')) {
292 $labels{-1} = "-- move up a level -- become a section";
298 return (\@values, \%labels);
302 my ($self, $article, $articles, $cgi, $req, $what) = @_;
304 if ($what eq 'listed') {
305 my @values = qw(0 1);
306 my %labels = ( 0=>"No", 1=>"Yes");
307 if ($article->{level} <= 2) {
308 $labels{2} = "In Sections, but not menu";
312 $labels{2} = "In content, but not menus";
315 return popup_menu(-name=>'listed',
318 -default=>$article->{listed});
321 my ($values, $labels) = $self->possible_parents($article, $articles, $req);
323 if (defined $article->{parentid}) {
324 $html = popup_menu(-name=>'parentid',
327 -default => $article->{parentid},
331 $html = popup_menu(-name=>'parentid',
337 # munge the html - we display a default value, so we need to wrap the
338 # default <select /> around this one
339 $html =~ s!^<select[^>]+>|</select>!!gi;
345 my ($arg, $acts, $funcname, $templater) = @_;
346 my ($func, $args) = split ' ', $arg, 2;
347 return $templater->perform($acts, $func, $args) ? 'checked' : '';
350 sub iter_get_images {
351 my ($self, $article) = @_;
353 $article->{id} or return;
354 $self->get_images($article);
358 my ($article, $articles) = @_;
361 $article->{id} or return;
362 if (UNIVERSAL::isa($article, 'Article')) {
363 @children = $article->children;
365 elsif ($article->{id}) {
366 @children = $articles->children($article->{id});
369 return sort { $b->{displayOrder} <=> $a->{displayOrder} } @children;
372 sub tag_if_have_child_type {
373 my ($level, $cfg) = @_;
375 defined $cfg->entry("level names", $level+1);
379 my ($args, $acts, $isname, $templater) = @_;
381 my ($func, $funcargs) = split ' ', $args, 2;
382 return $templater->perform($acts, $func, $funcargs) ? 'Yes' : 'No';
385 sub default_template {
386 my ($self, $article, $cfg, $templates) = @_;
388 if ($article->{parentid}) {
389 my $template = $cfg->entry("children of $article->{parentid}", "template");
391 if $template && grep $_ eq $template, @$templates;
393 if ($article->{level}) {
394 my $template = $cfg->entry("level $article->{level}", "template");
396 if $template && grep $_ eq $template, @$templates;
398 return $templates->[0];
402 my ($self, $article, $cfg, $cgi) = @_;
404 my @templates = sort { $a->{name} cmp $b->{name} } $self->templates_long($article);
406 if ($article->{template} && grep $_->{name} eq $article->{template}, @templates) {
407 $default = $article->{template};
411 $default = $self->default_template($article, $cfg, \@templates);
418 $_->{name} eq $_->{description}
420 : "$_->{description} ($_->{name})"
423 return popup_menu(-name => 'template',
424 -values => [ map $_->{name}, @templates ],
426 -default => $default,
431 my ($self, $article) = @_;
434 my $imagedir = cfg_image_dir($self->cfg);
435 if (opendir TITLE_IMAGES, "$imagedir/titles") {
437 grep -f "$imagedir/titles/$_" && /\.(gif|jpeg|jpg|png)$/i,
438 readdir TITLE_IMAGES;
439 closedir TITLE_IMAGES;
445 sub tag_title_images {
446 my ($self, $article, $cfg, $cgi) = @_;
448 my @images = $self->title_images($article);
449 my @values = ( '', @images );
450 my %labels = ( '' => 'None', map { $_ => $_ } @images );
452 popup_menu(-name=>'titleImage',
455 -default=>$article->{id} ? $article->{titleImage} : '',
459 sub base_template_dirs {
464 my ($self, $article) = @_;
466 my @dirs = $self->base_template_dirs;
467 if (my $parentid = $article->{parentid}) {
468 my $section = "children of $parentid";
469 if (my $dirs = $self->cfg->entry($section, 'template_dirs')) {
470 push @dirs, split /,/, $dirs;
473 if (my $id = $article->{id}) {
474 my $section = "article $id";
475 if (my $dirs = $self->{cfg}->entry($section, 'template_dirs')) {
476 push @dirs, split /,/, $dirs;
479 if ($article->{level}) {
480 push @dirs, $article->{level};
481 my $dirs = $self->{cfg}->entry("level $article->{level}", 'template_dirs');
482 push @dirs, split /,/, $dirs if $dirs;
489 my ($self, $article) = @_;
491 my @dirs = $self->template_dirs($article);
493 my @basedirs = BSE::Template->template_dirs($self->{cfg});
494 for my $basedir (@basedirs) {
495 for my $dir (@dirs) {
496 my $path = File::Spec->catdir($basedir, $dir);
498 if (opendir TEMPLATE_DIR, $path) {
499 push(@templates, sort map "$dir/$_",
500 grep -f "$path/$_" && /\.(tmpl|html)$/i, readdir TEMPLATE_DIR);
501 closedir TEMPLATE_DIR;
507 # eliminate any dups, and order it nicely
509 @templates = sort { lc($a) cmp lc($b) }
510 grep !$seen{$_}++, @templates;
512 return (@templates, $self->extra_templates($article));
515 sub extra_templates {
516 my ($self, $article) = @_;
518 my $basedir = $self->{cfg}->entryVar('paths', 'templates');
520 if (my $id = $article->{id}) {
521 push @templates, 'index.tmpl'
522 if $id == 1 && -f "$basedir/index.html";
523 push @templates, 'index2.tmpl'
524 if $id == 2 && -f "$basedir/index2.html";
525 my $shopid = $self->{cfg}->entryErr('articles', 'shop');
526 push @templates, "shop_sect.tmpl"
527 if $id == $shopid && -f "$basedir/shop_sect.tmpl";
528 my $section = "article $id";
529 my $extras = $self->{cfg}->entry($section, 'extra_templates');
530 push @templates, grep /\.(tmpl|html)$/i, split /,/, $extras
540 return '' unless $article->{id} && $article->{id} != -1;
542 <a href="$ENV{SCRIPT_NAME}?id=$article->{parentid}">Edit parent</a> |
549 return unless $article->{id} && $article->{id} > 0;
553 sub _load_step_kids {
554 my ($article, $step_kids) = @_;
556 my @stepkids = OtherParents->getBy(parentId=>$article->{id}) if $article->{id};
557 %$step_kids = map { $_->{childId} => $_ } @stepkids;
558 $step_kids->{loaded} = 1;
561 sub tag_if_step_kid {
562 my ($article, $allkids, $rallkid_index, $step_kids) = @_;
564 _load_step_kids($article, $step_kids) unless $step_kids->{loaded};
566 my $kid = $allkids->[$$rallkid_index]
568 exists $step_kids->{$kid->{id}};
572 my ($article, $allkids, $rallkid_index, $step_kids, $arg) = @_;
574 _load_step_kids($article, $step_kids) unless $step_kids->{loaded};
576 my $kid = $allkids->[$$rallkid_index]
578 my $step_kid = $step_kids->{$kid->{id}}
581 #print STDERR "found kid (want $arg): ", Dumper($kid), Dumper($step_kid);
582 escape_html($step_kid->{$arg});
585 sub tag_move_stepkid {
586 my ($self, $cgi, $req, $article, $allkids, $rallkids_index, $arg,
587 $acts, $funcname, $templater) = @_;
589 $req->user_can(edit_reorder_children => $article)
592 @$allkids > 1 or return '';
594 my ($img_prefix, $urladd) = DevHelp::Tags->get_parms($arg, $acts, $templater);
595 $img_prefix = '' unless defined $img_prefix;
596 $urladd = '' unless defined $urladd;
598 my $cgi_uri = $self->{cfg}->entry('uri', 'cgi', '/cgi-bin');
599 my $url = $ENV{SCRIPT_NAME} . "?id=$article->{id}";
600 if ($cgi->param('_t')) {
601 $url .= "&_t=".$cgi->param('_t');
606 if ($$rallkids_index < $#$allkids) {
607 $down_url = "$cgi_uri/admin/move.pl?stepparent=$article->{id}&d=swap&id=$allkids->[$$rallkids_index]{id}&other=$allkids->[$$rallkids_index+1]{id}";
610 if ($$rallkids_index > 0) {
611 $up_url = "$cgi_uri/admin/move.pl?stepparent=$article->{id}&d=swap&id=$allkids->[$$rallkids_index]{id}&other=$allkids->[$$rallkids_index-1]{id}";
614 return make_arrows($req->cfg, $down_url, $up_url, $url, $img_prefix);
617 sub possible_stepkids {
618 my ($req, $article, $articles, $stepkids) = @_;
620 $req->user_can(edit_stepkid_add => $article)
626 my @possible = sort { lc $a->{title} cmp lc $b->{title} }
627 $article->possible_stepchildren;
628 if ($req->access_control && $req->cfg->entry('basic', 'access_filter_steps', 0)) {
629 @possible = grep $req->user_can(edit_stepparent_add => $_->{id}), @possible;
634 sub tag_possible_stepkids {
635 my ($step_kids, $req, $article, $possstepkids, $articles, $cgi) = @_;
637 _load_step_kids($article, $step_kids) unless $step_kids->{loaded};
638 @$possstepkids = possible_stepkids($req, $article, $articles, $step_kids)
639 unless @$possstepkids;
640 my %labels = map { $_->{id} => "$_->{title} ($_->{id})" } @$possstepkids;
642 popup_menu(-name=>'stepkid',
643 -values=> [ map $_->{id}, @$possstepkids ],
644 -labels => \%labels);
647 sub tag_if_possible_stepkids {
648 my ($step_kids, $req, $article, $possstepkids, $articles, $cgi) = @_;
650 _load_step_kids($article, $step_kids) unless $step_kids->{loaded};
651 @$possstepkids = possible_stepkids($req, $article, $articles, $step_kids)
652 unless @$possstepkids;
657 sub iter_get_stepparents {
660 return unless $article->{id} && $article->{id} > 0;
662 OtherParents->getBy(childId=>$article->{id});
665 sub tag_ifStepParents {
666 my ($args, $acts, $funcname, $templater) = @_;
668 return $templater->perform($acts, 'ifStepparents', '');
671 sub tag_stepparent_targ {
672 my ($article, $targs, $rindex, $arg) = @_;
674 if ($article->{id} && $article->{id} > 0 && !@$targs) {
675 @$targs = $article->step_parents;
677 escape_html($targs->[$$rindex]{$arg});
680 sub tag_move_stepparent {
681 my ($self, $cgi, $req, $article, $stepparents, $rindex, $arg,
682 $acts, $funcname, $templater) = @_;
684 $req->user_can(edit_reorder_stepparents => $article)
687 @$stepparents > 1 or return '';
689 my ($img_prefix, $urladd) = DevHelp::Tags->get_parms($arg, $acts, $templater);
690 $img_prefix = '' unless defined $img_prefix;
691 $urladd = '' unless defined $urladd;
693 my $cgi_uri = $self->{cfg}->entry('uri', 'cgi', '/cgi-bin');
694 my $images_uri = $self->{cfg}->entry('uri', 'images', '/images');
696 my $url = $ENV{SCRIPT_NAME} . "?id=$article->{id}";
697 if ($cgi->param('_t')) {
698 $url .= "&_t=".$cgi->param('_t');
701 $url .= "#stepparents";
702 my $blank = qq!<img src="$images_uri/trans_pixel.gif" width="17" height="13" border="0" align="absbottom" alt="" />!;
704 if ($$rindex < $#$stepparents) {
705 $down_url = "$cgi_uri/admin/move.pl?stepchild=$article->{id}&id=$stepparents->[$$rindex]{parentId}&d=swap&other=$stepparents->[$$rindex+1]{parentId}";
709 $up_url = "$cgi_uri/admin/move.pl?stepchild=$article->{id}&id=$stepparents->[$$rindex]{parentId}&d=swap&other=$stepparents->[$$rindex-1]{parentId}";
712 return make_arrows($req->cfg, $down_url, $up_url, $url, $img_prefix);
715 sub _stepparent_possibles {
716 my ($req, $article, $articles, $targs) = @_;
718 $req->user_can(edit_stepparent_add => $article)
724 @$targs = $article->step_parents unless @$targs;
725 my %targs = map { $_->{id}, 1 } @$targs;
726 my @possibles = $article->possible_stepparents;
727 if ($req->access_control && $req->cfg->entry('basic', 'access_filter_steps', 0)) {
728 @possibles = grep $req->user_can(edit_stepkid_add => $_->{id}), @possibles;
730 @possibles = sort { lc $a->{title} cmp lc $b->{title} } @possibles;
735 sub tag_if_stepparent_possibles {
736 my ($req, $article, $articles, $targs, $possibles) = @_;
738 if ($article->{id} && $article->{id} > 0 && !@$possibles) {
739 @$possibles = _stepparent_possibles($req, $article, $articles, $targs);
744 sub tag_stepparent_possibles {
745 my ($cgi, $req, $article, $articles, $targs, $possibles) = @_;
747 if ($article->{id} && $article->{id} > 0 && !@$possibles) {
748 @$possibles = _stepparent_possibles($req, $article, $articles, $targs);
750 popup_menu(-name=>'stepparent',
751 -values => [ map $_->{id}, @$possibles ],
752 -labels => { map { $_->{id}, "$_->{title} ($_->{id})" }
757 my ($self, $article) = @_;
759 return $self->get_files($article);
763 my ($self, $article) = @_;
765 return unless $article->{id} && $article->{id} > 0;
767 return $article->files;
770 sub tag_edit_parent {
773 return '' unless $article->{id} && $article->{id} != -1;
776 <a href="$ENV{SCRIPT_NAME}?id=$article->{parentid}">Edit parent</a> |
780 sub tag_if_children {
781 my ($args, $acts, $funcname, $templater) = @_;
783 return $templater->perform($acts, 'ifChildren', '');
787 my ($self, $req, $article, $kids, $rindex, $arg,
788 $acts, $funcname, $templater) = @_;
790 $req->user_can('edit_reorder_children', $article)
793 @$kids > 1 or return '';
795 $$rindex >=0 && $$rindex < @$kids
796 or return '** movechild can only be used in the children iterator **';
798 my ($img_prefix, $urladd) = DevHelp::Tags->get_parms($arg, $acts, $templater);
799 $img_prefix = '' unless defined $img_prefix;
800 $urladd = '' unless defined $urladd;
802 my $cgi_uri = $self->{cfg}->entry('uri', 'cgi', '/cgi-bin');
803 my $images_uri = $self->{cfg}->entry('uri', 'images', '/images');
804 my $urlbase = admin_base_url($req->cfg);
805 my $refresh_url = "$urlbase$ENV{SCRIPT_NAME}?id=$article->{id}";
806 my $t = $req->cgi->param('_t');
807 if ($t && $t =~ /^\w+$/) {
808 $refresh_url .= "&_t=$t";
811 $refresh_url .= $urladd;
813 my $id = $kids->[$$rindex]{id};
815 if ($$rindex < $#$kids) {
816 $down_url = "$cgi_uri/admin/move.pl?id=$id&d=down&edit=1&all=1";
820 $up_url = "$cgi_uri/admin/move.pl?id=$id&d=up&edit=1&all=1"
823 return make_arrows($req->cfg, $down_url, $up_url, $refresh_url, $img_prefix);
827 my ($cfg, $article, $args, $acts, $funcname, $templater) = @_;
828 my ($which, $name) = split / /, $args, 2;
832 && ($gen_class = $templater->perform($acts, $which, 'generator'))) {
833 eval "use $gen_class";
835 my $gen = $gen_class->new(top => $article, cfg => $cfg);
836 my $link = $gen->edit_link($templater->perform($acts, $which, 'id'));
837 return qq!<a href="$link">$name</a>!;
844 my ($req, $article, $rindex, $images, $arg,
845 $acts, $funcname, $templater) = @_;
847 $req->user_can(edit_images_reorder => $article)
850 @$images > 1 or return '';
852 $$rindex >= 0 && $$rindex < @$images
853 or return '** imgmove can only be used in image iterator **';
855 my ($img_prefix, $urladd) = DevHelp::Tags->get_parms($arg, $acts, $templater);
856 $img_prefix = '' unless defined $img_prefix;
857 $urladd = '' unless defined $urladd;
859 my $urlbase = admin_base_url($req->cfg);
860 my $url = "$urlbase$ENV{SCRIPT_NAME}?id=$article->{id}";
861 my $t = $req->cgi->param('_t');
862 if ($t && $t =~ /^\w+$/) {
867 my $image = $images->[$$rindex];
868 my $csrfp = $req->get_csrf_token("admin_move_image");
869 my $baseurl = "$ENV{SCRIPT_NAME}?id=$article->{id}&imageid=$image->{id}&";
870 $baseurl .= "_csrfp=$csrfp&";
872 if ($$rindex < $#$images) {
873 $down_url = $baseurl . "moveimgdown=1";
877 $up_url = $baseurl . "moveimgup=1";
879 return make_arrows($req->cfg, $down_url, $up_url, $url, $img_prefix);
883 my ($self, $req, $article, $files, $rindex, $arg,
884 $acts, $funcname, $templater) = @_;
886 $req->user_can('edit_files_reorder', $article)
889 @$files > 1 or return '';
891 my ($img_prefix, $urladd) = DevHelp::Tags->get_parms($arg, $acts, $templater);
892 $img_prefix = '' unless defined $img_prefix;
893 $urladd = '' unless defined $urladd;
895 $$rindex >= 0 && $$rindex < @$files
896 or return '** movefiles can only be used in the files iterator **';
898 my $urlbase = admin_base_url($req->cfg);
899 my $url = "$urlbase$ENV{SCRIPT_NAME}?id=$article->{id}$urladd";
900 my $t = $req->cgi->param('_t');
901 if ($t && $t =~ /^\w+$/) {
906 my $csrfp = $req->get_csrf_token("admin_move_file");
907 my $baseurl = "$ENV{SCRIPT_NAME}?fileswap=1&id=$article->{id}&";
908 $baseurl .= "_csrfp=$csrfp&";
909 if ($$rindex < $#$files) {
910 $down_url = $baseurl . "file1=$files->[$$rindex]{id}&file2=$files->[$$rindex+1]{id}";
914 $up_url = $baseurl . "file1=$files->[$$rindex]{id}&file2=$files->[$$rindex-1]{id}";
917 return make_arrows($req->cfg, $down_url, $up_url, $url, $img_prefix);
921 my ($article, $cgi, $args, $acts, $funcname, $templater) = @_;
923 my ($col, $func, $funcargs) = split ' ', $args, 3;
924 my $value = $cgi->param($col);
925 if (defined $value) {
926 return escape_html($value);
930 return $templater->perform($acts, $func, $funcargs);
933 $value = $article->{$args};
934 defined $value or $value = '';
935 return escape_html($value);
940 sub iter_admin_users {
941 require BSE::TB::AdminUsers;
943 BSE::TB::AdminUsers->all;
946 sub iter_admin_groups {
947 require BSE::TB::AdminGroups;
949 BSE::TB::AdminGroups->all;
952 sub tag_if_field_perm {
953 my ($req, $article, $field) = @_;
955 unless ($field =~ /^\w+$/) {
956 print STDERR "Bad fieldname '$field'\n";
959 if ($article->{id}) {
960 return $req->user_can("edit_field_edit_$field", $article);
963 #print STDERR "adding, always successful\n";
969 my ($self, $req, $article, $args, $acts, $funcname, $templater) = @_;
971 my ($col, $func, $funcargs) = split ' ', $args, 3;
972 if ($article->{id}) {
974 return $templater->perform($acts, $func, $funcargs);
977 my $value = $article->{$args};
978 defined $value or $value = '';
979 return escape_html($value, '<>&"');
983 my $value = $self->default_value($req, $article, $col);
984 defined $value or $value = '';
985 return escape_html($value, '<>&"');
995 sub tag_if_flag_set {
996 my ($article, $arg, $acts, $funcname, $templater) = @_;
998 my @args = DevHelp::Tags->get_parms($arg, $acts, $templater);
1001 return index($article->{flags}, $args[0]) >= 0;
1005 my ($article, $articles) = @_;
1008 my $temp = $article;
1009 defined($temp->{parentid}) or return;
1010 while ($temp->{parentid} > 0
1011 and my $crumb = $articles->getByPkey($temp->{parentid})) {
1012 unshift @crumbs, $crumb;
1020 my ($args, $acts, $funcname, $templater) = @_;
1022 exists $acts->{$args} or return "** need an article name **";
1023 my $generator = $templater->perform($acts, $args, 'generator');
1025 $generator =~ /^(?:BSE::)?Generate::(\w+)$/
1026 or return "** invalid generator $generator **";
1031 sub _get_thumbs_class {
1034 $self->{cfg}->entry('editor', 'allow_thumb', 0)
1037 my $class = $self->{cfg}->entry('editor', 'thumbs_class')
1040 (my $filename = "$class.pm") =~ s!::!/!g;
1041 eval { require $filename; };
1043 print STDERR "** Error loading thumbs_class $class ($filename): $@\n";
1047 eval { $obj = $class->new($self->{cfg}) };
1049 print STDERR "** Error creating thumbs objects $class: $@\n";
1056 sub tag_thumbimage {
1057 my ($cfg, $thumbs_obj, $current_image, $args) = @_;
1059 $thumbs_obj or return '';
1061 $$current_image or return '** no current image **';
1063 my $imagedir = cfg_image_dir($cfg);
1065 my $filename = "$imagedir/$$current_image->{image}";
1066 -e $filename or return "** image file missing **";
1068 defined $args && $args =~ /\S/
1069 or $args = "editor";
1071 my $image = $$current_image;
1072 return $image->thumb
1079 sub tag_file_display {
1080 my ($self, $files, $file_index) = @_;
1082 $$file_index >= 0 && $$file_index < @$files
1083 or return "* file_display only usable inside a files iterator *";
1084 my $file = $files->[$$file_index];
1086 my $disp_type = $self->cfg->entry("editor", "file_display", "");
1088 return $file->inline
1091 field => $disp_type,
1096 my ($self, $cfg, $rcurrent, $args) = @_;
1101 my ($align, $rest) = split ' ', $args, 2;
1103 if ($align && exists $im->{$align}) {
1104 if ($align eq 'src') {
1105 return escape_html($im->image_url($self->{cfg}));
1108 return escape_html($im->{$align});
1112 return $im->formatted
1122 my ($self, $acts, $request, $article, $articles, $msg, $errors) = @_;
1124 my $cgi = $request->cgi;
1125 my $show_full = $cgi->param('f_showfull');
1126 my $if_error = $msg || ($errors && keys %$errors) || $request->cgi->param("_e");
1127 $msg ||= join "\n", map escape_html($_), $cgi->param('message'), $cgi->param('m');
1128 $msg ||= $request->message($errors);
1130 if ($article->{id}) {
1131 if ($article->{parentid} > 0) {
1132 $parent = $article->parent;
1135 $parent = { title=>"No parent - this is a section", id=>-1 };
1139 $parent = { title=>"How did we get here?", id=>0 };
1141 my $cfg = $self->{cfg};
1142 my $mbcs = $cfg->entry('html', 'mbcs', 0);
1143 my $tag_hash = $mbcs ? \&tag_hash_mbcs : \&tag_hash;
1144 my $thumbs_obj_real = $self->_get_thumbs_class();
1145 my $thumbs_obj = $show_full ? undef : $thumbs_obj_real;
1156 my $stepparent_index;
1157 my @stepparent_targs;
1158 my @stepparentpossibles;
1163 my $it = BSE::Util::Iterate->new;
1166 $request->admin_tags,
1167 article => [ $tag_hash, $article ],
1168 old => [ \&tag_old, $article, $cgi ],
1169 default => [ \&tag_default, $self, $request, $article ],
1170 articleType => [ \&tag_art_type, $article->{level}, $cfg ],
1171 parentType => [ \&tag_art_type, $article->{level}-1, $cfg ],
1172 ifNew => [ \&tag_if_new, $article ],
1173 list => [ \&tag_list, $self, $article, $articles, $cgi, $request ],
1174 script => $ENV{SCRIPT_NAME},
1175 level => $article->{level},
1176 checked => \&tag_checked,
1178 ([ \&iter_get_images, $self, $article ], 'image', 'images', \@images,
1179 \$image_index, undef, \$current_image),
1180 image => [ tag_image => $self, $cfg, \$current_image ],
1181 thumbimage => [ \&tag_thumbimage, $cfg, $thumbs_obj, \$current_image ],
1182 ifThumbs => defined($thumbs_obj),
1183 ifCanThumbs => defined($thumbs_obj_real),
1184 imgmove => [ \&tag_imgmove, $request, $article, \$image_index, \@images ],
1186 ifError => $if_error,
1187 DevHelp::Tags->make_iterator2
1188 ([ \&iter_get_kids, $article, $articles ],
1189 'child', 'children', \@children, \$child_index),
1190 ifchildren => \&tag_if_children,
1191 childtype => [ \&tag_art_type, $article->{level}+1, $cfg ],
1192 ifHaveChildType => [ \&tag_if_have_child_type, $article->{level}, $cfg ],
1193 movechild => [ \&tag_movechild, $self, $request, $article, \@children,
1196 templates => [ \&tag_templates, $self, $article, $cfg, $cgi ],
1197 titleImages => [ \&tag_title_images, $self, $article, $cfg, $cgi ],
1198 editParent => [ \&tag_edit_parent, $article ],
1199 DevHelp::Tags->make_iterator2
1200 ([ \&iter_allkids, $article ], 'kid', 'kids', \@allkids, \$allkid_index),
1202 [ \&tag_if_step_kid, $article, \@allkids, \$allkid_index, \%stepkids ],
1203 stepkid => [ \&tag_step_kid, $article, \@allkids, \$allkid_index,
1206 [ \&tag_move_stepkid, $self, $cgi, $request, $article, \@allkids,
1208 possible_stepkids =>
1209 [ \&tag_possible_stepkids, \%stepkids, $request, $article,
1210 \@possstepkids, $articles, $cgi ],
1212 [ \&tag_if_possible_stepkids, \%stepkids, $request, $article,
1213 \@possstepkids, $articles, $cgi ],
1214 DevHelp::Tags->make_iterator2
1215 ( [ \&iter_get_stepparents, $article ], 'stepparent', 'stepparents',
1216 \@stepparents, \$stepparent_index),
1217 ifStepParents => \&tag_ifStepParents,
1219 [ \&tag_stepparent_targ, $article, \@stepparent_targs,
1220 \$stepparent_index ],
1222 [ \&tag_move_stepparent, $self, $cgi, $request, $article, \@stepparents,
1223 \$stepparent_index ],
1224 ifStepparentPossibles =>
1225 [ \&tag_if_stepparent_possibles, $request, $article, $articles,
1226 \@stepparent_targs, \@stepparentpossibles, ],
1227 stepparent_possibles =>
1228 [ \&tag_stepparent_possibles, $cgi, $request, $article, $articles,
1229 \@stepparent_targs, \@stepparentpossibles, ],
1230 DevHelp::Tags->make_iterator2
1231 ([ iter_files => $self, $article ], 'file', 'files', \@files, \$file_index ),
1233 [ \&tag_movefiles, $self, $request, $article, \@files, \$file_index ],
1236 code => [ iter_file_metas => $self, \@files, \$file_index ],
1237 plural => "file_metas",
1238 single => "file_meta",
1241 file_display => [ tag_file_display => $self, \@files, \$file_index ],
1242 DevHelp::Tags->make_iterator2
1243 (\&iter_admin_users, 'iadminuser', 'adminusers'),
1244 DevHelp::Tags->make_iterator2
1245 (\&iter_admin_groups, 'iadmingroup', 'admingroups'),
1246 edit => [ \&tag_edit_link, $cfg, $article ],
1247 error => [ $tag_hash, $errors ],
1248 error_img => [ \&tag_error_img, $cfg, $errors ],
1249 ifFieldPerm => [ \&tag_if_field_perm, $request, $article ],
1250 parent => [ $tag_hash, $parent ],
1251 DevHelp::Tags->make_iterator2
1252 ([ \&iter_flags, $self ], 'flag', 'flags' ),
1253 ifFlagSet => [ \&tag_if_flag_set, $article ],
1254 DevHelp::Tags->make_iterator2
1255 ([ \&iter_crumbs, $article, $articles ], 'crumb', 'crumbs' ),
1256 typename => \&tag_typename,
1257 $it->make_iterator([ \&iter_groups, $request ],
1258 'group', 'groups', \@groups, undef, undef,
1260 $it->make_iterator([ iter_image_stores => $self],
1261 'image_store', 'image_stores'),
1262 $it->make_iterator([ iter_file_stores => $self],
1263 'file_store', 'file_stores'),
1264 ifGroupRequired => [ \&tag_ifGroupRequired, $article, \$current_group ],
1268 sub iter_image_stores {
1271 my $mgr = $self->_image_manager;
1273 return map +{ name => $_->name, description => $_->description },
1280 require BSE::TB::ArticleFiles;
1282 return BSE::TB::ArticleFiles->file_manager($self->cfg);
1285 sub iter_file_stores {
1288 require BSE::TB::ArticleFiles;
1289 my $mgr = $self->_file_manager($self->cfg);
1291 return map +{ name => $_->name, description => $_->description },
1298 require BSE::TB::SiteUserGroups;
1299 BSE::TB::SiteUserGroups->admin_and_query_groups($req->cfg);
1302 sub tag_ifGroupRequired {
1303 my ($article, $rgroup) = @_;
1305 $$rgroup or return 0;
1307 $article->is_accessible_to($$rgroup);
1311 my ($self, $article, $cgi) = @_;
1313 my $base = $article->{level};
1314 my $t = $cgi->param('_t');
1315 if ($t && $t =~ /^\w+$/) {
1318 return $self->{cfg}->entry('admin templates', $base,
1319 "admin/edit_$base");
1323 my ($self, $article, $cgi) = @_;
1325 $self->edit_template($article, $cgi);
1329 my ($self, $request, $article, $articles, $msg, $errors) = @_;
1331 my $cgi = $request->cgi;
1333 %acts = $self->low_edit_tags(\%acts, $request, $article, $articles, $msg,
1335 my $template = $article->{id} ?
1336 $self->edit_template($article, $cgi) : $self->add_template($article, $cgi);
1338 return $request->response($template, \%acts);
1342 my ($self, $request, $article, $articles, $msg, $errors) = @_;
1344 return $self->low_edit_form($request, $article, $articles, $msg, $errors);
1347 sub _dummy_article {
1348 my ($self, $req, $articles, $rmsg) = @_;
1351 my $cgi = $req->cgi;
1352 my $parentid = $cgi->param('parentid');
1354 if ($parentid =~ /^\d+$/) {
1355 if (my $parent = $self->get_parent($parentid, $articles)) {
1356 $level = $parent->{level}+1;
1362 elsif ($parentid eq "-1") {
1366 unless (defined $level) {
1367 $level = $cgi->param('level');
1368 undef $level unless defined $level && $level =~ /^\d+$/
1369 && $level > 0 && $level < 100;
1370 defined $level or $level = 3;
1374 my @cols = Article->columns;
1375 @article{@cols} = ('') x @cols;
1377 $article{parentid} = $parentid;
1378 $article{level} = $level;
1379 $article{body} = '<maximum of 64Kb>';
1380 $article{listed} = 1;
1381 $article{generator} = $self->generator;
1383 my ($values, $labels) = $self->possible_parents(\%article, $articles, $req);
1385 $$rmsg = "You can't add children to any article at that level";
1393 my ($self, $req, $article, $articles, $msg, $errors) = @_;
1395 return $self->low_edit_form($req, $article, $articles, $msg, $errors);
1398 sub generator { 'Generate::Article' }
1403 my $gen = $self->generator;
1405 ($gen =~ /(\w+)$/)[0] || 'Article';
1408 sub _validate_common {
1409 my ($self, $data, $articles, $errors, $article) = @_;
1411 # if (defined $data->{parentid} && $data->{parentid} =~ /^(?:-1|\d+)$/) {
1412 # unless ($data->{parentid} == -1 or
1413 # $articles->getByPkey($data->{parentid})) {
1414 # $errors->{parentid} = "Selected parent article doesn't exist";
1418 # $errors->{parentid} = "You need to select a valid parent";
1420 if (exists $data->{title} && $data->{title} !~ /\S/) {
1421 $errors->{title} = "Please enter a title";
1424 if (exists $data->{template} && $data->{template} =~ /\.\./) {
1425 $errors->{template} = "Please only select templates from the list provided";
1427 if (exists $data->{linkAlias}
1428 && length $data->{linkAlias}) {
1429 unless ($data->{linkAlias} =~ /\A[a-zA-Z0-9-_]+\z/
1430 && $data->{linkAlias} =~ /[A-Za-z]/) {
1431 $errors->{linkAlias} = "Link alias must contain only alphanumerics and contain at least one letter";
1437 my ($self, $data, $articles, $errors) = @_;
1439 $self->_validate_common($data, $articles, $errors);
1440 if (!$errors->{linkAlias} && defined $data->{linkAlias} && length $data->{linkAlias}) {
1441 my $other = $articles->getBy(linkAlias => $data->{linkAlias});
1443 and $errors->{linkAlias} =
1444 "Duplicate link alias - already used by article $other->{id}";
1446 custom_class($self->{cfg})
1447 ->article_validate($data, undef, $self->typename, $errors);
1449 return !keys %$errors;
1453 my ($self, $article, $data, $articles, $errors, $ajax) = @_;
1455 $self->_validate_common($data, $articles, $errors, $article);
1456 custom_class($self->{cfg})
1457 ->article_validate($data, $article, $self->typename, $errors);
1459 if (exists $data->{release}) {
1460 if ($ajax && !dh_parse_sql_date($data->{release})
1461 || !$ajax && !dh_parse_date($data->{release})) {
1462 $errors->{release} = "Invalid release date";
1466 if (!$errors->{linkAlias}
1467 && defined $data->{linkAlias}
1468 && length $data->{linkAlias}
1469 && $data->{linkAlias} ne $article->{linkAlias}) {
1470 my $other = $articles->getBy(linkAlias => $data->{linkAlias});
1471 $other && $other->{id} != $article->{id}
1472 and $errors->{linkAlias} = "Duplicate link alias - already used by article $other->{id}";
1475 return !keys %$errors;
1478 sub validate_parent {
1483 my ($self, $req, $data, $articles) = @_;
1485 custom_class($self->{cfg})
1486 ->article_fill_new($data, $self->typename);
1492 my ($self, $article) = @_;
1494 # check the config for the article and any of its ancestors
1495 my $work_article = $article;
1496 my $path = $self->{cfg}->entry('article uris', $work_article->{id});
1498 last if $work_article->{parentid} == -1;
1499 $work_article = $work_article->parent;
1500 $path = $self->{cfg}->entry('article uris', $work_article->{id});
1502 return $path if $path;
1504 $self->default_link_path($article);
1507 sub default_link_path {
1508 my ($self, $article) = @_;
1510 $self->{cfg}->entry('uri', 'articles', '/a');
1514 my ($self, $article) = @_;
1516 if ($article->is_dynamic) {
1517 return "/cgi-bin/page.pl?page=$article->{id}&title=".escape_uri($article->{title});
1520 my $article_uri = $self->link_path($article);
1521 my $link = "$article_uri/$article->{id}.html";
1522 my $link_titles = $self->{cfg}->entryBool('basic', 'link_titles', 0);
1524 (my $extra = lc $article->{title}) =~ tr/a-z0-9/_/sc;
1525 $link .= "/" . $extra . "_html";
1532 my ($self, $req, $article, $articles) = @_;
1534 $req->check_csrf("admin_add_article")
1535 or return $self->csrf_error($req, undef, "admin_add_article", "Add Article");
1537 my $cgi = $req->cgi;
1539 my $table_object = $self->table_object($articles);
1540 my @columns = $table_object->rowClass->columns;
1541 $self->save_thumbnail($cgi, undef, \%data);
1542 for my $name (@columns) {
1543 $data{$name} = $cgi->param($name)
1544 if defined $cgi->param($name);
1546 $data{flags} = join '', sort $cgi->param('flags');
1550 if (!defined $data{parentid} || $data{parentid} eq '') {
1551 $errors{parentid} = "Please select a parent";
1553 elsif ($data{parentid} !~ /^(?:-1|\d+)$/) {
1554 $errors{parentid} = "Invalid parent selection (template bug)";
1556 $self->validate(\%data, $articles, \%errors);
1558 if ($req->is_ajax) {
1559 return $req->json_content
1563 error_code => "FIELD",
1564 message => $req->message(\%errors),
1568 return $self->add_form($req, $article, $articles, $msg, \%errors);
1575 if ($data{parentid} > 0) {
1576 $parent = $articles->getByPkey($data{parentid}) or die;
1577 if ($req->user_can('edit_add_child', $parent)) {
1578 for my $name (@columns) {
1579 if (exists $data{$name} &&
1580 !$req->user_can("edit_add_field_$name", $parent)) {
1581 delete $data{$name};
1586 $parent_msg = "You cannot add a child to that article";
1587 $parent_code = "ACCESS";
1591 if ($req->user_can('edit_add_child')) {
1592 for my $name (@columns) {
1593 if (exists $data{$name} &&
1594 !$req->user_can("edit_add_field_$name")) {
1595 delete $data{$name};
1600 $parent_msg = "You cannot create a top-level article";
1601 $parent_code = "ACCESS";
1605 $self->validate_parent(\%data, $articles, $parent, \$parent_msg)
1606 or $parent_code = "PARENT";
1609 if ($req->is_ajax) {
1610 return $req->json_content
1613 message => $parent_msg,
1614 error_code => $parent_code,
1619 return $self->add_form($req, $article, $articles, $parent_msg);
1623 my $level = $parent ? $parent->{level}+1 : 1;
1624 $data{level} = $level;
1625 $data{displayOrder} = time;
1627 $data{admin} ||= '';
1628 $data{generator} = $self->generator;
1629 $data{lastModified} = now_sqldatetime();
1630 $data{listed} = 1 unless defined $data{listed};
1633 $data{pageTitle} = '' unless defined $data{pageTitle};
1634 my $user = $req->getuser;
1635 $data{createdBy} = $user ? $user->{logon} : '';
1636 $data{lastModifiedBy} = $user ? $user->{logon} : '';
1637 $data{created} = now_sqldatetime();
1640 $data{force_dynamic} = 0;
1641 $data{cached_dynamic} = 0;
1642 $data{inherit_siteuser_rights} = 1;
1645 $data{metaDescription} = '' unless defined $data{metaDescription};
1646 $data{metaKeywords} = '' unless defined $data{metaKeywords};
1649 $self->fill_new_data($req, \%data, $articles);
1650 for my $col (qw(titleImage imagePos template keyword menu titleAlias linkAlias body author summary)) {
1652 or $data{$col} = $self->default_value($req, \%data, $col);
1655 for my $col (qw/force_dynamic inherit_siteuser_rights/) {
1656 if ($req->user_can("edit_add_field_$col", $parent)
1657 && $cgi->param("save_$col")) {
1658 $data{$col} = $cgi->param($col) ? 1 : 0;
1661 $data{$col} = $self->default_value($req, \%data, $col);
1665 unless ($req->is_ajax) {
1666 for my $col (qw(release expire)) {
1667 $data{$col} = sql_date($data{$col});
1671 # these columns are handled a little differently
1672 for my $col (qw(release expire threshold summaryLength )) {
1674 or $data{$col} = $self->default_value($req, \%data, $col);
1678 $article = $table_object->add(@data{@columns});
1680 # we now have an id - generate the links
1682 $article->update_dynamic($self->{cfg});
1683 my $cgi_uri = $self->{cfg}->entry('uri', 'cgi', '/cgi-bin');
1684 $article->setAdmin("$cgi_uri/admin/admin.pl?id=$article->{id}");
1685 $article->setLink($self->make_link($article));
1688 my ($after_id) = $cgi->param("_after");
1689 if (defined $after_id) {
1690 Articles->reorder_child($article->{parentid}, $article->{id}, $after_id);
1691 # reload, the displayOrder probably changed
1692 $article = $articles->getByPkey($article->{id});
1695 use Util 'generate_article';
1696 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
1698 if ($req->is_ajax) {
1699 return $req->json_content
1703 article => $self->_article_data($req, $article),
1708 my $r = $cgi->param('r');
1710 $r .= ($r =~ /\?/) ? '&' : '?';
1711 $r .= "id=$article->{id}";
1714 $r = admin_base_url($req->cfg) . $article->{admin};
1716 return BSE::Template->get_refresh($r, $self->{cfg});
1720 my ($self, $req, $article, $data) = @_;
1722 if (exists $data->{body}) {
1723 $data->{body} =~ s/\x0D\x0A/\n/g;
1724 $data->{body} =~ tr/\r/\n/;
1726 for my $col (Article->columns) {
1727 next if $col =~ /^custom/;
1728 $article->{$col} = $data->{$col}
1729 if exists $data->{$col} && $col ne 'id' && $col ne 'parentid';
1731 custom_class($self->{cfg})
1732 ->article_fill_old($article, $data, $self->typename);
1738 my ($self, $req, $article) = @_;
1740 my $article_data = $article->data_only;
1741 $article_data->{link} = $article->link($req->cfg);
1742 $article_data->{images} =
1744 map $self->_image_data($req->cfg, $_), $article->images
1746 $article_data->{files} =
1748 map $_->data_only, $article->files,
1751 return $article_data;
1762 ACCESS - user doesn't have access to this article.
1766 LASTMOD - lastModified value doesn't match that in the article
1770 PARENT - invalid parentid specified
1777 my ($self, $req, $article, $articles) = @_;
1779 $req->check_csrf("admin_save_article")
1780 or return $self->csrf_error($req, $article, "admin_save_article", "Save Article");
1782 $req->user_can(edit_save => $article)
1783 or return $self->_service_error
1784 ($req, $article, $articles, "You don't have access to save this article",
1787 my $old_dynamic = $article->is_dynamic;
1788 my $cgi = $req->cgi;
1790 for my $name ($article->columns) {
1791 $data{$name} = $cgi->param($name)
1792 if defined($cgi->param($name)) and $name ne 'id' && $name ne 'parentid'
1793 && $req->user_can("edit_field_edit_$name", $article);
1797 # checks editor lastModified against record lastModified
1798 if ($self->{cfg}->entry('editor', 'check_modified')) {
1799 if ($article->{lastModified} ne $cgi->param('lastModified')) {
1800 my $whoModified = '';
1801 my $timeModified = ampm_time($article->{lastModified});
1802 if ($article->{lastModifiedBy}) {
1803 $whoModified = "by '$article->{lastModifiedBy}'";
1805 print STDERR "non-matching lastModified, article not saved\n";
1806 my $msg = "Article changes not saved, this article was modified $whoModified at $timeModified since this editor was loaded";
1807 return $self->_service_error($req, $article, $articles, $msg, {}, "LASTMOD");
1812 # possibly this needs tighter error checking
1813 $data{flags} = join '', sort $cgi->param('flags')
1814 if $req->user_can("edit_field_edit_flags", $article);
1816 if (exists $article->{template} &&
1817 $article->{template} =~ m|\.\.|) {
1818 $errors{template} = "Please only select templates from the list provided";
1820 $self->validate_old($article, \%data, $articles, \%errors, scalar $req->is_ajax)
1821 or return $self->_service_error($req, $article, $articles, undef, \%errors, "FIELD");
1822 $self->save_thumbnail($cgi, $article, \%data)
1823 if $req->user_can('edit_field_edit_thumbImage', $article);
1824 $self->fill_old_data($req, $article, \%data);
1827 my $newparentid = $cgi->param('parentid');
1829 && $req->user_can('edit_field_edit_parentid', $article)
1830 && $newparentid != $article->{parentid}) {
1833 if ($newparentid == -1) {
1834 require BSE::Edit::Site;
1835 $newparent = BSE::TB::Site->new;
1836 $parent_editor = BSE::Edit::Site->new(cfg => $req->cfg);
1839 $newparent = $articles->getByPkey($newparentid);
1840 ($parent_editor, $newparent) = $self->article_class($newparent, $articles, $req->cfg);
1844 if ($self->can_reparent_to($article, $newparent, $parent_editor, $articles, \$msg)
1845 && $self->reparent($article, $newparentid, $articles, \$msg)) {
1846 # nothing to do here
1849 return $self->_service_error($req, $article, $articles, $msg, {}, "PARENT");
1853 return $self->_service_error($req, $article, $articles, "No such parent article", {}, "PARENT");
1857 $article->{listed} = $cgi->param('listed')
1858 if defined $cgi->param('listed') &&
1859 $req->user_can('edit_field_edit_listed', $article);
1861 if ($req->user_can('edit_field_edit_release', $article)) {
1862 my $release = $cgi->param("release");
1863 if (defined $release && $release =~ /\S/) {
1864 if ($req->is_ajax) {
1865 $article->{release} = $release;
1868 $article->{release} = sql_date($release)
1873 $article->{expire} = sql_date($cgi->param('expire')) || $Constants::D_99
1874 if defined $cgi->param('expire') &&
1875 $req->user_can('edit_field_edit_expire', $article);
1876 $article->{lastModified} = now_sqldatetime();
1877 for my $col (qw/force_dynamic inherit_siteuser_rights/) {
1878 if ($req->user_can("edit_field_edit_$col", $article)
1879 && $cgi->param("save_$col")) {
1880 $article->{$col} = $cgi->param($col) ? 1 : 0;
1885 my $user = $req->getuser;
1886 $article->{lastModifiedBy} = $user ? $user->{logon} : '';
1889 my @save_group_ids = $cgi->param('save_group_id');
1890 if ($req->user_can('edit_field_edit_group_id')
1891 && @save_group_ids) {
1892 require BSE::TB::SiteUserGroups;
1893 my %groups = map { $_->{id} => $_ }
1894 BSE::TB::SiteUserGroups->admin_and_query_groups($self->{cfg});
1895 my %set = map { $_ => 1 } $cgi->param('group_id');
1896 my %current = map { $_ => 1 } $article->group_ids;
1898 for my $group_id (@save_group_ids) {
1899 $groups{$group_id} or next;
1900 if ($current{$group_id} && !$set{$group_id}) {
1901 $article->remove_group_id($group_id);
1903 elsif (!$current{$group_id} && $set{$group_id}) {
1904 $article->add_group_id($group_id);
1909 my $old_link = $article->{link};
1910 # this need to go last
1911 $article->update_dynamic($self->{cfg});
1912 if ($article->{link} &&
1913 !$self->{cfg}->entry('protect link', $article->{id})) {
1914 my $article_uri = $self->make_link($article);
1915 $article->setLink($article_uri);
1922 @extra_regen = $self->update_child_dynamic($article, $articles, $req);
1924 if ($article->is_dynamic || $old_dynamic) {
1925 if (!$old_dynamic and $old_link) {
1926 unlink $article->link_to_filename($self->{cfg}, $old_link);
1928 elsif (!$article->is_dynamic) {
1929 unlink $article->cached_filename($self->{cfg});
1933 my ($after_id) = $cgi->param("_after");
1934 if (defined $after_id) {
1935 Articles->reorder_child($article->{parentid}, $article->{id}, $after_id);
1936 # reload, the displayOrder probably changed
1937 $article = $articles->getByPkey($article->{id});
1940 use Util 'generate_article';
1941 if ($Constants::AUTO_GENERATE) {
1942 generate_article($articles, $article);
1943 for my $regen_id (@extra_regen) {
1944 my $regen = $articles->getByPkey($regen_id);
1945 Util::generate_low($articles, $regen, $self->{cfg});
1949 if ($req->is_ajax) {
1950 return $req->json_content
1954 article => $self->_article_data($req, $article),
1959 return $self->refresh($article, $cgi, undef, 'Article saved');
1962 sub can_reparent_to {
1963 my ($self, $article, $newparent, $parent_editor, $articles, $rmsg) = @_;
1967 my @child_types = $parent_editor->child_types;
1968 if (!grep $_ eq ref $self, @child_types) {
1969 my ($child_type) = (ref $self) =~ /(\w+)$/;
1970 my ($parent_type) = (ref $parent_editor) =~ /(\w+)$/;
1972 $$rmsg = "A $child_type cannot be a child of a $parent_type";
1976 # the article cannot become a child of itself or one of it's
1978 if ($article->{id} == $newparent->id
1979 || $self->is_descendant($article->id, $newparent->id, $articles)) {
1980 $$rmsg = "Cannot become a child of itself or of a descendant";
1984 my $shopid = $self->{cfg}->entryErr('articles', 'shop');
1985 if ($self->shop_article) { # if this article belongs in the shop
1986 unless ($newparent->id == $shopid
1987 || $self->is_descendant($shopid, $newparent->{id}, $articles)) {
1988 $$rmsg = "This article belongs in the shop";
1993 if ($newparent->id == $shopid
1994 || $self->is_descendant($shopid, $newparent->id, $articles)) {
1995 $$rmsg = "This article doesn't belong in the shop";
2003 sub shop_article { 0 }
2005 sub update_child_dynamic {
2006 my ($self, $article, $articles, $req) = @_;
2008 my $cfg = $req->cfg;
2009 my @stack = $article->children;
2012 my $workart = pop @stack;
2013 my $old_dynamic = $workart->is_dynamic; # before update
2014 my $old_link = $workart->{link};
2016 ($editor, $workart) = $self->article_class($workart, $articles, $cfg);
2018 $workart->update_dynamic($cfg);
2019 if ($old_dynamic != $workart->is_dynamic) {
2021 if ($article->{link} && !$cfg->entry('protect link', $workart->{id})) {
2022 my $uri = $editor->make_link($workart);
2023 $workart->setLink($uri);
2025 !$old_dynamic && $old_link
2026 and unlink $workart->link_to_filename($cfg, $old_link);
2027 $workart->is_dynamic
2028 or unlink $workart->cached_filename($cfg);
2031 # save dynamic cache change and link if that changed
2034 push @stack, $workart->children;
2035 push @regen, $workart->{id};
2043 my ($year, $month, $day);
2046 if (($day, $month, $year) = ($str =~ m!(\d+)/(\d+)/(\d+)!)) {
2047 $year += 2000 if $year < 100;
2049 return sprintf("%04d-%02d-%02d", $year, $month, $day);
2055 # Converts 24hr time to 12hr AM/PM time
2058 my ($hour, $minute, $second, $ampm);
2061 if (($hour, $minute, $second) = ($str =~ m!(\d+):(\d+):(\d+)!)) {
2067 $hour = 12 if $hour == 0;
2070 return sprintf("%02d:%02d:%02d $ampm", $hour, $minute, $second);
2077 my ($self, $article, $newparentid, $articles, $rmsg) = @_;
2080 if ($newparentid == -1) {
2084 my $parent = $articles->getByPkey($newparentid);
2086 $$rmsg = "Cannot get new parent article";
2089 $newlevel = $parent->{level} + 1;
2091 # the caller will save this one
2092 $article->{parentid} = $newparentid;
2093 $article->{level} = $newlevel;
2094 $article->{displayOrder} = time;
2096 my @change = ( [ $article->{id}, $newlevel ] );
2098 my $this = shift @change;
2099 my ($art, $level) = @$this;
2101 my @kids = $articles->getBy(parentid=>$art);
2102 push @change, map { [ $_->{id}, $level+1 ] } @kids;
2104 for my $kid (@kids) {
2105 $kid->{level} = $level+1;
2113 # tests if $desc is a descendant of $art
2114 # where both are article ids
2116 my ($self, $art, $desc, $articles) = @_;
2120 my $parent = shift @check;
2121 $parent == $desc and return 1;
2122 my @kids = $articles->getBy(parentid=>$parent);
2123 push @check, map $_->{id}, @kids;
2129 sub save_thumbnail {
2130 my ($self, $cgi, $original, $newdata) = @_;
2132 unless ($original) {
2133 @$newdata{qw/thumbImage thumbWidth thumbHeight/} = ('', 0, 0);
2135 my $imagedir = cfg_image_dir($self->{cfg});
2136 if ($cgi->param('remove_thumb') && $original && $original->{thumbImage}) {
2137 unlink("$imagedir/$original->{thumbImage}");
2138 @$newdata{qw/thumbImage thumbWidth thumbHeight/} = ('', 0, 0);
2140 my $image = $cgi->param('thumbnail');
2141 if ($image && -s $image) {
2142 # where to put it...
2144 $image =~ /([\w.-]+)$/ and $name = $1;
2145 my $filename = time . "_" . $name;
2149 $filename = time . '_' . $counter . '_' . $name
2150 until sysopen( OUTPUT, "$imagedir/$filename",
2151 O_WRONLY| O_CREAT| O_EXCL)
2152 || ++$counter > 100;
2154 fileno(OUTPUT) or die "Could not open image file: $!";
2160 # read the image in from the browser and output it to our
2162 print STDERR "\$image ",ref $image,"\n";
2164 print OUTPUT $buffer while sysread $image, $buffer, 1024;
2167 or die "Could not close image output file: $!";
2171 if ($original && $original->{thumbImage}) {
2172 #unlink("$imagedir/$original->{thumbImage}");
2174 @$newdata{qw/thumbWidth thumbHeight/} = imgsize("$imagedir/$filename");
2175 $newdata->{thumbImage} = $filename;
2180 my ($self, $article) = @_;
2182 my $shopid = $self->{cfg}->entryErr('articles', 'shop');
2183 if ($article && $article->{id} && $article->{id} == $shopid) {
2184 return ( 'BSE::Edit::Catalog' );
2186 return ( 'BSE::Edit::Article' );
2190 my ($self, $req, $article, $articles) = @_;
2192 $req->check_csrf("admin_add_stepkid")
2193 or return $self->csrf_error($req, $article, "admin_add_stepkid", "Add Stepkid");
2195 $req->user_can(edit_stepkid_add => $article)
2196 or return $self->edit_form($req, $article, $articles,
2197 "You don't have access to add step children to this article");
2199 my $cgi = $req->cgi;
2200 require 'BSE/Admin/StepParents.pm';
2202 my $childId = $cgi->param('stepkid');
2204 or die "No stepkid supplied to add_stepkid";
2206 or die "Invalid stepkid supplied to add_stepkid";
2207 my $child = $articles->getByPkey($childId)
2208 or die "Article $childId not found";
2210 $req->user_can(edit_stepparent_add => $child)
2211 or die "You don't have access to add a stepparent to that article\n";
2213 my $release = $cgi->param('release');
2214 dh_parse_date($release) or $release = undef;
2215 my $expire = $cgi->param('expire');
2216 dh_parse_date($expire) or $expire = undef;
2219 BSE::Admin::StepParents->add($article, $child, $release, $expire);
2222 return $self->edit_form($req, $article, $articles, $@);
2225 use Util 'generate_article';
2226 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
2228 return $self->refresh($article, $cgi, 'step', 'Stepchild added');
2232 my ($self, $req, $article, $articles) = @_;
2234 $req->check_csrf("admin_remove_stepkid")
2235 or return $self->csrf_error($req, $article, "admin_del_stepkid", "Delete Stepkid");
2236 $req->user_can(edit_stepkid_delete => $article)
2237 or return $self->edit_form($req, $article, $articles,
2238 "You don't have access to delete stepchildren from this article");
2240 my $cgi = $req->cgi;
2241 require 'BSE/Admin/StepParents.pm';
2243 my $childId = $cgi->param('stepkid');
2245 or die "No stepkid supplied to add_stepkid";
2247 or die "Invalid stepkid supplied to add_stepkid";
2248 my $child = $articles->getByPkey($childId)
2249 or die "Article $childId not found";
2251 $req->user_can(edit_stepparent_delete => $child)
2252 or die "You cannot remove stepparents from that article\n";
2254 BSE::Admin::StepParents->del($article, $child);
2258 return $self->edit_form($req, $article, $articles, $@);
2260 use Util 'generate_article';
2261 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
2263 return $self->refresh($article, $cgi, 'step', 'Stepchild deleted');
2267 my ($self, $req, $article, $articles) = @_;
2269 $req->check_csrf("admin_save_stepkids")
2270 or return $self->csrf_error($req, $article, "admin_save_stepkids", "Save Stepkids");
2272 $req->user_can(edit_stepkid_save => $article)
2273 or return $self->edit_form($req, $article, $articles,
2274 "No access to save stepkid data for this article");
2276 my $cgi = $req->cgi;
2277 require 'BSE/Admin/StepParents.pm';
2278 my @stepcats = OtherParents->getBy(parentId=>$article->{id});
2279 my %stepcats = map { $_->{parentId}, $_ } @stepcats;
2280 my %datedefs = ( release => '2000-01-01', expire=>'2999-12-31' );
2281 for my $stepcat (@stepcats) {
2282 $req->user_can(edit_stepparent_save => $stepcat->{childId})
2284 for my $name (qw/release expire/) {
2285 my $date = $cgi->param($name.'_'.$stepcat->{childId});
2286 if (defined $date) {
2288 $date = $datedefs{$name};
2290 elsif (dh_parse_date($date)) {
2291 use BSE::Util::SQL qw/date_to_sql/;
2292 $date = date_to_sql($date);
2295 return $self->refresh($article, $cgi, '', "Invalid date '$date'");
2297 $stepcat->{$name} = $date;
2303 $@ and return $self->refresh($article, $cgi, '', $@);
2305 use Util 'generate_article';
2306 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
2308 return $self->refresh($article, $cgi, 'step', 'Stepchild information saved');
2311 sub add_stepparent {
2312 my ($self, $req, $article, $articles) = @_;
2314 $req->check_csrf("admin_add_stepparent")
2315 or return $self->csrf_error($req, $article, "admin_add_stepparent", "Add Stepparent");
2317 $req->user_can(edit_stepparent_add => $article)
2318 or return $self->edit_form($req, $article, $articles,
2319 "You don't have access to add stepparents to this article");
2321 my $cgi = $req->cgi;
2322 require 'BSE/Admin/StepParents.pm';
2324 my $step_parent_id = $cgi->param('stepparent');
2325 defined($step_parent_id)
2326 or die "No stepparent supplied to add_stepparent";
2327 int($step_parent_id) eq $step_parent_id
2328 or die "Invalid stepcat supplied to add_stepcat";
2329 my $step_parent = $articles->getByPkey($step_parent_id)
2330 or die "Parent $step_parent_id not found\n";
2332 $req->user_can(edit_stepkid_add => $step_parent)
2333 or die "You don't have access to add a stepkid to that article\n";
2335 my $release = $cgi->param('release');
2337 or $release = "01/01/2000";
2338 $release eq '' or dh_parse_date($release)
2339 or die "Invalid release date";
2340 my $expire = $cgi->param('expire');
2342 or $expire = '31/12/2999';
2343 $expire eq '' or dh_parse_date($expire)
2344 or die "Invalid expire data";
2347 BSE::Admin::StepParents->add($step_parent, $article, $release, $expire);
2349 $@ and return $self->refresh($article, $cgi, 'step', $@);
2351 use Util 'generate_article';
2352 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
2354 return $self->refresh($article, $cgi, 'stepparents', 'Stepparent added');
2357 sub del_stepparent {
2358 my ($self, $req, $article, $articles) = @_;
2360 $req->check_csrf("admin_remove_stepparent")
2361 or return $self->csrf_error($req, $article, "admin_del_stepparent", "Delete Stepparent");
2363 $req->user_can(edit_stepparent_delete => $article)
2364 or return $self->edit_form($req, $article, $articles,
2365 "You cannot remove stepparents from that article");
2367 my $cgi = $req->cgi;
2368 require 'BSE/Admin/StepParents.pm';
2369 my $step_parent_id = $cgi->param('stepparent');
2370 defined($step_parent_id)
2371 or return $self->refresh($article, $cgi, 'stepparents',
2372 "No stepparent supplied to add_stepcat");
2373 int($step_parent_id) eq $step_parent_id
2374 or return $self->refresh($article, $cgi, 'stepparents',
2375 "Invalid stepparent supplied to add_stepparent");
2376 my $step_parent = $articles->getByPkey($step_parent_id)
2377 or return $self->refresh($article, $cgi, 'stepparent',
2378 "Stepparent $step_parent_id not found");
2380 $req->user_can(edit_stepkid_delete => $step_parent)
2381 or die "You don't have access to remove the stepkid from that article\n";
2384 BSE::Admin::StepParents->del($step_parent, $article);
2386 $@ and return $self->refresh($article, $cgi, 'stepparents', $@);
2388 use Util 'generate_article';
2389 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
2391 return $self->refresh($article, $cgi, 'stepparents', 'Stepparent deleted');
2394 sub save_stepparents {
2395 my ($self, $req, $article, $articles) = @_;
2397 $req->check_csrf("admin_save_stepparents")
2398 or return $self->csrf_error($req, $article, "admin_save_stepparents", "Save Stepparents");
2399 $req->user_can(edit_stepparent_save => $article)
2400 or return $self->edit_form($req, $article, $articles,
2401 "No access to save stepparent data for this artice");
2403 my $cgi = $req->cgi;
2405 require 'BSE/Admin/StepParents.pm';
2406 my @stepparents = OtherParents->getBy(childId=>$article->{id});
2407 my %stepparents = map { $_->{parentId}, $_ } @stepparents;
2408 my %datedefs = ( release => '2000-01-01', expire=>'2999-12-31' );
2409 for my $stepparent (@stepparents) {
2410 $req->user_can(edit_stepkid_save => $stepparent->{parentId})
2412 for my $name (qw/release expire/) {
2413 my $date = $cgi->param($name.'_'.$stepparent->{parentId});
2414 if (defined $date) {
2416 $date = $datedefs{$name};
2418 elsif (dh_parse_date($date)) {
2419 use BSE::Util::SQL qw/date_to_sql/;
2420 $date = date_to_sql($date);
2423 return $self->refresh($article, $cgi, "Invalid date '$date'");
2425 $stepparent->{$name} = $date;
2429 $stepparent->save();
2431 $@ and return $self->refresh($article, $cgi, '', $@);
2434 use Util 'generate_article';
2435 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
2437 return $self->refresh($article, $cgi, 'stepparents',
2438 'Stepparent information saved');
2442 my ($self, $article, $cgi, $name, $message, $extras) = @_;
2444 my $url = $cgi->param('r');
2446 if ($url !~ /[?&](m|message)=/ && $message) {
2447 # add in messages if none in the provided refresh
2448 my @msgs = ref $message ? @$message : $message;
2449 my $sep = $url =~ /\?/ ? "&" : "?";
2450 for my $msg (@msgs) {
2451 $url .= $sep . "m=" . CGI::escape($msg);
2456 my $urlbase = admin_base_url($self->{cfg});
2457 $url = "$urlbase$ENV{SCRIPT_NAME}?id=$article->{id}";
2459 my @msgs = ref $message ? @$message : $message;
2460 for my $msg (@msgs) {
2461 $url .= "&m=" . CGI::escape($msg);
2464 if ($cgi->param('_t')) {
2465 $url .= "&_t=".CGI::escape($cgi->param('_t'));
2467 $url .= $extras if defined $extras;
2468 my $cgiextras = $cgi->param('e');
2469 $url .= "#$name" if $name;
2476 my ($self, $article, $cgi, $name, $message, $extras) = @_;
2478 my $url = $self->refresh_url($article, $cgi, $name, $message, $extras);
2480 return BSE::Template->get_refresh($url, $self->{cfg});
2484 my ($self, $req, $article, $articles, $msg, $errors) = @_;
2487 %acts = $self->low_edit_tags(\%acts, $req, $article, $articles, $msg, $errors);
2488 my $template = 'admin/article_img';
2490 return $req->dyn_response($template, \%acts);
2493 sub save_image_changes {
2494 my ($self, $req, $article, $articles) = @_;
2496 $req->check_csrf("admin_save_images")
2497 or return $self->csrf_error($req, $article, "admin_save_images", "Save Images");
2499 $req->user_can(edit_images_save => $article)
2500 or return $self->edit_form($req, $article, $articles,
2501 "You don't have access to save image information for this article");
2503 my $image_dir = cfg_image_dir($req->cfg);
2505 my $cgi = $req->cgi;
2506 my $image_pos = $cgi->param('imagePos');
2508 && $image_pos =~ /^(?:tl|tr|bl|br)$/
2509 && $image_pos ne $article->{imagePos}) {
2510 $article->{imagePos} = $image_pos;
2513 my @images = $self->get_images($article);
2516 return $self->refresh($article, $cgi, undef, 'No images to save information for');
2523 for my $image (@images) {
2524 my $id = $image->{id};
2526 my $alt = $cgi->param("alt$id");
2527 if ($alt ne $image->{alt}) {
2528 $changes{$id}{alt} = $alt;
2531 my $url = $cgi->param("url$id");
2532 if (defined $url && $url ne $image->{url}) {
2533 $changes{$id}{url} = $url;
2536 my $name = $cgi->param("name$id");
2537 if (defined $name && $name ne $image->{name}) {
2539 if ($article->{id} > 0) {
2540 $changes{$id}{name} = '';
2543 $errors{"name$id"} = "Identifiers are required for global images";
2546 elsif ($name =~ /^[a-z_]\w*$/i) {
2548 if ($self->validate_image_name($name, \$msg)) {
2549 # check for duplicates after the loop
2550 push @{$names{lc $name}}, $image->{id}
2552 $changes{$id}{name} = $name;
2555 $errors{"name$id"} = $msg;
2559 $errors{"name$id"} = 'Image name must be empty or alphanumeric and unique to the article';
2563 push @{$names{lc $image->{name}}}, $image->{id}
2564 if length $image->{name};
2567 my $filename = $cgi->param("image$id");
2568 if (defined $filename && length $filename) {
2569 my $in_fh = $cgi->upload("image$id");
2571 # work out where to put it
2572 require DevHelp::FileUpload;
2574 my ($image_name, $out_fh) = DevHelp::FileUpload->make_img_filename
2575 ($image_dir, $filename . '', \$msg);
2579 while ($data = <$in_fh>) {
2580 print $out_fh $data;
2584 my $full_filename = "$image_dir/$image_name";
2585 require Image::Size;
2586 my ($width, $height, $type) = Image::Size::imgsize($full_filename);
2590 image => $image->{image},
2591 storage => $image->{storage}
2593 push @new_images, $image_name;
2595 $changes{$id}{image} = $image_name;
2596 $changes{$id}{storage} = 'local';
2597 $changes{$id}{src} = "/images/$image_name";
2598 $changes{$id}{width} = $width;
2599 $changes{$id}{height} = $height;
2600 $changes{$id}{ftype} = $self->_image_ftype($type);
2603 $errors{"image$id"} = $type;
2607 $errors{"image$id"} = $msg;
2612 $errors{"image$id"} = "No image file received";
2616 # look for duplicate names
2617 for my $name (keys %names) {
2618 if (@{$names{$name}} > 1) {
2619 for my $id (@{$names{$name}}) {
2620 $errors{"name$id"} = 'Image name must be unique to the article';
2625 # remove files that won't be stored because validation failed
2626 unlink map "$image_dir/$_", @new_images;
2628 return $self->edit_form($req, $article, $articles, undef,
2632 my $mgr = $self->_image_manager($req->cfg);
2633 $req->flash('Image information saved');
2634 my $changes_found = 0;
2635 my $auto_store = $cgi->param('auto_storage');
2636 for my $image (@images) {
2637 my $id = $image->{id};
2639 if ($changes{$id}) {
2640 my $changes = $changes{$id};
2643 for my $field (keys %$changes) {
2644 $image->{$field} = $changes->{$field};
2649 my $old_storage = $image->{storage};
2650 my $new_storage = $auto_store ? '' : $cgi->param("storage$id");
2651 defined $new_storage or $new_storage = $image->{storage};
2652 $new_storage = $mgr->select_store($image->{image}, $new_storage, $image);
2653 if ($new_storage ne $old_storage) {
2655 $image->{src} = $mgr->store($image->{image}, $new_storage, $image);
2656 $image->{storage} = $new_storage;
2660 if ($old_storage ne 'local') {
2661 $mgr->unstore($image->{image}, $old_storage);
2666 # delete any image files that were replaced
2667 for my $old_image (values %old_images) {
2668 my ($image, $storage) = @$old_image{qw/image storage/};
2669 if ($storage ne 'local') {
2670 $mgr->unstore($image->{image}, $storage);
2672 unlink "$image_dir/$image";
2675 if ($changes_found) {
2676 use Util 'generate_article';
2677 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
2680 return $self->refresh($article, $cgi);
2683 =item _service_error
2685 This function is called on various errors.
2687 If a _service parameter was supplied, returns text like:
2693 Field-Error: I<field-name1> - I<message1>
2695 Field-Error: I<field-name2> - I<message2>
2699 If the request is detected as an ajax request or a _ parameter is
2700 supplied, return JSON like:
2702 { error: I<message> }
2704 Otherwise display the normal edit page with the error.
2708 sub _service_error {
2709 my ($self, $req, $article, $articles, $msg, $error, $code) = @_;
2713 $article = $self->_dummy_article($req, $articles, \$mymsg);
2716 map $_ => '', Article->columns
2720 if ($req->cgi->param('_service')) {
2722 $body .= "Result: failure\n";
2724 for my $field (keys %$error) {
2725 my $text = $error->{$field};
2727 $body .= "Field-Error: $field - $text\n";
2729 my $text = join ('/', values %$error);
2731 $body .= "Error: $text\n";
2734 $body .= "Error: $msg\n";
2737 $body .= "Error: $error\n";
2741 type => 'text/plain',
2745 elsif ((() = $req->cgi->param('_')) ||
2746 (exists $ENV{HTTP_X_REQUESTED_WITH}
2747 && $ENV{HTTP_X_REQUESTED_WITH} =~ /XMLHttpRequest/)) {
2754 $msg and $result->{message} = $msg;
2755 $code and $result->{error_code} = $code;
2756 my $json_result = $req->json_content($result);
2758 if (!exists $ENV{HTTP_X_REQUESTED_WITH}
2759 && $ENV{HTTP_X_REQUESTED_WITH} !~ /XMLHttpRequest/) {
2760 $json_result->{type} = "text/plain";
2763 return $json_result;
2766 return $self->edit_form($req, $article, $articles, $msg, $error);
2770 sub _service_success {
2771 my ($self, $results) = @_;
2773 my $body = "Result: success\n";
2774 for my $field (keys %$results) {
2775 $body .= "$field: $results->{$field}\n";
2779 type => 'text/plain',
2785 my ($self, $type) = @_;
2787 if ($type eq 'CWS' || $type eq 'SWF') {
2795 my ($self, $cfg, $article, $image, %opts) = @_;
2797 my $errors = $opts{errors}
2798 or die "No errors parameter";
2800 my $imageref = $opts{name};
2801 if (defined $imageref && $imageref ne '') {
2802 if ($imageref =~ /^[a-z_]\w+$/i) {
2803 # make sure it's unique
2804 my @images = $self->get_images($article);
2805 for my $img (@images) {
2806 if (defined $img->{name} && lc $img->{name} eq lc $imageref) {
2807 $errors->{name} = 'Image name must be unique to the article';
2813 $errors->{name} = 'Image name must be empty or alphanumeric beginning with an alpha character';
2819 unless ($errors->{name}) {
2821 $self->validate_image_name($imageref, \$workmsg)
2822 or $errors->{name} = $workmsg;
2827 $errors->{image} = 'Image file is empty';
2831 $errors->{image} = 'Please enter an image filename';
2836 my $imagename = $opts{filename} || $image;
2837 $imagename .= ''; # force it into a string
2839 $imagename =~ tr/ //d;
2840 $imagename =~ /([\w.-]+)$/ and $basename = $1;
2842 # create a filename that we hope is unique
2843 my $filename = time. '_'. $basename;
2845 # for the sysopen() constants
2848 my $imagedir = cfg_image_dir($cfg);
2849 # loop until we have a unique filename
2851 $filename = time. '_' . $counter . '_' . $basename
2852 until sysopen( OUTPUT, "$imagedir/$filename", O_WRONLY| O_CREAT| O_EXCL)
2853 || ++$counter > 100;
2855 fileno(OUTPUT) or die "Could not open image file: $!";
2857 # for OSs with special text line endings
2864 # read the image in from the browser and output it to our output filehandle
2865 print OUTPUT $buffer while read $image, $buffer, 1024;
2869 or die "Could not close image file $filename: $!";
2874 my($width,$height, $type) = imgsize("$imagedir/$filename");
2876 my $alt = $opts{alt};
2877 defined $alt or $alt = '';
2878 my $url = $opts{url};
2879 defined $url or $url = '';
2882 articleId => $article->{id},
2891 src => '/images/' . $filename,
2892 ftype => $self->_image_ftype($type),
2894 require BSE::TB::Images;
2895 my @cols = BSE::TB::Image->columns;
2897 my $imageobj = BSE::TB::Images->add(@image{@cols});
2899 my $storage = $opts{storage};
2900 defined $storage or $storage = 'local';
2901 my $image_manager = $self->_image_manager($cfg);
2902 local $SIG{__DIE__};
2905 $storage = $image_manager->select_store($filename, $storage, $imageobj);
2906 $src = $image_manager->store($filename, $storage, $imageobj);
2909 $imageobj->{src} = $src;
2910 $imageobj->{storage} = $storage;
2915 $errors->{flash} = $@;
2922 my ($self, $cfg, $image) = @_;
2924 my $data = $image->data_only;
2925 $data->{src} = $image->image_url($cfg);
2931 my ($self, $req, $article, $articles) = @_;
2933 $req->check_csrf("admin_add_image")
2934 or return $self->csrf_error($req, $article, "admin_add_image", "Add Image");
2935 $req->user_can(edit_images_add => $article)
2936 or return $self->_service_error($req, $article, $articles,
2937 "You don't have access to add new images to this article");
2939 my $cgi = $req->cgi;
2946 scalar($cgi->param('image')),
2947 name => scalar($cgi->param('name')),
2948 alt => scalar($cgi->param('altIn')),
2949 url => scalar($cgi->param('url')),
2950 storage => scalar($cgi->param('storage')),
2955 or return $self->_service_error($req, $article, $articles, undef, \%errors);
2957 # typically a soft failure from the storage
2959 and $req->flash($errors{flash});
2961 use Util 'generate_article';
2962 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
2964 if ($cgi->param('_service')) {
2965 return $self->_service_success
2968 image => $imageobj->{id},
2972 elsif ($cgi->param("_") || $req->is_ajax) {
2973 my $resp = $req->json_content
2976 image => $self->_image_data($req->cfg, $imageobj),
2979 # the browser handles this directly, tell it that it's text
2980 $resp->{type} = "text/plain";
2985 return $self->refresh($article, $cgi, undef, 'New image added');
2989 sub _image_manager {
2992 require BSE::StorageMgr::Images;
2994 return BSE::StorageMgr::Images->new(cfg => $self->cfg);
2999 my ($self, $req, $article, $articles, $imageid) = @_;
3001 $req->check_csrf("admin_remove_image")
3002 or return $self->csrf_error($req, $article, "admin_remove_image", "Remove Image");
3004 $req->user_can(edit_images_delete => $article)
3005 or return $self->edit_form($req, $article, $articles,
3006 "You don't have access to delete images from this article");
3010 my @images = $self->get_images($article);
3011 my ($image) = grep $_->{id} == $imageid, @images
3012 or return $self->show_images($req, $article, $articles, "No such image");
3014 if ($image->{storage} ne 'local') {
3015 my $mgr = $self->_image_manager($req->cfg);
3016 $mgr->unstore($image->{image}, $image->{storage});
3019 my $imagedir = cfg_image_dir($req->cfg);
3020 unlink "$imagedir$image->{image}";
3023 use Util 'generate_article';
3024 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
3026 return $self->refresh($article, $req->cgi, undef, 'Image removed');
3030 my ($self, $req, $article, $articles) = @_;
3032 $req->check_csrf("admin_move_image")
3033 or return $self->csrf_error($req, $article, "admin_move_image", "Move Image");
3034 $req->user_can(edit_images_reorder => $article)
3035 or return $self->edit_form($req, $article, $articles,
3036 "You don't have access to reorder images in this article");
3038 my $imageid = $req->cgi->param('imageid');
3039 my @images = $self->get_images($article);
3040 my ($imgindex) = grep $images[$_]{id} == $imageid, 0..$#images
3041 or return $self->edit_form($req, $article, $articles, "No such image");
3043 or return $self->edit_form($req, $article, $articles, "Image is already at the top");
3044 my ($to, $from) = @images[$imgindex-1, $imgindex];
3045 ($to->{displayOrder}, $from->{displayOrder}) =
3046 ($from->{displayOrder}, $to->{displayOrder});
3050 use Util 'generate_article';
3051 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
3053 return $self->refresh($article, $req->cgi, undef, 'Image moved');
3057 my ($self, $req, $article, $articles) = @_;
3059 $req->check_csrf("admin_move_image")
3060 or return $self->csrf_error($req, $article, "admin_move_image", "Move Image");
3061 $req->user_can(edit_images_reorder => $article)
3062 or return $self->edit_form($req, $article, $articles,
3063 "You don't have access to reorder images in this article");
3065 my $imageid = $req->cgi->param('imageid');
3066 my @images = $self->get_images($article);
3067 my ($imgindex) = grep $images[$_]{id} == $imageid, 0..$#images
3068 or return $self->edit_form($req, $article, $articles, "No such image");
3069 $imgindex < $#images
3070 or return $self->edit_form($req, $article, $articles, "Image is already at the end");
3071 my ($to, $from) = @images[$imgindex+1, $imgindex];
3072 ($to->{displayOrder}, $from->{displayOrder}) =
3073 ($from->{displayOrder}, $to->{displayOrder});
3077 use Util 'generate_article';
3078 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
3080 return $self->refresh($article, $req->cgi, undef, 'Image moved');
3084 my ($self, $req, $article) = @_;
3086 my $cgi = $req->cgi;
3087 my $cfg = $req->cfg;
3088 my $im_id = $cgi->param('im');
3090 if (defined $im_id && $im_id =~ /^\d+$/) {
3091 ($image) = grep $_->{id} == $im_id, $self->get_images($article);
3093 my $thumb_obj = $self->_get_thumbs_class();
3095 if ($image && $thumb_obj) {
3096 my $geometry_id = $cgi->param('g');
3097 defined $geometry_id or $geometry_id = 'editor';
3098 my $geometry = $cfg->entry('thumb geometries', $geometry_id, 'scale(200x200)');
3099 my $imagedir = $cfg->entry('paths', 'images', $Constants::IMAGEDIR);
3102 ($data, $type) = $thumb_obj->thumb_data
3104 filename => "$imagedir/$image->{image}",
3105 geometry => $geometry,
3110 type => 'text/plain',
3111 content => 'Error: '.$error
3115 if ($type && $data) {
3122 "Content-Length: ".length($data),
3123 "Cache-Control: max-age=3600",
3128 # grab the nothumb image
3129 my $uri = $cfg->entry('editor', 'default_thumbnail', '/images/admin/nothumb.png');
3130 my $filebase = $Constants::CONTENTBASE;
3131 if (open IMG, "<$filebase/$uri") {
3133 my $data = do { local $/; <IMG> };
3135 my $type = $uri =~ /\.(\w+)$/ ? $1 : 'png';
3138 type => "image/$type",
3140 headers => [ "Content-Length: ".length($data) ],
3147 content => "<html><body>Cannot make thumb or default image</body></html>",
3153 sub req_edit_image {
3154 my ($self, $req, $article, $articles, $errors) = @_;
3156 my $cgi = $req->cgi;
3158 my $id = $cgi->param('image_id');
3160 my ($image) = grep $_->{id} == $id, $self->get_images($article)
3161 or return $self->edit_form($req, $article, $articles,
3163 $req->user_can(edit_images_save => $article)
3164 or return $self->edit_form($req, $article, $articles,
3165 "You don't have access to save image information for this article");
3170 $self->low_edit_tags(\%acts, $req, $article, $articles, undef,
3172 eimage => [ \&tag_hash, $image ],
3173 error_img => [ \&tag_error_img, $req->cfg, $errors ],
3176 return $req->response('admin/image_edit', \%acts);
3179 sub req_save_image {
3180 my ($self, $req, $article, $articles) = @_;
3182 $req->check_csrf("admin_save_image")
3183 or return $self->csrf_error($req, $article, "admin_save_image", "Save Image");
3184 my $cgi = $req->cgi;
3186 my $id = $cgi->param('image_id');
3188 my @images = $self->get_images($article);
3189 my ($image) = grep $_->{id} == $id, @images
3190 or return $self->edit_form($req, $article, $articles,
3192 $req->user_can(edit_images_save => $article)
3193 or return $self->edit_form($req, $article, $articles,
3194 "You don't have access to save image information for this article");
3196 my $image_dir = cfg_image_dir($req->cfg);
3198 my $old_storage = $image->{storage};
3202 my $alt = $cgi->param('alt');
3203 defined $alt and $image->{alt} = $alt;
3204 my $url = $cgi->param('url');
3205 defined $url and $image->{url} = $url;
3206 my @other_images = grep $_->{id} != $id, @images;
3207 my $name = $cgi->param('name');
3208 if (defined $name) {
3210 if ($name !~ /^[a-z_]\w*$/i) {
3211 $errors{name} = 'Image name must be empty or alphanumeric and unique to the article';
3213 elsif (grep $name eq $_->{name}, @other_images) {
3214 $errors{name} = 'Image name must be unique to the article';
3217 $image->{name} = $name;
3221 if ($article->{id} == -1) {
3222 $errors{name} = "Identifiers are required for global images";
3225 $image->{name} = '';
3229 my $filename = $cgi->param('image');
3230 if (defined $filename && length $filename) {
3231 my $in_fh = $cgi->upload('image');
3233 require DevHelp::FileUpload;
3235 my ($image_name, $out_fh) = DevHelp::FileUpload->make_img_filename
3236 ($image_dir, $filename . '', \$msg);
3241 while ($data = <$in_fh>) {
3242 print $out_fh $data;
3247 my $full_filename = "$image_dir/$image_name";
3248 require Image::Size;
3249 my ($width, $height, $type) = Image::Size::imgsize($full_filename);
3251 $delete_file = $image->{image};
3252 $image->{image} = $image_name;
3253 $image->{width} = $width;
3254 $image->{height} = $height;
3255 $image->{storage} = 'local'; # not on the remote store yet
3256 $image->{src} = '/images/' . $image_name;
3257 $image->{ftype} = $self->_image_ftype($type);
3260 $errors{image} = $type;
3264 $errors{image} = $msg;
3268 $errors{image} = "No image file received";
3272 and return $self->req_edit_image($req, $article, $articles, \%errors);
3274 my $new_storage = $cgi->param('storage');
3275 defined $new_storage or $new_storage = $image->{storage};
3277 my $mgr = $self->_image_manager($req->cfg);
3279 if ($old_storage ne 'local') {
3280 $mgr->unstore($delete_file, $old_storage);
3282 unlink "$image_dir/$delete_file";
3284 $req->flash("Image saved");
3287 $mgr->select_store($image->{image}, $new_storage);
3288 if ($image->{storage} ne $new_storage) {
3289 # handles both new images (which sets storage to local) and changing
3290 # the storage for old images
3291 my $old_storage = $image->{storage};
3292 my $src = $mgr->store($image->{image}, $new_storage, $image);
3293 $image->{src} = $src;
3294 $image->{storage} = $new_storage;
3298 $@ and $req->flash("There was a problem adding it to the new storage: $@");
3299 if ($image->{storage} ne $old_storage && $old_storage ne 'local') {
3301 $mgr->unstore($image->{image}, $old_storage);
3303 $@ and $req->flash("There was a problem removing if from the old storage: $@");
3306 return $self->refresh($article, $cgi);
3310 my ($self, $articles, $article) = @_;
3316 my ($self, $articles) = @_;
3321 sub _refresh_filelist {
3322 my ($self, $req, $article, $msg) = @_;
3324 return $self->refresh($article, $req->cgi, undef, $msg);
3328 my ($self, $req, $article, $articles, $msg, $errors) = @_;
3331 %acts = $self->low_edit_tags(\%acts, $req, $article, $articles, $msg, $errors);
3332 my $template = 'admin/filelist';
3334 return BSE::Template->get_response($template, $req->cfg, \%acts);
3341 maxlength => MAX_FILE_DISPLAYNAME_LENGTH,
3342 description => 'Filename'
3346 rules => 'dh_one_line',
3348 description => 'Description',
3352 description => 'Identifier',
3357 description => "Category",
3363 my ($self, $req, $article, $articles) = @_;
3365 $req->check_csrf("admin_add_file")
3366 or return $self->csrf_error($req, $article, "admin_add_file", "Add File");
3367 $req->user_can(edit_files_add => $article)
3368 or return $self->edit_form($req, $article, $articles,
3369 "You don't have access to add files to this article");
3372 my $cgi = $req->cgi;
3373 require BSE::TB::ArticleFiles;
3374 my @cols = BSE::TB::ArticleFile->columns;
3376 for my $col (@cols) {
3377 if (defined $cgi->param($col)) {
3378 $file{$col} = $cgi->param($col);
3384 $req->validate(errors => \%errors,
3385 fields => \%file_fields,
3386 section => $article->{id} == -1 ? 'Global File Validation' : 'Article File Validation');
3389 my $file = $cgi->param('file');
3391 $errors{file} = 'Please enter a filename';
3393 if ($file && -z $file) {
3394 $errors{file} = 'File is empty';
3397 $file{forSale} = 0 + exists $file{forSale};
3398 $file{articleId} = $article->{id};
3399 $file{download} = 0 + exists $file{download};
3400 $file{requireUser} = 0 + exists $file{requireUser};
3401 $file{hide_from_list} = 0 + exists $file{hide_from_list};
3402 $file{category} ||= '';
3404 defined $file{name} or $file{name} = '';
3405 if ($article->{id} == -1 && $file{name} eq '') {
3406 $errors{name} = 'Identifier is required for global files';
3408 if (!$errors{name} && length $file{name} && $file{name} !~/^\w+$/) {
3409 $errors{name} = "Identifier must be a single word";
3411 if (!$errors{name} && length $file{name}) {
3412 my @files = $self->get_files($article);
3413 if (grep lc $_->{name} eq lc $file{name}, @files) {
3414 $errors{name} = "Duplicate file identifier $file{name}";
3419 and return $self->edit_form($req, $article, $articles, undef, \%errors);
3422 my $workfile = $file;
3423 $workfile =~ s![^\w.:/\\-]+!_!g;
3424 $workfile =~ tr/_/_/s;
3425 $workfile =~ /([ \w.-]+)$/ and $basename = $1;
3426 $basename =~ tr/ /_/;
3427 $file{displayName} = $basename;
3428 $file{file} = $file;
3430 local $SIG{__DIE__};
3433 $article->add_file($self->cfg, %file);
3437 or return $self->edit_form($req, $article, $articles, $@);
3439 $req->flash("New file added");
3441 my $storage = $cgi->param("storage") || "";
3445 $article->apply_storage($self->cfg, $fileobj, $storage, \$msg);
3447 $msg and $req->flash($msg);
3450 and $req->flash($@);
3452 # my $downloadPath = $self->{cfg}->entryVar('paths', 'downloads');
3455 # unless ($file{contentType}) {
3456 # unless ($file =~ /\.([^.]+)$/) {
3457 # $file{contentType} = "application/octet-stream";
3459 # unless ($file{contentType}) {
3460 # $file{contentType} = content_type($self->cfg, $file);
3465 # # if the user supplies a really long filename, it can overflow the
3468 # my $work_filename = $basename;
3469 # if (length $work_filename > 60) {
3470 # $work_filename = substr($work_filename, -60);
3473 # my $filename = time. '_'. $work_filename;
3475 # # for the sysopen() constants
3478 # # loop until we have a unique filename
3480 # $filename = time. '_' . $counter . '_' . $work_filename
3481 # until sysopen( OUTPUT, "$downloadPath/$filename",
3482 # O_WRONLY| O_CREAT| O_EXCL)
3483 # || ++$counter > 100;
3485 # fileno(OUTPUT) or die "Could not open file: $!";
3487 # # for OSs with special text line endings
3494 # # read the image in from the browser and output it to our output filehandle
3495 # print OUTPUT $buffer while read $file, $buffer, 8192;
3499 # or die "Could not close file $filename: $!";
3501 # use BSE::Util::SQL qw/now_datetime/;
3502 # $file{filename} = $filename;
3503 # $file{displayName} = $basename;
3504 # $file{sizeInBytes} = -s $file;
3505 # $file{displayOrder} = time;
3506 # $file{whenUploaded} = now_datetime();
3507 # $file{storage} = 'local';
3509 # $file{file_handler} = "";
3511 # require BSE::TB::ArticleFiles;
3512 # my $fileobj = BSE::TB::ArticleFiles->add(@file{@cols});
3514 # my $storage = $cgi->param('storage');
3515 # defined $storage or $storage = 'local';
3516 # my $file_manager = $self->_file_manager($req->cfg);
3518 # local $SIG{__DIE__};
3521 # $storage = $self->_select_filestore($req, $file_manager, $storage, $fileobj);
3522 # $src = $file_manager->store($filename, $storage, $fileobj);
3525 # $fileobj->{src} = $src;
3526 # $fileobj->{storage} = $storage;
3534 # $fileobj->set_handler($req->cfg);
3537 use Util 'generate_article';
3538 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
3540 $self->_refresh_filelist($req, $article);
3544 my ($self, $req, $article, $articles) = @_;
3546 $req->check_csrf("admin_move_file")
3547 or return $self->csrf_error($req, $article, "admin_move_file", "Move File");
3549 $req->user_can('edit_files_reorder', $article)
3550 or return $self->edit_form($req, $article, $articles,
3551 "You don't have access to reorder files in this article");
3553 my $cgi = $req->cgi;
3554 my $id1 = $cgi->param('file1');
3555 my $id2 = $cgi->param('file2');
3558 my @files = $self->get_files($article);
3560 my ($file1) = grep $_->{id} == $id1, @files;
3561 my ($file2) = grep $_->{id} == $id2, @files;
3563 if ($file1 && $file2) {
3564 ($file1->{displayOrder}, $file2->{displayOrder})
3565 = ($file2->{displayOrder}, $file1->{displayOrder});
3571 use Util 'generate_article';
3572 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
3574 $self->refresh($article, $req->cgi, undef, 'File moved');
3578 my ($self, $req, $article, $articles) = @_;
3580 $req->check_csrf("admin_remove_file")
3581 or return $self->csrf_error($req, $article, "admin_remove_file", "Delete File");
3582 $req->user_can('edit_files_delete', $article)
3583 or return $self->edit_form($req, $article, $articles,
3584 "You don't have access to delete files from this article");
3586 my $cgi = $req->cgi;
3587 my $fileid = $cgi->param('file');
3589 my @files = $self->get_files($article);
3591 my ($file) = grep $_->{id} == $fileid, @files;
3594 if ($file->{storage} ne 'local') {
3595 my $mgr = $self->_file_manager($self->cfg);
3596 $mgr->unstore($file->{filename}, $file->{storage});
3599 $file->remove($req->cfg);
3603 use Util 'generate_article';
3604 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
3606 $self->_refresh_filelist($req, $article, 'File deleted');
3610 my ($self, $req, $article, $articles) = @_;
3612 $req->check_csrf("admin_save_files")
3613 or return $self->csrf_error($req, $article, "admin_save_files", "Save Files");
3615 $req->user_can('edit_files_save', $article)
3616 or return $self->edit_form($req, $article, $articles,
3617 "You don't have access to save file information for this article");
3618 my @files = $self->get_files($article);
3620 my $download_path = BSE::TB::ArticleFiles->download_path($self->{cfg});
3622 my $cgi = $req->cgi;
3628 my @content_changed;
3629 for my $file (@files) {
3630 my $id = $file->{id};
3631 my $desc = $cgi->param("description_$id");
3632 defined $desc and $file->{description} = $desc;
3633 my $type = $cgi->param("contentType_$id");
3634 if (defined $type and $type ne $file->{contentType}) {
3635 ++$store_anyway{$id};
3636 $file->{contentType} = $type;
3638 my $notes = $cgi->param("notes_$id");
3639 defined $notes and $file->{notes} = $notes;
3640 my $category = $cgi->param("category_$id");
3641 defined $category and $file->{category} = $category;
3642 my $name = $cgi->param("name_$id");
3643 if (defined $name) {
3644 $file->{name} = $name;
3646 if ($name =~ /^\w+$/) {
3647 push @{$names{$name}}, $id;
3650 $errors{"name_$id"} = "Invalid file identifier $name";
3653 elsif ($article->{id} == -1) {
3654 $errors{"name_$id"} = "Identifier is required for global files";
3658 push @{$names{$file->{name}}}, $id
3659 if length $file->{name};
3661 if ($cgi->param('save_file_flags')) {
3662 my $download = 0 + defined $cgi->param("download_$id");
3663 if ($download != $file->{download}) {
3664 ++$store_anyway{$file->{id}};
3665 $file->{download} = $download;
3667 $file->{forSale} = 0 + defined $cgi->param("forSale_$id");
3668 $file->{requireUser} = 0 + defined $cgi->param("requireUser_$id");
3669 $file->{hide_from_list} = 0 + defined $cgi->param("hide_from_list_$id");
3672 my $filex = $cgi->param("file_$id");
3673 my $in_fh = $cgi->upload("file_$id");
3674 if (defined $filex && length $filex) {
3675 if (length $filex <= MAX_FILE_DISPLAYNAME_LENGTH) {
3678 require DevHelp::FileUpload;
3680 my ($file_name, $out_fh) = DevHelp::FileUpload->make_img_filename
3681 ($download_path, $filex . '', \$msg);
3686 while ($data = <$in_fh>) {
3687 print $out_fh $data;
3691 my $display_name = $filex;
3692 $display_name =~ s!.*[\\:/]!!;
3693 $display_name =~ s/[^\w._-]+/_/g;
3694 my $full_name = "$download_path/$file_name";
3695 push @old_files, [ $file->{filename}, $file->{storage} ];
3696 push @new_files, $file_name;
3698 $file->{filename} = $file_name;
3699 $file->{storage} = 'local';
3700 $file->{sizeInBytes} = -s $full_name;
3701 $file->{whenUploaded} = now_datetime();
3702 $file->{displayName} = $display_name;
3703 push @content_changed, $file;
3706 $errors{"file_$id"} = $msg;
3710 $errors{"file_$id"} = "File is empty";
3714 $errors{"file_$id"} = "No file data received";
3718 $errors{"file_$id"} = "Filename too long";
3722 for my $name (keys %names) {
3723 if (@{$names{$name}} > 1) {
3724 for my $id (@{$names{$name}}) {
3725 $errors{"name_$id"} = 'File identifier must be unique to the article';
3730 # remove the uploaded replacements
3731 unlink map "$download_path/$_", @new_files;
3733 return $self->edit_form($req, $article, $articles, undef, \%errors);
3735 $req->flash('File information saved');
3736 my $mgr = $self->_file_manager($self->cfg);
3737 for my $file (@files) {
3740 my $storage = $cgi->param("storage_$file->{id}");
3741 defined $storage or $storage = 'local';
3743 $storage = $article->select_filestore($mgr, $file, $storage, \$msg);
3744 $msg and $req->flash($msg);
3745 if ($storage ne $file->{storage} || $store_anyway{$file->{id}}) {
3746 my $old_storage = $file->{storage};
3748 $file->{src} = $mgr->store($file->{filename}, $storage, $file);
3749 $file->{storage} = $storage;
3752 if ($old_storage ne $storage) {
3753 $mgr->unstore($file->{filename}, $old_storage);
3757 and $req->flash("Could not move $file->{displayName} to $storage: $@");
3761 # remove the replaced files
3762 for my $file (@old_files) {
3763 my ($filename, $storage) = @$file;
3766 $mgr->unstore($filename, $storage);
3769 and $req->flash("Error removing $filename from $storage: $@");
3771 unlink "$download_path/$filename";
3774 # update file type metadatas
3775 for my $file (@content_changed) {
3776 $file->set_handler($self->{cfg});
3780 use Util 'generate_article';
3781 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
3783 $self->_refresh_filelist($req, $article);
3787 my ($self, $req, $article, $articles, $errors) = @_;
3789 my $cgi = $req->cgi;
3791 my $id = $cgi->param('file_id');
3793 my ($file) = grep $_->{id} == $id, $self->get_files($article)
3794 or return $self->edit_form($req, $article, $articles,
3796 $req->user_can(edit_files_save => $article)
3797 or return $self->edit_form($req, $article, $articles,
3798 "You don't have access to save file information for this article");
3800 my $name = $cgi->param('name');
3801 $name && $name =~ /^\w+$/
3802 or return $self->edit_form($req, $article, $articles,
3803 "Missing or invalid metadata name");
3805 my $meta = $file->meta_by_name($name)
3806 or return $self->edit_form($req, $article, $articles,
3807 "Metadata $name not defined for this file");
3811 type => $meta->content_type,
3812 content => $meta->value,
3816 sub tag_old_checked {
3817 my ($errors, $cgi, $file, $key) = @_;
3819 return $errors ? $cgi->param($key) : $file->{$key};
3822 sub tag_filemeta_value {
3823 my ($file, $args, $acts, $funcname, $templater) = @_;
3825 my ($name) = DevHelp::Tags->get_parms($args, $acts, $templater)
3826 or return "* no meta name supplied *";
3828 my $meta = $file->meta_by_name($name)
3831 $meta->content_type eq "text/plain"
3832 or return "* $name has type " . $meta->content_type . " and cannot be displayed inline *";
3834 return escape_html($meta->value);
3837 sub tag_ifFilemeta_set {
3838 my ($file, $args, $acts, $funcname, $templater) = @_;
3840 my ($name) = DevHelp::Tags->get_parms($args, $acts, $templater)
3841 or return "* no meta name supplied *";
3843 my $meta = $file->meta_by_name($name)
3849 sub tag_filemeta_source {
3850 my ($file, $args, $acts, $funcname, $templater) = @_;
3852 my ($name) = DevHelp::Tags->get_parms($args, $acts, $templater)
3853 or return "* no meta name supplied *";
3855 return "$ENV{SCRIPT_NAME}?a_filemeta=1&id=$file->{articleId}&file_id=$file->{id}&name=$name";
3858 sub tag_filemeta_select {
3859 my ($cgi, $allmeta, $rcurr_meta, $file, $args, $acts, $funcname, $templater) = @_;
3862 if ($args =~ /\S/) {
3863 my ($name) = DevHelp::Tags->get_parms($args, $acts, $templater)
3864 or return "* cannot parse *";
3865 ($meta) = grep $_->name eq $name, @$allmeta
3866 or return "* cannot find meta field *";
3868 elsif ($$rcurr_meta) {
3869 $meta = $$rcurr_meta;
3872 return "* use in filemeta iterator or supply a name *";
3875 $meta->type eq "enum"
3876 or return "* can only use filemeta_select on enum metafields *";
3879 my @values = $meta->values;
3880 @labels{@values} = $meta->labels;
3882 my $field_name = "meta_" . $meta->name;
3883 my ($def) = $cgi->param($field_name);
3884 unless (defined $def) {
3885 my $value = $file->meta_by_name($meta->name);
3886 if ($value && $value->is_text) {
3887 $def = $value->value;
3890 defined $def or $def = $values[0];
3894 -name => $field_name,
3895 -values => \@values,
3896 -labels => \%labels,
3901 sub tag_filemeta_select_label {
3902 my ($allmeta, $rcurr_meta, $file, $args, $acts, $funcname, $templater) = @_;
3905 if ($args =~ /\S/) {
3906 my ($name) = DevHelp::Tags->get_parms($args, $acts, $templater)
3907 or return "* cannot parse *";
3908 ($meta) = grep $_->name eq $name, @$allmeta
3909 or return "* cannot find meta field *";
3911 elsif ($$rcurr_meta) {
3912 $meta = $$rcurr_meta;
3915 return "* use in filemeta iterator or supply a name *";
3918 $meta->type eq "enum"
3919 or return "* can only use filemeta_select_label on enum metafields *";
3922 my @values = $meta->values;
3923 @labels{@values} = $meta->labels;
3925 my $field_name = "meta_" . $meta->name;
3926 my $value = $file->meta_by_name($meta->name);
3928 if ($value->is_text) {
3929 if (exists $labels{$value->value}) {
3930 return escape_html($labels{$value->value});
3933 return escape_html($value->value);
3937 return "* cannot display type " . $value->content_type . " inline *";
3941 return "* " . $meta->name . " not set *";
3946 my ($self, $req, $article, $articles, $errors) = @_;
3948 my $cgi = $req->cgi;
3950 my $id = $cgi->param('file_id');
3952 my ($file) = grep $_->{id} == $id, $self->get_files($article)
3953 or return $self->edit_form($req, $article, $articles,
3955 $req->user_can(edit_files_save => $article)
3956 or return $self->edit_form($req, $article, $articles,
3957 "You don't have access to save file information for this article");
3959 my @metafields = $file->metafields($self->cfg);
3961 my $it = BSE::Util::Iterate->new;
3966 $self->low_edit_tags(\%acts, $req, $article, $articles, undef,
3968 efile => [ \&tag_hash, $file ],
3969 error_img => [ \&tag_error_img, $req->cfg, $errors ],
3971 [ \&tag_old_checked, $errors, $cgi, $file ],
3974 plural => "filemetas",
3975 single => "filemeta",
3976 data => \@metafields,
3977 store => \$current_meta,
3980 [ \&tag_filemeta_value, $file ],
3982 [ \&tag_ifFilemeta_set, $file ],
3984 [ \&tag_filemeta_source, $file ],
3986 [ \&tag_filemeta_select, $cgi, \@metafields, \$current_meta, $file ],
3987 filemeta_select_label =>
3988 [ \&tag_filemeta_select_label, \@metafields, \$current_meta, $file ],
3991 return $req->response('admin/file_edit', \%acts);
3995 my ($self, $req, $article, $articles) = @_;
3997 $req->check_csrf("admin_save_file")
3998 or return $self->csrf_error($req, $article, "admin_save_file", "Save File");
4000 my $cgi = $req->cgi;
4002 my @files = $self->get_files($article);
4004 my $id = $cgi->param('file_id');
4006 my ($file) = grep $_->{id} == $id, @files
4007 or return $self->edit_form($req, $article, $articles,
4009 $req->user_can(edit_files_save => $article)
4010 or return $self->edit_form($req, $article, $articles,
4011 "You don't have access to save file information for this article");
4012 my @other_files = grep $_->{id} != $id, @files;
4014 my $download_path = BSE::TB::ArticleFiles->download_path($self->{cfg});
4018 $req->validate(errors => \%errors,
4019 fields => \%file_fields,
4020 section => $article->{id} == -1 ? 'Global File Validation' : 'Article File Validation');
4022 my $store_anyway = 0;
4023 my $desc = $cgi->param("description");
4024 defined $desc and $file->{description} = $desc;
4025 my $type = $cgi->param("contentType");
4026 if (defined $type && $file->{contentType} ne $type) {
4028 $file->{contentType} = $type;
4030 my $notes = $cgi->param("notes");
4031 defined $notes and $file->{notes} = $notes;
4032 my $name = $cgi->param("name");
4033 if (defined $name) {
4034 $file->{name} = $name;
4036 if ($name =~ /^\w+$/) {
4037 if (grep lc $name eq lc $_->{name}, @other_files) {
4038 $errors{name} = 'File identifier must be unique to the article';
4042 $errors{name} = "Invalid file identifier $name";
4045 if (!$errors{name} && $article->{id} == -1) {
4047 or $errors{name} = "Identifier is required for global files";
4053 my @metafields = grep !$_->ro, $file->metafields($self->cfg);
4054 my %current_meta = map { $_ => 1 } $file->metanames;
4055 for my $meta (@metafields) {
4056 my $name = $meta->name;
4057 my $cgi_name = "meta_$name";
4058 if ($cgi->param("delete_$cgi_name")) {
4059 for my $metaname ($meta->metanames) {
4060 push @meta_delete, $metaname
4061 if $current_meta{$metaname};
4066 if ($meta->is_text) {
4067 my ($value) = $cgi->param($cgi_name);
4068 if (defined $value &&
4069 ($value =~ /\S/ || $current_meta{$meta->name})) {
4071 if ($meta->validate(value => $value, error => \$error)) {
4079 $errors{$cgi_name} = $error;
4084 my $im = $cgi->param($cgi_name);
4085 my $up = $cgi->upload($cgi_name);
4086 if (defined $im && $up) {
4087 my $data = do { local $/; <$up> };
4088 my ($width, $height, $type) = imgsize(\$data);
4090 if ($width && $height) {
4094 name => $meta->data_name,
4096 content_type => "image/\L$type",
4099 name => $meta->width_name,
4103 name => $meta->height_name,
4109 $errors{$cgi_name} = $type;
4116 if ($cgi->param('save_file_flags')) {
4117 my $download = 0 + defined $cgi->param("download");
4118 if ($download ne $file->{download}) {
4120 $file->{download} = $download;
4122 $file->{forSale} = 0 + defined $cgi->param("forSale");
4123 $file->{requireUser} = 0 + defined $cgi->param("requireUser");
4124 $file->{hide_from_list} = 0 + defined $cgi->param("hide_from_list");
4129 my $filex = $cgi->param("file");
4130 my $in_fh = $cgi->upload("file");
4131 if (defined $filex && length $filex) {
4134 require DevHelp::FileUpload;
4136 my ($file_name, $out_fh) = DevHelp::FileUpload->make_img_filename
4137 ($download_path, $filex . '', \$msg);
4142 while ($data = <$in_fh>) {
4143 print $out_fh $data;
4147 my $display_name = $filex;
4148 $display_name =~ s!.*[\\:/]!!;
4149 $display_name =~ s/[^\w._-]+/_/g;
4150 my $full_name = "$download_path/$file_name";
4151 @old_file = ( $file->{filename}, $file->{storage} );
4152 push @new_files, $file_name;
4154 $file->{filename} = $file_name;
4155 $file->{sizeInBytes} = -s $full_name;
4156 $file->{whenUploaded} = now_datetime();
4157 $file->{displayName} = $display_name;
4158 $file->{storage} = 'local';
4161 $errors{"file"} = $msg;
4165 $errors{"file"} = "File is empty";
4169 $errors{"file"} = "No file data received";
4174 # remove the uploaded replacements
4175 unlink map "$download_path/$_", @new_files;
4177 return $self->req_edit_file($req, $article, $articles, \%errors);
4181 $file->set_handler($self->cfg);
4184 $req->flash('File information saved');
4185 my $mgr = $self->_file_manager($self->cfg);
4187 my $storage = $cgi->param('storage');
4188 defined $storage or $storage = $file->{storage};
4190 $storage = $article->select_filestore($mgr, $file, $storage, \$msg);
4191 $msg and $req->flash($msg);
4192 if ($storage ne $file->{storage} || $store_anyway) {
4193 my $old_storage = $file->{storage};
4195 $file->{src} = $mgr->store($file->{filename}, $storage, $file);
4196 $file->{storage} = $storage;
4199 $mgr->unstore($file->{filename}, $old_storage)
4200 if $old_storage ne $storage;
4203 and $req->flash("Could not move $file->{displayName} to $storage: $@");
4206 for my $meta_delete (@meta_delete, map $_->{name}, @meta) {
4207 $file->delete_meta_by_name($meta_delete);
4209 for my $meta (@meta) {
4210 $file->add_meta(%$meta, appdata => 1);
4213 # remove the replaced files
4214 if (my ($old_name, $old_storage) = @old_file) {
4215 $mgr->unstore($old_name, $old_storage);
4216 unlink "$download_path/$old_name";
4219 use Util 'generate_article';
4220 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
4222 $self->_refresh_filelist($req, $article);
4226 my ($self, $req, $article, $articles, $rmsg, $rcode) = @_;
4228 unless ($req->user_can('edit_delete_article', $article, $rmsg)) {
4229 $$rmsg ||= "Access denied";
4234 if ($articles->children($article->{id})) {
4235 $$rmsg = "This article has children. You must delete the children first (or change their parents)";
4236 $$rcode = "CHILDREN";
4239 if (grep $_ == $article->{id}, @Constants::NO_DELETE) {
4240 $$rmsg = "Sorry, these pages are essential to the site structure - they cannot be deleted";
4241 $$rcode = "ESSENTIAL";
4244 if ($article->{id} == $Constants::SHOPID) {
4245 $$rmsg = "Sorry, these pages are essential to the store - they cannot be deleted - you may want to hide the store instead.";
4261 ACCESS - access denied
4265 CHILDREN - the article has children
4269 ESSENTIAL - the article is marked essential
4273 SHOP - the article is an essential part of the shop (the shop article
4278 JSON success response: { success: 1, article_id: I<id> }
4283 my ($self, $req, $article, $articles) = @_;
4285 $req->check_csrf("admin_remove_article")
4286 or return $self->csrf_error($req, $article, "admin_remove_article", "Remove Article");
4290 unless ($self->can_remove($req, $article, $articles, \$why_not, \$code)) {
4291 return $self->_service_error($req, $article, $articles, $why_not, {}, $code);
4294 my $id = $article->id;
4296 my $parentid = $article->{parentid};
4297 $article->remove($req->cfg);
4299 if ($req->is_ajax) {
4300 return $req->json_content
4307 my $url = $req->cgi->param('r');
4309 my $urlbase = admin_base_url($req->cfg);
4310 $url = "$urlbase$ENV{SCRIPT_NAME}?id=$parentid";
4311 $url .= "&message=Article+deleted";
4313 return BSE::Template->get_refresh($url, $self->{cfg});
4317 my ($self, $req, $article, $articles) = @_;
4319 $req->check_csrf("admin_save_article")
4320 or return $self->csrf_error($req, $article, "admin_save_article", "Unhide article");
4322 if ($req->user_can(edit_field_edit_listed => $article)
4323 && $req->user_can(edit_save => $article)) {
4324 $article->{listed} = 1;
4327 use Util 'generate_article';
4328 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
4330 return $self->refresh($article, $req->cgi, undef, 'Article unhidden');
4334 my ($self, $req, $article, $articles) = @_;
4336 $req->check_csrf("admin_save_article")
4337 or return $self->csrf_error($req, $article, "admin_save_article", "Hide article");
4339 if ($req->user_can(edit_field_edit_listed => $article)
4340 && $req->user_can(edit_save => $article)) {
4341 $article->{listed} = 0;
4344 use Util 'generate_article';
4345 generate_article($articles, $article) if $Constants::AUTO_GENERATE;
4347 my $r = $req->cgi->param('r');
4349 $r = admin_base_url($req->cfg)
4350 . "/cgi-bin/admin/add.pl?id=" . $article->{parentid};
4352 return $self->refresh($article, $req->cgi, undef, 'Article hidden');
4359 expire => $Constants::D_99,
4362 body => '<maximum of 64Kb>',
4364 inherit_siteuser_rights => 1,
4373 my ($self, $req, $article, $col) = @_;
4375 if ($article->{parentid}) {
4376 my $section = "children of $article->{parentid}";
4377 my $value = $req->cfg->entry($section, $col);
4378 if (defined $value) {
4382 my $section = "level $article->{level}";
4383 my $value = $req->cfg->entry($section, $col);
4384 defined($value) and return $value;
4386 $value = $self->type_default_value($req, $col);
4387 defined $value and return $value;
4389 exists $defaults{$col} and return $defaults{$col};
4391 $col eq 'release' and return now_sqldate();
4393 if ($col eq 'threshold') {
4394 my $parent = defined $article->{parentid} && $article->{parentid} != -1
4395 && Articles->getByPkey($article->{parentid});
4397 $parent and return $parent->{threshold};
4402 if ($col eq 'summaryLength') {
4403 my $parent = defined $article->{parentid} && $article->{parentid} != -1
4404 && Articles->getByPkey($article->{parentid});
4406 $parent and return $parent->{summaryLength};
4414 sub type_default_value {
4415 my ($self, $req, $col) = @_;
4417 return $req->cfg->entry('article defaults', $col);
4421 return ( 'article flags' );
4427 my $cfg = $self->{cfg};
4429 my @sections = $self->flag_sections;
4431 my %flags = map $cfg->entriesCS($_), reverse @sections;
4432 my @valid = grep /^\w$/, keys %flags;
4434 return map +{ id => $_, desc => $flags{$_} },
4435 sort { lc($flags{$a}) cmp lc($flags{$b}) }@valid;
4439 my ($self, $article) = @_;
4444 sub validate_image_name {
4445 my ($self, $name, $rmsg) = @_;
4447 1; # no extra validation
4451 my ($self, $req, $article, $articles, @extras) = @_;
4453 my $field_name = $req->cgi->param('field');
4454 unless ($field_name && exists $article->{$field_name}) {
4455 print STDERR "req_ajax_get: missing or invalid field parameter\n";
4457 content => 'Invalid or missing field name',
4459 "Status: 187" # bad request
4464 my $value = $article->{$field_name};
4465 defined $value or $value = '';
4467 my $charset = $req->cfg->entry('html', 'charset', 'iso-8859-1');
4471 Encode::from_to($value, $charset, 'utf8');
4477 type => 'text/plain; charset=utf-8',
4481 sub req_ajax_save_body {
4482 my ($self, $req, $article, $articles, @extras) = @_;
4484 my $cfg = $req->cfg;
4485 my $cgi = $req->cgi;
4487 unless ($req->user_can("edit_save", $article)
4488 && $req->user_can("edit_field_edit_body", $article)) {
4490 content => "Access denied to body",
4492 "Status: 187" # bad request
4498 # ajax always sends in UTF-8
4499 my $body = Encode::decode(utf8 => $cgi->param('body'));
4501 my $charset = $req->cfg->entry('html', 'charset', 'iso-8859-1');
4503 # convert it to our working charset
4504 # any characters that don't convert are replaced by some
4505 # substitution character, not defined by the documentation
4506 $body = Encode::encode($charset, $body);
4508 $article->{body} = $body;
4509 $article->{lastModified} = now_sqldatetime();
4510 my $user = $req->getuser;
4511 $article->{lastModifiedBy} = $user ? $user->{logon} : '';
4515 @extra_regen = $self->update_child_dynamic($article, $articles, $req);
4517 if ($Constants::AUTO_GENERATE) {
4519 Util::generate_article($articles, $article);
4520 for my $regen_id (@extra_regen) {
4521 my $regen = $articles->getByPkey($regen_id);
4522 Util::generate_low($articles, $regen, $self->{cfg});
4526 # we need the formatted body as the result
4527 my $genname = $article->{generator};
4528 eval "use $genname";
4529 $@ and die "Error on use $genname: $@";
4530 my $gen = $genname->new(article => $articles, cfg => $cfg, top => $article);
4532 %acts = $gen->baseActs($articles, \%acts, $article, 0);
4533 my $template = "<:body:>";
4534 my $formatted = BSE::Template->replace($template, $req->cfg, \%acts);
4538 content => $formatted,
4539 type => BSE::Template->html_type($cfg),
4543 sub iter_file_metas {
4544 my ($self, $files, $rfile_index) = @_;
4546 $$rfile_index < 0 || $$rfile_index >= @$files
4549 my $file = $files->[$$rfile_index];
4551 return $file->text_metadata;
4554 my %settable_fields = qw(title keyword author pageTitle);
4558 my ($self, $req, $article, $articles, @extras) = @_;
4560 my $cfg = $req->cfg;
4561 my $cgi = $req->cgi;
4563 my $field = $cgi->param('field');
4565 unless ($field && $settable_fields{$field}) {
4567 content => 'Invalid or missing field name',
4569 "Status: 187" # bad request
4573 unless ($req->user_can("edit_save", $article)
4574 && $req->user_can("edit_field_edit_$field", $article)) {
4576 content => "Access denied to $field",
4578 "Status: 187" # bad request
4584 # ajax always sends in UTF-8
4585 my $value = Encode::decode(utf8 => $cgi->param('value'));
4587 # hack - validate it if it's the title
4588 if ($field eq 'title') {
4589 if ($value !~ /\S/) {
4591 content => 'Invelid or missing field name',
4593 "Status: 187" # bad request
4599 my $charset = $req->cfg->entry('html', 'charset', 'iso-8859-1');
4601 # convert it to our working charset
4602 # any characters that don't convert are replaced by some
4603 # substitution character, not defined by the documentation
4604 $value = Encode::encode($charset, $value);
4606 $article->{$field} = $value;
4607 $article->{lastModified} = now_sqldatetime();
4608 my $user = $req->getuser;
4609 $article->{lastModifiedBy} = $user ? $user->{logon} : '';
4613 @extra_regen = $self->update_child_dynamic($article, $articles, $req);
4615 if ($Constants::AUTO_GENERATE) {
4617 Util::generate_article($articles, $article);
4618 for my $regen_id (@extra_regen) {
4619 my $regen = $articles->getByPkey($regen_id);
4620 Util::generate_low($articles, $regen, $self->{cfg});
4627 type => BSE::Template->html_type($cfg),
4632 my ($self, $req, $article, $name, $description) = @_;
4635 my $msg = $req->csrf_error;
4636 $errors{_csrfp} = $msg;
4638 $article ||= $self->_dummy_article($req, 'Articles', \$mymsg);
4640 require BSE::Edit::Site;
4641 my $site = BSE::Edit::Site->new(cfg=>$req->cfg, db=> BSE::DB->single);
4642 return $site->edit_sections($req, 'Articles', $mymsg);
4644 return $self->_service_error($req, $article, 'Articles', $msg, \%errors);
4649 Returns the csrf token for a given action.
4651 Must only be callable from Ajax requests.
4653 In general Ajax requests won't require a token, but some types of
4654 requests initiated by an Ajax based client might need a token, in
4655 particular: file uploads.
4660 my ($self, $req, $article, $articles) = @_;
4663 or return $self->_service_error($req, $article, $articles,
4664 "Only usable from Ajax", undef, "NOTAJAX");
4666 $ENV{REQUEST_METHOD} eq 'POST'
4667 or return $self->_service_error($req, $article, "Articles",
4668 "POST required for this action", {}, "NOTPOST");
4671 my (@names) = $req->cgi->param("name");
4672 @names or $errors{name} = "Missing parameter 'name'";
4673 unless ($errors{name}) {
4674 for my $name (@names) {
4676 or $errors{name} = "Invalid name: must be an identifier";
4681 and return $self->_service_error($req, $article, $articles,
4682 "Invalid parameter", \%errors, "FIELD");
4684 return $req->json_content
4690 map { $_ => $req->get_csrf_token($_) } @names,
4696 sub _article_kid_summary {
4697 my ($article_id, $depth) = @_;
4699 my @kids = BSE::DB->query(bseArticleKidSummary => $article_id);
4701 for my $kid (@kids) {
4702 $kid->{children} = [ _article_kid_summary($kid->{id}, $depth) ];
4703 $kid->{allkids} = [ Articles->allkid_summary($kid->{id}) ];
4712 Returns a JSON tree of articles.
4714 Requires an article id (-1 to start from the root).
4716 Takes an optional tree depth. 1 only shows immediate children of the
4722 my ($self, $req, $article, $articles) = @_;
4724 my $depth = $req->cgi->param("depth");
4725 defined $depth && $depth =~ /^\d+$/ and $depth >= 1
4726 or $depth = 10000; # something large
4729 or return $self->_service_error($req, $article, $articles, "Only available to Ajax requests", {}, "NOTAJAX");
4731 return $req->json_content
4736 _article_kid_summary($article->id, $depth),
4743 Returns the article as JSON.
4745 Populates images with images and files with files.
4747 The article data is in the article member of the returned object.
4752 my ($self, $req, $article, $articles) = @_;
4755 or return $self->_service_error($req, $article, $articles, "Only available to Ajax requests", {}, "NOTAJAX");
4757 return $req->json_content
4760 article => $self->_article_data($req, $article),
4764 sub templates_long {
4765 my ($self, $article) = @_;
4767 my @templates = $self->templates($article);
4769 my $cfg = $self->{cfg};
4773 description => $cfg->entry("template descriptions", $_, $_),
4777 sub _populate_config {
4778 my ($self, $req, $article, $articles, $conf) = @_;
4780 my $cfg = $req->cfg;
4781 my %geos = $cfg->entries("thumb geometries");
4783 my @cols = $self->table_object($articles)->rowClass->columns;
4785 for my $col (@cols) {
4786 my $def = $self->default_value($req, $article, $col);
4787 defined $def and $defaults{$col} = $def;
4789 my @templates = $self->templates($article);
4790 $defaults{template} =
4791 $self->default_template($article, $req->cfg, \@templates);
4793 $conf->{templates} = [ $self->templates_long($article) ];
4794 $conf->{thumb_geometries} =
4800 description => $cfg->entry("thumb geometry $_", "description", $_),
4804 $conf->{defaults} = \%defaults;
4805 $conf->{upload_progress} = $req->_tracking_uploads;
4806 my @child_types = $self->child_types($article);
4807 s/^BSE::Edit::// for @child_types;
4808 $conf->{child_types} = \@child_types;
4809 $conf->{flags} = [ $self->flags ];
4814 Returns configuration information as JSON.
4816 Returns an object of the form:
4824 description: "template.tmpl", // or from [template descriptions]
4832 description: "geoid", // or from [thumb geometry id].description
4840 child_types: [ "Article" ],
4843 { id => "A", desc => "description" },
4846 // possibible custom data
4849 To define custom data add entries to the [extra a_config] section,
4850 keys become the keys in the returned structure pointing at hashes
4851 containing that section from the system configuration. Custom keys
4852 may not conflict with system defined keys.
4857 my ($self, $req, $article, $articles) = @_;
4860 or return $self->_service_error($req, $article, $articles, "Only available to Ajax requests", {}, "NOTAJAX");
4863 $self->_populate_config($req, $article, $articles, \%conf);
4866 my $cfg = $req->cfg;
4867 my %custom = $cfg->entries("extra a_config");
4868 for my $key (keys %custom) {
4869 exists $conf{$key} and next;
4871 my $section = $custom{$key};
4872 $section =~ s/\{(level|generator|parentid|template)\}/$article->{$1}/g;
4874 $section eq "db" and die;
4876 $conf{$key} = { $cfg->entries($section) };
4879 return $req->json_content
4891 Tony Cook <tony@develop-help.com>