1 # This module is part of debbugs, and is released
2 # under the terms of the GPL version 2, or any later
3 # version at your option.
4 # See the file README and COPYING for more information.
6 # [Other people have contributed to this file; their copyrights should
8 # Copyright 2007 by Don Armstrong <don@donarmstrong.com>.
14 Debbugs::CGI -- General routines for the cgi scripts
18 use Debbugs::CGI qw(:url :html);
22 This module is a replacement for parts of common.pl; subroutines in
23 common.pl will be gradually phased out and replaced with equivalent
24 (or better) functionality here.
34 use vars qw($VERSION $DEBUG %EXPORT_TAGS @EXPORT_OK @EXPORT);
35 use Exporter qw(import);
37 use feature qw(state);
42 ($VERSION) = q$Revision: 1.3 $ =~ /^Revision:\s+([^\s+])/;
43 $DEBUG = 0 unless defined $DEBUG;
46 %EXPORT_TAGS = (url => [qw(bug_links bug_linklist maybelink),
47 qw(set_url_params version_url),
48 qw(submitterurl mainturl munge_url),
49 qw(package_links bug_links),
51 html => [qw(html_escape htmlize_bugs htmlize_packagelinks),
52 qw(maybelink htmlize_addresslinks htmlize_maintlinks),
54 util => [qw(cgi_parameters quitcgi),
56 forms => [qw(option_form form_options_and_normal_param)],
57 usertags => [qw(add_user)],
58 misc => [qw(maint_decode)],
59 package_search => [qw(@package_search_key_order %package_search_keys)],
60 cache => [qw(calculate_etag etag_does_not_match)],
61 #status => [qw(getbugstatus)],
64 Exporter::export_ok_tags(keys %EXPORT_TAGS);
65 $EXPORT_TAGS{all} = [@EXPORT_OK];
71 use Debbugs::Common qw(getparsedaddrs make_list);
72 use Params::Validate qw(validate_with :types);
74 use Debbugs::Config qw(:config);
75 use Debbugs::Status qw(splitpackages isstrongseverity);
76 use Debbugs::User qw();
80 use Storable qw(dclone);
82 use List::AllUtils qw(max);
84 use Digest::MD5 qw(md5_hex);
87 use Debbugs::Text qw(fill_in_template);
96 Sets the url params which will be used to generate urls.
105 my $url = Debbugs::URI->new($_[0]||'');
106 %URL_PARAMS = %{$url->query_form_hash};
113 my $url = munge_url($url,%params_to_munge);
115 Munges a url, replacing parameters with %params_to_munge as appropriate.
122 my $new_url = Debbugs::URI->new($url);
123 my @old_param = $new_url->query_form();
125 while (my ($key,$value) = splice @old_param,0,2) {
126 push @new_param,($key,$value) unless exists $params{$key};
128 $new_url->query_form(@new_param,
129 map {($_,$params{$_})}
131 return $new_url->as_string;
137 version_url(package => $package,found => $found,fixed => $fixed)
139 Creates a link to the version cgi script
143 =item package -- source package whose graph to display
145 =item found -- arrayref of found versions
147 =item fixed -- arrayref of fixed versions
149 =item width -- optional width of graph
151 =item height -- optional height of graph
153 =item info -- display html info surrounding graph; defaults to 1 if
154 width and height are not passed.
156 =item collapse -- whether to collapse the graph; defaults to 1 if
157 width and height are passed.
164 my %params = validate_with(params => \@_,
165 spec => {package => {type => SCALAR|ARRAYREF,
167 found => {type => ARRAYREF,
170 fixed => {type => ARRAYREF,
173 width => {type => SCALAR,
176 height => {type => SCALAR,
179 absolute => {type => BOOLEAN,
182 collapse => {type => BOOLEAN,
185 info => {type => BOOLEAN,
190 if (not defined $params{width} and not defined $params{height}) {
191 $params{info} = 1 if not exists $params{info};
193 my $url = Debbugs::URI->new('version.cgi?');
194 $url->query_form(%params);
195 return $url->as_string;
202 Escapes html entities by calling HTML::Entities::encode_entities;
209 return HTML::Entities::encode_entities($string,q(<>&"'));
212 =head2 cgi_parameters
216 Returns all of the cgi_parameters from a CGI script using CGI::Simple
221 my %options = validate_with(params => \@_,
222 spec => {query => {type => OBJECT,
225 single => {type => ARRAYREF,
228 default => {type => HASHREF,
233 my $q = $options{query};
235 @single{@{$options{single}}} = (1) x @{$options{single}};
237 for my $paramname ($q->param) {
238 if ($single{$paramname}) {
239 $param{$paramname} = $q->param($paramname);
242 $param{$paramname} = [$q->param($paramname)];
245 for my $default (keys %{$options{default}}) {
246 if (not exists $param{$default}) {
247 # We'll clone the reference here to avoid surprises later.
248 $param{$default} = ref($options{default}{$default})?
249 dclone($options{default}{$default}):$options{default}{$default};
257 my ($msg, $status) = @_;
258 $status //= '500 Internal Server Error';
259 print "Status: $status\n";
260 print "Content-Type: text/html\n\n";
261 print fill_in_template(template=>'cgi/quit',
262 variables => {msg => $msg}
270 =head2 htmlize_packagelinks
274 Given a scalar containing a list of packages separated by something
275 that L<Debbugs::CGI/splitpackages> can separate, returns a
276 formatted set of links to packages in html.
280 sub htmlize_packagelinks {
282 return '' unless defined $pkgs and $pkgs ne '';
283 my @pkglist = splitpackages($pkgs);
285 carp "htmlize_packagelinks is deprecated, use package_links instead";
287 return 'Package' . (@pkglist > 1 ? 's' : '') . ': ' .
288 package_links(package =>\@pkglist,
295 join(', ', package_links(packages => \@packages))
297 Given a list of packages, return a list of html which links to the package
301 =item package -- arrayref or scalar of package(s)
303 =item submitter -- arrayref or scalar of submitter(s)
305 =item src -- arrayref or scalar of source(s)
307 =item maintainer -- arrayref or scalar of maintainer(s)
309 =item links_only -- return only links, not htmlized links, defaults to
310 returning htmlized links.
312 =item class -- class of the a href, defaults to ''
318 our @package_search_key_order = (package => 'in package',
320 severity => 'with severity',
321 src => 'in source package',
322 maint => 'in packages maintained by',
323 submitter => 'submitted by',
325 status => 'with status',
326 affects => 'which affect package',
327 correspondent => 'with mail from',
328 newest => 'newest bugs',
331 our %package_search_keys = @package_search_key_order;
332 our %package_links_invalid_options =
333 map {($_,1)} (keys %package_search_keys,
338 {(map { ($_,{type => SCALAR|ARRAYREF,
341 } keys %package_search_keys,
342 ## these are aliases for package
344 source => {type => SCALAR|ARRAYREF,
347 maintainer => {type => SCALAR|ARRAYREF,
351 links_only => {type => BOOLEAN,
354 class => {type => SCALAR,
357 separator => {type => SCALAR,
360 options => {type => HASHREF,
364 my %param = validate_with(params => \@_,
367 my %options = %{$param{options}};
368 for (grep {$package_links_invalid_options{$_}} keys %options) {
371 ## remove aliases for source and maintainer
372 if (exists $param{source}) {
373 $param{src} = [exists $param{src}?make_list($param{src}):(),
374 make_list($param{source}),
376 delete $param{source};
378 if (exists $param{maintainer}) {
379 $param{maint} = [exists $param{maint}?make_list($param{maint}):(),
380 make_list($param{maintainer}),
382 delete $param{maintainer};
384 my $has_options = keys %options;
386 for my $type (qw(src package)) {
387 next unless exists $param{$type};
388 for my $target (make_list($param{$type})) {
390 if ($target =~ s/^src://) {
392 } elsif ($t_type eq 'source') {
393 $target = 'src:'.$target;
397 (munge_url('pkgreport.cgi?',
404 ('pkgreport.cgi?'.$t_type.'='.uri_escape_utf8($target),
409 for my $type (qw(maint owner submitter correspondent)) {
410 next unless exists $param{$type};
411 for my $target (make_list($param{$type})) {
412 my $addr = getparsedaddrs($target);
413 $addr = defined $addr?$addr->address:'';
416 (munge_url('pkgreport.cgi?',
423 $type.'='.uri_escape_utf8($target),
429 my ($link,$link_name);
431 if (length $param{class}) {
432 $class = q( class=").html_escape($param{class}).q(");
434 while (($link,$link_name) = splice(@links,0,2)) {
435 if ($param{links_only}) {
441 html_escape($link).q(">).
442 html_escape($link_name).q(</a>);
449 return join($param{separator},@return);
455 join(', ', bug_links(bug => \@packages))
457 Given a list of bugs, return a list of html which links to the bugs
461 =item bug -- arrayref or scalar of bug(s)
463 =item links_only -- return only links, not htmlized links, defaults to
464 returning htmlized links.
466 =item class -- class of the a href, defaults to ''
473 state $spec = {bug => {type => SCALAR|ARRAYREF,
476 links_only => {type => BOOLEAN,
479 class => {type => SCALAR,
482 separator => {type => SCALAR,
485 options => {type => HASHREF,
489 my %param = validate_with(params => \@_,
492 my %options = %{$param{options}};
495 delete $options{$_} if exists $options{$_};
497 my $has_options = keys %options;
500 push @links, map {(munge_url('bugreport.cgi?',
505 } make_list($param{bug}) if exists $param{bug};
507 push @links, map {('bugreport.cgi?bug='.uri_escape_utf8($_),
509 make_list($param{bug}) if exists $param{bug};
512 my ($link,$link_name);
514 if (length $param{class}) {
515 $class = q( class=").html_escape($param{class}).q(");
517 while (($link,$link_name) = splice(@links,0,2)) {
518 if ($param{links_only}) {
524 html_escape($link).q(">).
525 html_escape($link_name).q(</a>);
532 return join($param{separator},@return);
541 maybelink('http://foobarbaz,http://bleh',qr/[, ]+/);
542 maybelink('http://foobarbaz,http://bleh',qr/[, ]+/,', ');
545 In the first form, links the link if it looks like a link. In the
546 second form, first splits based on the regex, then reassembles the
547 link, linking things that look like links. In the third form, rejoins
548 the split links with commas and spaces.
553 my ($links,$regex,$join) = @_;
554 if (not defined $regex and not defined $join) {
555 $links =~ s{(.*?)((?:(?:ftp|http|https)://[\S~-]+?/?)?)([\)\'\:\.\,]?(?:\s|\.<|$))}
556 {html_escape($1).(length $2?q(<a href=").html_escape($2).q(">).html_escape($2).q(</a>):'').html_escape($3)}geimo;
559 $join = ' ' if not defined $join;
562 if (defined $regex) {
563 @segments = split $regex, $links;
566 @segments = ($links);
568 for my $in (@segments) {
569 if ($in =~ /^[a-zA-Z0-9+.-]+:/) { # RFC 1738 scheme
570 push @return, qq{<a href="$in">} . html_escape($in) . '</a>';
572 push @return, html_escape($in);
575 return @return?join($join,@return):'';
579 =head2 htmlize_addresslinks
581 htmlize_addresslinks($prefixfunc,$urlfunc,$addresses,$class);
584 Generate a comma-separated list of HTML links to each address given in
585 $addresses, which should be a comma-separated list of RFC822
586 addresses. $urlfunc should be a reference to a function like mainturl
587 or submitterurl which returns the URL for each individual address.
592 sub htmlize_addresslinks {
593 my ($prefixfunc, $urlfunc, $addresses,$class) = @_;
594 carp "htmlize_addresslinks is deprecated";
596 $class = defined $class?qq(class="$class" ):'';
597 if (defined $addresses and $addresses ne '') {
598 my @addrs = getparsedaddrs($addresses);
599 my $prefix = (ref $prefixfunc) ?
600 $prefixfunc->(scalar @addrs):$prefixfunc;
603 { sprintf qq(<a ${class}).
605 $urlfunc->($_->address),
606 html_escape($_->format) ||
612 my $prefix = (ref $prefixfunc) ?
613 $prefixfunc->(1) : $prefixfunc;
614 return sprintf '%s<a '.$class.'href="%s">(unknown)</a>',
615 $prefix, $urlfunc->('');
620 my $addr = getparsedaddrs($_[0] || "");
621 $addr = defined $addr?$addr->address:'';
625 sub mainturl { package_links(maintainer => $_[0], links_only => 1); }
626 sub submitterurl { package_links(submitter => $_[0], links_only => 1); }
627 sub htmlize_maintlinks {
628 my ($prefixfunc, $maints) = @_;
629 carp "htmlize_maintlinks is deprecated";
630 return htmlize_addresslinks($prefixfunc, \&mainturl, $maints);
635 bug_linklist($separator,$class,@bugs)
637 Creates a set of links to C<@bugs> separated by C<$separator> with
638 link class C<$class>.
640 XXX Use L<Params::Validate>; we want to be able to support query
641 arguments here too; we should be able to combine bug_links and this
648 my ($sep,$class,@bugs) = @_;
649 carp "bug_linklist is deprecated; use bug_links instead";
650 return scalar bug_links(bug=>\@bugs,class=>$class,separator=>$sep);
655 my ($user,$usertags,$bug_usertags,$seen_users,$cats,$hidden) = @_;
656 $seen_users = {} if not defined $seen_users;
657 $bug_usertags = {} if not defined $bug_usertags;
658 $usertags = {} if not defined $usertags;
659 $cats = {} if not defined $cats;
660 $hidden = {} if not defined $hidden;
661 return if exists $seen_users->{$user};
662 $seen_users->{$user} = 1;
664 my $u = Debbugs::User::get_user($user);
666 my %vis = map { $_, 1 } @{$u->{"visible_cats"}};
667 for my $c (keys %{$u->{"categories"}}) {
668 $cats->{$c} = $u->{"categories"}->{$c};
669 $hidden->{$c} = 1 unless defined $vis{$c};
671 for my $t (keys %{$u->{"tags"}}) {
672 $usertags->{$t} = [] unless defined $usertags->{$t};
673 push @{$usertags->{$t}}, @{$u->{"tags"}->{$t}};
676 %{$bug_usertags} = ();
677 for my $t (keys %{$usertags}) {
678 for my $b (@{$usertags->{$t}}) {
679 $bug_usertags->{$b} = [] unless defined $bug_usertags->{$b};
680 push @{$bug_usertags->{$b}}, $t;
691 =head2 form_options_and_normal_param
693 my ($form_option,$param) = form_options_and_normal_param(\%param)
694 if $param{form_options};
695 my $form_option = form_options_and_normal_param(\%param)
696 if $param{form_options};
698 Translates from special form_options to a set of parameters which can
699 be used to run the current page.
701 The idea behind this is to allow complex forms to relatively easily
702 cause options that the existing cgi scripts understand to be set.
704 Currently there are two commands which are understood:
705 combine, and concatenate.
709 Combine works by entering key,value pairs into the parameters using
710 the key field option input field, and the value field option input
713 For example, you would have
715 <input type="hidden" name="_fo_combine_key_fo_searchkey_value_fo_searchvalue" value="1">
717 which would combine the _fo_searchkey and _fo_searchvalue input fields, so
719 <input type="text" name="_fo_searchkey" value="foo">
720 <input type="text" name="_fo_searchvalue" value="bar">
722 would yield foo=>'bar' in %param.
726 Concatenate concatenates values into a single entry in a parameter
728 For example, you would have
730 <input type="hidden" name="_fo_concatentate_into_foo_with_:_fo_blah_fo_bleargh" value="1">
732 which would combine the _fo_searchkey and _fo_searchvalue input fields, so
734 <input type="text" name="_fo_blah" value="bar">
735 <input type="text" name="_fo_bleargh" value="baz">
737 would yield foo=>'bar:baz' in %param.
742 my $form_option_leader = '_fo_';
743 sub form_options_and_normal_param{
744 my ($orig_param) = @_;
745 # all form_option parameters start with _fo_
746 my ($param,$form_option) = ({},{});
747 for my $key (keys %{$orig_param}) {
748 if ($key =~ /^\Q$form_option_leader\E/) {
749 $form_option->{$key} = $orig_param->{$key};
752 $param->{$key} = $orig_param->{$key};
755 # at this point, we check for commands
756 COMMAND: for my $key (keys %{$form_option}) {
757 $key =~ s/^\Q$form_option_leader\E//;
758 if (my ($key_name,$value_name) =
759 $key =~ /combine_key(\Q$form_option_leader\E.+)
760 _value(\Q$form_option_leader\E.+)$/x
762 next unless defined $form_option->{$key_name};
763 next unless defined $form_option->{$value_name};
764 my @keys = make_list($form_option->{$key_name});
765 my @values = make_list($form_option->{$value_name});
766 for my $i (0 .. $#keys) {
767 last if $i > $#values;
768 next if not defined $keys[$i];
769 next if not defined $values[$i];
770 __add_to_param($param,
776 elsif (my ($field,$concatenate_key,$fields) =
777 $key =~ /concatenate_into_(.+?)((?:_with_[^_])?)
778 ((?:\Q$form_option_leader\E.+?)+)
781 if (length $concatenate_key) {
782 $concatenate_key =~ s/_with_//;
785 $concatenate_key = ':';
787 my @fields = $fields =~ m/(\Q$form_option_leader\E.+?)(?:(?=\Q$form_option_leader\E)|$)/g;
790 for my $f (@fields) {
791 next COMMAND unless defined $form_option->{$f};
792 $field_list{$f} = [make_list($form_option->{$f})];
793 $max_num = max($max_num,$#{$field_list{$f}});
795 for my $i (0 .. $max_num) {
796 next unless @fields == grep {$i <= $#{$field_list{$_}} and
797 defined $field_list{$_}[$i]} @fields;
798 __add_to_param($param,
800 join($concatenate_key,
801 map {$field_list{$_}[$i]} @fields
807 return wantarray?($form_option,$param):$form_option;
812 print option_form(template=>'pkgreport_options',
814 form_options => $form_options,
822 my %param = validate_with(params => \@_,
823 spec => {template => {type => SCALAR,
825 variables => {type => HASHREF,
828 language => {type => SCALAR,
831 param => {type => HASHREF,
834 form_options => {type => HASHREF,
840 # First, we need to see if we need to add particular types of
842 my $variables = dclone($param{variables});
843 $variables->{param} = dclone($param{param});
844 for my $key (keys %{$param{form_option}}) {
845 # strip out leader; shouldn't be anything here without one,
846 # but skip stupid things anyway
847 next unless $key =~ s/^\Q$form_option_leader\E//;
848 if ($key =~ /^add_(.+)$/) {
849 # this causes a specific parameter to be added
850 __add_to_param($variables->{param},
855 elsif ($key =~ /^delete_(.+?)(?:_(\d+))?$/) {
856 next unless exists $variables->{param}{$1};
857 if (ref $variables->{param}{$1} eq 'ARRAY' and
859 defined $variables->{param}{$1}[$2]
861 splice @{$variables->{param}{$1}},$2,1;
864 delete $variables->{param}{$1};
867 # we'll add extra comands here once I figure out what they
870 # now at this point, we're ready to create the template
871 return Debbugs::Text::fill_in_template(template=>$param{template},
872 (exists $param{language}?(language=>$param{language}):()),
873 variables => $variables,
874 hole_var => {'&html_escape' => \&html_escape,
880 my ($param,$key,@values) = @_;
882 if (exists $param->{$key} and not
883 ref $param->{$key}) {
884 @{$param->{$key}} = [$param->{$key},
889 push @{$param->{$key}}, @values;
903 Decodes the funky maintainer encoding.
905 Don't ask me what in the world it does.
911 return () unless @input;
913 for my $input (@input) {
914 my $decoded = $input;
915 $decoded =~ s/-([^_]+)/-$1_-/g;
916 $decoded =~ s/_/-20_/g;
917 $decoded =~ s/^,(.*),(.*),([^,]+)$/$1-40_$2-20_-28_$3-29_/;
918 $decoded =~ s/^([^,]+),(.*),(.*),/$1-20_-3c_$2-40_$3-3e_/;
919 $decoded =~ s/\./-2e_/g;
920 $decoded =~ s/-([0-9a-f]{2})_/pack('H*',$1)/ge;
921 push @output,$decoded;
923 wantarray ? @output : $output[0];
928 =head2 calculate_etags
930 calculate_etags(files => [qw(list of files)],additional_data => [qw(any additional data)]);
934 sub calculate_etags {
936 validate_with(params => \@_,
937 spec => {files => {type => ARRAYREF,
940 additional_data => {type => ARRAYREF,
945 my @additional_data = @{$param{additional_data}};
946 for my $file (@{$param{files}}) {
947 my $st = stat($file) or warn "Unable to stat $file: $!";
948 push @additional_data,$st->mtime;
949 push @additional_data,$st->size;
951 return(md5_hex(join('',sort @additional_data)));
954 =head2 etag_does_not_match
956 etag_does_not_match(cgi=>$q,files=>[qw(list of files)],
957 additional_data=>[qw(any additional data)])
960 Checks to see if the CGI request contains an etag which matches the calculated
963 If there wasn't an etag given, or the etag given doesn't match, return the etag.
965 If the etag does match, return 0.
969 sub etag_does_not_match {
971 validate_with(params => \@_,
972 spec => {files => {type => ARRAYREF,
975 additional_data => {type => ARRAYREF,
978 cgi => {type => OBJECT},
982 $param{cgi}->http('if-none-match');
984 calculate_etags(files=>$param{files},
985 additional_data=>$param{additional_data});
986 if (not defined $submitted_etag or
987 length($submitted_etag) != 32
988 or $etag ne $submitted_etag
992 if ($etag eq $submitted_etag) {