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);
39 use Debbugs::Common qw(getparsedaddrs make_list);
40 use Params::Validate qw(validate_with :types);
42 use Debbugs::Config qw(:config);
43 use Debbugs::Status qw(splitpackages isstrongseverity);
44 use Debbugs::User qw();
48 use Storable qw(dclone);
50 use List::AllUtils qw(max);
52 use Digest::MD5 qw(md5_hex);
55 use Debbugs::Text qw(fill_in_template);
61 ($VERSION) = q$Revision: 1.3 $ =~ /^Revision:\s+([^\s+])/;
62 $DEBUG = 0 unless defined $DEBUG;
65 %EXPORT_TAGS = (url => [qw(bug_links bug_linklist maybelink),
66 qw(set_url_params version_url),
67 qw(submitterurl mainturl munge_url),
68 qw(package_links bug_links),
70 html => [qw(html_escape htmlize_bugs htmlize_packagelinks),
71 qw(maybelink htmlize_addresslinks htmlize_maintlinks),
73 util => [qw(cgi_parameters quitcgi),
75 forms => [qw(option_form form_options_and_normal_param)],
76 usertags => [qw(add_user)],
77 misc => [qw(maint_decode)],
78 package_search => [qw(@package_search_key_order %package_search_keys)],
79 cache => [qw(calculate_etag etag_does_not_match)],
80 #status => [qw(getbugstatus)],
83 Exporter::export_ok_tags(keys %EXPORT_TAGS);
84 $EXPORT_TAGS{all} = [@EXPORT_OK];
94 Sets the url params which will be used to generate urls.
103 my $url = Debbugs::URI->new($_[0]||'');
104 %URL_PARAMS = %{$url->query_form_hash};
111 my $url = munge_url($url,%params_to_munge);
113 Munges a url, replacing parameters with %params_to_munge as appropriate.
120 my $new_url = Debbugs::URI->new($url);
121 my @old_param = $new_url->query_form();
123 while (my ($key,$value) = splice @old_param,0,2) {
124 push @new_param,($key,$value) unless exists $params{$key};
126 $new_url->query_form(@new_param,
127 map {($_,$params{$_})}
129 return $new_url->as_string;
135 version_url(package => $package,found => $found,fixed => $fixed)
137 Creates a link to the version cgi script
141 =item package -- source package whose graph to display
143 =item found -- arrayref of found versions
145 =item fixed -- arrayref of fixed versions
147 =item width -- optional width of graph
149 =item height -- optional height of graph
151 =item info -- display html info surrounding graph; defaults to 1 if
152 width and height are not passed.
154 =item collapse -- whether to collapse the graph; defaults to 1 if
155 width and height are passed.
162 my %params = validate_with(params => \@_,
163 spec => {package => {type => SCALAR|ARRAYREF,
165 found => {type => ARRAYREF,
168 fixed => {type => ARRAYREF,
171 width => {type => SCALAR,
174 height => {type => SCALAR,
177 absolute => {type => BOOLEAN,
180 collapse => {type => BOOLEAN,
183 info => {type => BOOLEAN,
188 if (not defined $params{width} and not defined $params{height}) {
189 $params{info} = 1 if not exists $params{info};
191 my $url = Debbugs::URI->new('version.cgi?');
192 $url->query_form(%params);
193 return $url->as_string;
200 Escapes html entities by calling HTML::Entities::encode_entities;
207 return HTML::Entities::encode_entities($string,q(<>&"'));
210 =head2 cgi_parameters
214 Returns all of the cgi_parameters from a CGI script using CGI::Simple
219 my %options = validate_with(params => \@_,
220 spec => {query => {type => OBJECT,
223 single => {type => ARRAYREF,
226 default => {type => HASHREF,
231 my $q = $options{query};
233 @single{@{$options{single}}} = (1) x @{$options{single}};
235 for my $paramname ($q->param) {
236 if ($single{$paramname}) {
237 $param{$paramname} = $q->param($paramname);
240 $param{$paramname} = [$q->param($paramname)];
243 for my $default (keys %{$options{default}}) {
244 if (not exists $param{$default}) {
245 # We'll clone the reference here to avoid surprises later.
246 $param{$default} = ref($options{default}{$default})?
247 dclone($options{default}{$default}):$options{default}{$default};
255 my ($msg, $status) = @_;
256 $status //= '500 Internal Server Error';
257 print "Status: $status\n";
258 print "Content-Type: text/html\n\n";
259 print fill_in_template(template=>'cgi/quit',
260 variables => {msg => $msg}
268 =head2 htmlize_packagelinks
272 Given a scalar containing a list of packages separated by something
273 that L<Debbugs::CGI/splitpackages> can separate, returns a
274 formatted set of links to packages in html.
278 sub htmlize_packagelinks {
280 return '' unless defined $pkgs and $pkgs ne '';
281 my @pkglist = splitpackages($pkgs);
283 carp "htmlize_packagelinks is deprecated, use package_links instead";
285 return 'Package' . (@pkglist > 1 ? 's' : '') . ': ' .
286 package_links(package =>\@pkglist,
293 join(', ', package_links(packages => \@packages))
295 Given a list of packages, return a list of html which links to the package
299 =item package -- arrayref or scalar of package(s)
301 =item submitter -- arrayref or scalar of submitter(s)
303 =item src -- arrayref or scalar of source(s)
305 =item maintainer -- arrayref or scalar of maintainer(s)
307 =item links_only -- return only links, not htmlized links, defaults to
308 returning htmlized links.
310 =item class -- class of the a href, defaults to ''
316 our @package_search_key_order = (package => 'in package',
318 severity => 'with severity',
319 src => 'in source package',
320 maint => 'in packages maintained by',
321 submitter => 'submitted by',
323 status => 'with status',
324 affects => 'which affect package',
325 correspondent => 'with mail from',
326 newest => 'newest bugs',
329 our %package_search_keys = @package_search_key_order;
333 my %param = validate_with(params => \@_,
334 spec => {(map { ($_,{type => SCALAR|ARRAYREF,
337 } keys %package_search_keys,
339 links_only => {type => BOOLEAN,
342 class => {type => SCALAR,
345 separator => {type => SCALAR,
348 options => {type => HASHREF,
355 my %map = (source => 'src',
356 maintainer => 'maint',
359 return $map{$key} if exists $map{$key};
363 my %options = %{$param{options}};
364 for ((keys %package_search_keys,qw(msg att))) {
365 delete $options{$_} if exists $options{$_};
368 for my $type (qw(src package)) {
369 push @links, map {my $t_type = $type;
370 if ($_ =~ s/^src://) {
373 (munge_url('pkgreport.cgi?',
377 ($t_type eq 'src'?'src:':'').$_);
378 } make_list($param{$type}) if exists $param{$type};
380 for my $type (qw(maint owner submitter correspondent)) {
381 push @links, map {my $addr = getparsedaddrs($_);
382 $addr = defined $addr?$addr->address:'';
383 (munge_url('pkgreport.cgi?',
388 } make_list($param{$type}) if exists $param{$type};
391 my ($link,$link_name);
393 if (length $param{class}) {
394 $class = q( class=").html_escape($param{class}).q(");
396 while (($link,$link_name) = splice(@links,0,2)) {
397 if ($param{links_only}) {
403 html_escape($link).q(">).
404 html_escape($link_name).q(</a>);
411 return join($param{separator},@return);
417 join(', ', bug_links(bug => \@packages))
419 Given a list of bugs, return a list of html which links to the bugs
423 =item bug -- arrayref or scalar of bug(s)
425 =item links_only -- return only links, not htmlized links, defaults to
426 returning htmlized links.
428 =item class -- class of the a href, defaults to ''
435 my %param = validate_with(params => \@_,
436 spec => {bug => {type => SCALAR|ARRAYREF,
439 links_only => {type => BOOLEAN,
442 class => {type => SCALAR,
445 separator => {type => SCALAR,
448 options => {type => HASHREF,
453 my %options = %{$param{options}};
456 delete $options{$_} if exists $options{$_};
459 push @links, map {(munge_url('bugreport.cgi?',
464 } make_list($param{bug}) if exists $param{bug};
466 my ($link,$link_name);
468 if (length $param{class}) {
469 $class = q( class=").html_escape($param{class}).q(");
471 while (($link,$link_name) = splice(@links,0,2)) {
472 if ($param{links_only}) {
478 html_escape($link).q(">).
479 html_escape($link_name).q(</a>);
486 return join($param{separator},@return);
495 maybelink('http://foobarbaz,http://bleh',qr/[, ]+/);
496 maybelink('http://foobarbaz,http://bleh',qr/[, ]+/,', ');
499 In the first form, links the link if it looks like a link. In the
500 second form, first splits based on the regex, then reassembles the
501 link, linking things that look like links. In the third form, rejoins
502 the split links with commas and spaces.
507 my ($links,$regex,$join) = @_;
508 if (not defined $regex and not defined $join) {
509 $links =~ s{(.*?)((?:(?:ftp|http|https)://[\S~-]+?/?)?)([\)\'\:\.\,]?(?:\s|\.<|$))}
510 {html_escape($1).(length $2?q(<a href=").html_escape($2).q(">).html_escape($2).q(</a>):'').html_escape($3)}geimo;
513 $join = ' ' if not defined $join;
516 if (defined $regex) {
517 @segments = split $regex, $links;
520 @segments = ($links);
522 for my $in (@segments) {
523 if ($in =~ /^[a-zA-Z0-9+.-]+:/) { # RFC 1738 scheme
524 push @return, qq{<a href="$in">} . html_escape($in) . '</a>';
526 push @return, html_escape($in);
529 return @return?join($join,@return):'';
533 =head2 htmlize_addresslinks
535 htmlize_addresslinks($prefixfunc,$urlfunc,$addresses,$class);
538 Generate a comma-separated list of HTML links to each address given in
539 $addresses, which should be a comma-separated list of RFC822
540 addresses. $urlfunc should be a reference to a function like mainturl
541 or submitterurl which returns the URL for each individual address.
546 sub htmlize_addresslinks {
547 my ($prefixfunc, $urlfunc, $addresses,$class) = @_;
548 carp "htmlize_addresslinks is deprecated";
550 $class = defined $class?qq(class="$class" ):'';
551 if (defined $addresses and $addresses ne '') {
552 my @addrs = getparsedaddrs($addresses);
553 my $prefix = (ref $prefixfunc) ?
554 $prefixfunc->(scalar @addrs):$prefixfunc;
557 { sprintf qq(<a ${class}).
559 $urlfunc->($_->address),
560 html_escape($_->format) ||
566 my $prefix = (ref $prefixfunc) ?
567 $prefixfunc->(1) : $prefixfunc;
568 return sprintf '%s<a '.$class.'href="%s">(unknown)</a>',
569 $prefix, $urlfunc->('');
574 my $addr = getparsedaddrs($_[0] || "");
575 $addr = defined $addr?$addr->address:'';
579 sub mainturl { package_links(maint => $_[0], links_only => 1); }
580 sub submitterurl { package_links(submitter => $_[0], links_only => 1); }
581 sub htmlize_maintlinks {
582 my ($prefixfunc, $maints) = @_;
583 carp "htmlize_maintlinks is deprecated";
584 return htmlize_addresslinks($prefixfunc, \&mainturl, $maints);
589 bug_linklist($separator,$class,@bugs)
591 Creates a set of links to C<@bugs> separated by C<$separator> with
592 link class C<$class>.
594 XXX Use L<Params::Validate>; we want to be able to support query
595 arguments here too; we should be able to combine bug_links and this
602 my ($sep,$class,@bugs) = @_;
603 carp "bug_linklist is deprecated; use bug_links instead";
604 return scalar bug_links(bug=>\@bugs,class=>$class,separator=>$sep);
609 my ($user,$usertags,$bug_usertags,$seen_users,$cats,$hidden) = @_;
610 $seen_users = {} if not defined $seen_users;
611 $bug_usertags = {} if not defined $bug_usertags;
612 $usertags = {} if not defined $usertags;
613 $cats = {} if not defined $cats;
614 $hidden = {} if not defined $hidden;
615 return if exists $seen_users->{$user};
616 $seen_users->{$user} = 1;
618 my $u = Debbugs::User::get_user($user);
620 my %vis = map { $_, 1 } @{$u->{"visible_cats"}};
621 for my $c (keys %{$u->{"categories"}}) {
622 $cats->{$c} = $u->{"categories"}->{$c};
623 $hidden->{$c} = 1 unless defined $vis{$c};
625 for my $t (keys %{$u->{"tags"}}) {
626 $usertags->{$t} = [] unless defined $usertags->{$t};
627 push @{$usertags->{$t}}, @{$u->{"tags"}->{$t}};
630 %{$bug_usertags} = ();
631 for my $t (keys %{$usertags}) {
632 for my $b (@{$usertags->{$t}}) {
633 $bug_usertags->{$b} = [] unless defined $bug_usertags->{$b};
634 push @{$bug_usertags->{$b}}, $t;
645 =head2 form_options_and_normal_param
647 my ($form_option,$param) = form_options_and_normal_param(\%param)
648 if $param{form_options};
649 my $form_option = form_options_and_normal_param(\%param)
650 if $param{form_options};
652 Translates from special form_options to a set of parameters which can
653 be used to run the current page.
655 The idea behind this is to allow complex forms to relatively easily
656 cause options that the existing cgi scripts understand to be set.
658 Currently there are two commands which are understood:
659 combine, and concatenate.
663 Combine works by entering key,value pairs into the parameters using
664 the key field option input field, and the value field option input
667 For example, you would have
669 <input type="hidden" name="_fo_combine_key_fo_searchkey_value_fo_searchvalue" value="1">
671 which would combine the _fo_searchkey and _fo_searchvalue input fields, so
673 <input type="text" name="_fo_searchkey" value="foo">
674 <input type="text" name="_fo_searchvalue" value="bar">
676 would yield foo=>'bar' in %param.
680 Concatenate concatenates values into a single entry in a parameter
682 For example, you would have
684 <input type="hidden" name="_fo_concatentate_into_foo_with_:_fo_blah_fo_bleargh" value="1">
686 which would combine the _fo_searchkey and _fo_searchvalue input fields, so
688 <input type="text" name="_fo_blah" value="bar">
689 <input type="text" name="_fo_bleargh" value="baz">
691 would yield foo=>'bar:baz' in %param.
696 my $form_option_leader = '_fo_';
697 sub form_options_and_normal_param{
698 my ($orig_param) = @_;
699 # all form_option parameters start with _fo_
700 my ($param,$form_option) = ({},{});
701 for my $key (keys %{$orig_param}) {
702 if ($key =~ /^\Q$form_option_leader\E/) {
703 $form_option->{$key} = $orig_param->{$key};
706 $param->{$key} = $orig_param->{$key};
709 # at this point, we check for commands
710 COMMAND: for my $key (keys %{$form_option}) {
711 $key =~ s/^\Q$form_option_leader\E//;
712 if (my ($key_name,$value_name) =
713 $key =~ /combine_key(\Q$form_option_leader\E.+)
714 _value(\Q$form_option_leader\E.+)$/x
716 next unless defined $form_option->{$key_name};
717 next unless defined $form_option->{$value_name};
718 my @keys = make_list($form_option->{$key_name});
719 my @values = make_list($form_option->{$value_name});
720 for my $i (0 .. $#keys) {
721 last if $i > $#values;
722 next if not defined $keys[$i];
723 next if not defined $values[$i];
724 __add_to_param($param,
730 elsif (my ($field,$concatenate_key,$fields) =
731 $key =~ /concatenate_into_(.+?)((?:_with_[^_])?)
732 ((?:\Q$form_option_leader\E.+?)+)
735 if (length $concatenate_key) {
736 $concatenate_key =~ s/_with_//;
739 $concatenate_key = ':';
741 my @fields = $fields =~ m/(\Q$form_option_leader\E.+?)(?:(?=\Q$form_option_leader\E)|$)/g;
744 for my $f (@fields) {
745 next COMMAND unless defined $form_option->{$f};
746 $field_list{$f} = [make_list($form_option->{$f})];
747 $max_num = max($max_num,$#{$field_list{$f}});
749 for my $i (0 .. $max_num) {
750 next unless @fields == grep {$i <= $#{$field_list{$_}} and
751 defined $field_list{$_}[$i]} @fields;
752 __add_to_param($param,
754 join($concatenate_key,
755 map {$field_list{$_}[$i]} @fields
761 return wantarray?($form_option,$param):$form_option;
766 print option_form(template=>'pkgreport_options',
768 form_options => $form_options,
776 my %param = validate_with(params => \@_,
777 spec => {template => {type => SCALAR,
779 variables => {type => HASHREF,
782 language => {type => SCALAR,
785 param => {type => HASHREF,
788 form_options => {type => HASHREF,
794 # First, we need to see if we need to add particular types of
796 my $variables = dclone($param{variables});
797 $variables->{param} = dclone($param{param});
798 for my $key (keys %{$param{form_option}}) {
799 # strip out leader; shouldn't be anything here without one,
800 # but skip stupid things anyway
801 next unless $key =~ s/^\Q$form_option_leader\E//;
802 if ($key =~ /^add_(.+)$/) {
803 # this causes a specific parameter to be added
804 __add_to_param($variables->{param},
809 elsif ($key =~ /^delete_(.+?)(?:_(\d+))?$/) {
810 next unless exists $variables->{param}{$1};
811 if (ref $variables->{param}{$1} eq 'ARRAY' and
813 defined $variables->{param}{$1}[$2]
815 splice @{$variables->{param}{$1}},$2,1;
818 delete $variables->{param}{$1};
821 # we'll add extra comands here once I figure out what they
824 # add in a few utility routines
825 $variables->{output_select_options} = sub {
826 my ($options,$value) = @_;
827 my @options = @{$options};
829 while (my ($o_value,$name) = splice @options,0,2) {
831 if (defined $value and $o_value eq $value) {
832 $selected = ' selected';
834 $output .= q(<option value=").html_escape($o_value).qq("$selected>).
835 html_escape($name).qq(</option>\n);
839 $variables->{make_list} = sub { make_list(@_);
841 # now at this point, we're ready to create the template
842 return Debbugs::Text::fill_in_template(template=>$param{template},
843 (exists $param{language}?(language=>$param{language}):()),
844 variables => $variables,
845 hole_var => {'&html_escape' => \&html_escape,
851 my ($param,$key,@values) = @_;
853 if (exists $param->{$key} and not
854 ref $param->{$key}) {
855 @{$param->{$key}} = [$param->{$key},
860 push @{$param->{$key}}, @values;
874 Decodes the funky maintainer encoding.
876 Don't ask me what in the world it does.
882 return () unless @input;
884 for my $input (@input) {
885 my $decoded = $input;
886 $decoded =~ s/-([^_]+)/-$1_-/g;
887 $decoded =~ s/_/-20_/g;
888 $decoded =~ s/^,(.*),(.*),([^,]+)$/$1-40_$2-20_-28_$3-29_/;
889 $decoded =~ s/^([^,]+),(.*),(.*),/$1-20_-3c_$2-40_$3-3e_/;
890 $decoded =~ s/\./-2e_/g;
891 $decoded =~ s/-([0-9a-f]{2})_/pack('H*',$1)/ge;
892 push @output,$decoded;
894 wantarray ? @output : $output[0];
899 =head2 calculate_etags
901 calculate_etags(files => [qw(list of files)],additional_data => [qw(any additional data)]);
905 sub calculate_etags {
907 validate_with(params => \@_,
908 spec => {files => {type => ARRAYREF,
911 additional_data => {type => ARRAYREF,
916 my @additional_data = @{$param{additional_data}};
917 for my $file (@{$param{files}}) {
918 my $st = stat($file) or warn "Unable to stat $file: $!";
919 push @additional_data,$st->mtime;
920 push @additional_data,$st->size;
922 return(md5_hex(join('',sort @additional_data)));
925 =head2 etag_does_not_match
927 etag_does_not_match(cgi=>$q,files=>[qw(list of files)],
928 additional_data=>[qw(any additional data)])
931 Checks to see if the CGI request contains an etag which matches the calculated
934 If there wasn't an etag given, or the etag given doesn't match, return the etag.
936 If the etag does match, return 0.
940 sub etag_does_not_match {
942 validate_with(params => \@_,
943 spec => {files => {type => ARRAYREF,
946 additional_data => {type => ARRAYREF,
949 cgi => {type => OBJECT},
953 $param{cgi}->http('if-none-match');
955 calculate_etags(files=>$param{files},
956 additional_data=>$param{additional_data});
957 if (not defined $submitted_etag or
958 length($submitted_etag) != 32
959 or $etag ne $submitted_etag
963 if ($etag eq $submitted_etag) {