- return HTML::Entities::encode_entities($string)
-}
-
-my %common_bugusertags;
-
-# =head2 get_bug_status
-#
-# my $status = getbugstatus($bug_num)
-#
-# my $status = getbugstatus($bug_num,$bug_index)
-#
-#
-# =cut
-#
-# sub get_bug_status {
-# my ($bugnum,$bugidx) = @_;
-#
-# my %status;
-#
-# if (defined $bugidx and exists $bugidx->{$bugnum}) {
-# %status = %{ $bugidx->{$bugnum} };
-# $status{pending} = $status{ status };
-# $status{id} = $bugnum;
-# return \%status;
-# }
-#
-# my $location = getbuglocation($bugnum, 'summary');
-# return {} if not length $location;
-# %status = %{ readbug( $bugnum, $location ) };
-# $status{id} = $bugnum;
-#
-#
-# if (defined $common_bugusertags{$bugnum}) {
-# $status{keywords} = "" unless defined $status{keywords};
-# $status{keywords} .= " " unless $status{keywords} eq "";
-# $status{keywords} .= join(" ", @{$common_bugusertags{$bugnum}});
-# }
-# $status{tags} = $status{keywords};
-# my %tags = map { $_ => 1 } split ' ', $status{tags};
-#
-# $status{"package"} =~ s/\s*$//;
-# $status{"package"} = 'unknown' if ($status{"package"} eq '');
-# $status{"severity"} = 'normal' if ($status{"severity"} eq '');
-#
-# $status{"pending"} = 'pending';
-# $status{"pending"} = 'forwarded' if (length($status{"forwarded"}));
-# $status{"pending"} = 'pending-fixed' if ($tags{pending});
-# $status{"pending"} = 'fixed' if ($tags{fixed});
-#
-# my @versions;
-# if (defined $common_version) {
-# @versions = ($common_version);
-# } elsif (defined $common_dist) {
-# @versions = getversions($status{package}, $common_dist, $common_arch);
-# }
-#
-# # TODO: This should probably be handled further out for efficiency and
-# # for more ease of distinguishing between pkg= and src= queries.
-# my @sourceversions = makesourceversions($status{package}, $common_arch,
-# @versions);
-#
-# if (@sourceversions) {
-# # Resolve bugginess states (we might be looking at multiple
-# # architectures, say). Found wins, then fixed, then absent.
-# my $maxbuggy = 'absent';
-# for my $version (@sourceversions) {
-# my $buggy = buggyversion($bugnum, $version, \%status);
-# if ($buggy eq 'found') {
-# $maxbuggy = 'found';
-# last;
-# } elsif ($buggy eq 'fixed' and $maxbuggy ne 'found') {
-# $maxbuggy = 'fixed';
-# }
-# }
-# if ($maxbuggy eq 'absent') {
-# $status{"pending"} = 'absent';
-# } elsif ($maxbuggy eq 'fixed') {
-# $status{"pending"} = 'done';
-# }
-# }
-#
-# if (length($status{done}) and
-# (not @sourceversions or not @{$status{fixed_versions}})) {
-# $status{"pending"} = 'done';
-# }
-#
-# return \%status;
-# }
+ return HTML::Entities::encode_entities($string,q(<>&"'));
+}
+
+=head2 cgi_parameters
+
+ cgi_parameters
+
+Returns all of the cgi_parameters from a CGI script using CGI::Simple
+
+=cut
+
+sub cgi_parameters {
+ my %options = validate_with(params => \@_,
+ spec => {query => {type => OBJECT,
+ can => 'param',
+ },
+ single => {type => ARRAYREF,
+ default => [],
+ },
+ default => {type => HASHREF,
+ default => {},
+ },
+ },
+ );
+ my $q = $options{query};
+ my %single;
+ @single{@{$options{single}}} = (1) x @{$options{single}};
+ my %param;
+ for my $paramname ($q->param) {
+ if ($single{$paramname}) {
+ $param{$paramname} = $q->param($paramname);
+ }
+ else {
+ $param{$paramname} = [$q->param($paramname)];
+ }
+ }
+ for my $default (keys %{$options{default}}) {
+ if (not exists $param{$default}) {
+ # We'll clone the reference here to avoid surprises later.
+ $param{$default} = ref($options{default}{$default})?
+ dclone($options{default}{$default}):$options{default}{$default};
+ }
+ }
+ return %param;
+}
+
+
+sub quitcgi {
+ my ($msg, $status) = @_;
+ $status //= '500 Internal Server Error';
+ print "Status: $status\n";
+ print "Content-Type: text/html\n\n";
+ print fill_in_template(template=>'cgi/quit',
+ variables => {msg => $msg}
+ );
+ exit 0;
+}
+
+
+=head1 HTML
+
+=head2 htmlize_packagelinks
+
+ htmlize_packagelinks
+
+Given a scalar containing a list of packages separated by something
+that L<Debbugs::CGI/splitpackages> can separate, returns a
+formatted set of links to packages in html.
+
+=cut
+
+sub htmlize_packagelinks {
+ my ($pkgs) = @_;
+ return '' unless defined $pkgs and $pkgs ne '';
+ my @pkglist = splitpackages($pkgs);
+
+ carp "htmlize_packagelinks is deprecated, use package_links instead";
+
+ return 'Package' . (@pkglist > 1 ? 's' : '') . ': ' .
+ package_links(package =>\@pkglist,
+ class => 'submitter'
+ );
+}
+
+=head2 package_links
+
+ join(', ', package_links(packages => \@packages))
+
+Given a list of packages, return a list of html which links to the package
+
+=over
+
+=item package -- arrayref or scalar of package(s)
+
+=item submitter -- arrayref or scalar of submitter(s)
+
+=item src -- arrayref or scalar of source(s)
+
+=item maintainer -- arrayref or scalar of maintainer(s)
+
+=item links_only -- return only links, not htmlized links, defaults to
+returning htmlized links.
+
+=item class -- class of the a href, defaults to ''
+
+=back
+
+=cut
+
+our @package_search_key_order = (package => 'in package',
+ tag => 'tagged',
+ severity => 'with severity',
+ src => 'in source package',
+ maint => 'in packages maintained by',
+ submitter => 'submitted by',
+ owner => 'owned by',
+ status => 'with status',
+ affects => 'which affect package',
+ correspondent => 'with mail from',
+ newest => 'newest bugs',
+ bugs => 'in bug',
+ );
+our %package_search_keys = @package_search_key_order;
+our %package_links_invalid_options =
+ map {($_,1)} (keys %package_search_keys,
+ qw(msg att));
+
+sub package_links {
+ state $spec =
+ {(map { ($_,{type => SCALAR|ARRAYREF,
+ optional => 1,
+ });
+ } keys %package_search_keys,
+ ## these are aliases for package
+ ## search keys
+ source => {type => SCALAR|ARRAYREF,
+ optional => 1,
+ },
+ maintainer => {type => SCALAR|ARRAYREF,
+ optional => 1,
+ },
+ ),
+ links_only => {type => BOOLEAN,
+ default => 0,
+ },
+ class => {type => SCALAR,
+ default => '',
+ },
+ separator => {type => SCALAR,
+ default => ', ',
+ },
+ options => {type => HASHREF,
+ default => {},
+ },
+ };
+ my %param = validate_with(params => \@_,
+ spec => $spec,
+ );
+ my %options = %{$param{options}};
+ for (grep {$package_links_invalid_options{$_}} keys %options) {
+ delete $options{$_};
+ }
+ ## remove aliases for source and maintainer
+ if (exists $param{source}) {
+ $param{src} = [exists $param{src}?make_list($param{src}):(),
+ make_list($param{source}),
+ ];
+ delete $param{source};
+ }
+ if (exists $param{maintainer}) {
+ $param{maint} = [exists $param{maint}?make_list($param{maint}):(),
+ make_list($param{maintainer}),
+ ];
+ delete $param{maintainer};
+ }
+ my $has_options = keys %options;
+ my @links = ();
+ for my $type (qw(src package)) {
+ next unless exists $param{$type};
+ for my $target (make_list($param{$type})) {
+ my $t_type = $type;
+ if ($target =~ s/^src://) {
+ $t_type = 'source';
+ } elsif ($t_type eq 'source') {
+ $target = 'src:'.$target;
+ }
+ if ($has_options) {
+ push @links,
+ (munge_url('pkgreport.cgi?',
+ %options,
+ $t_type => $target,
+ ),
+ $target);
+ } else {
+ push @links,
+ ('pkgreport.cgi?'.$t_type.'='.uri_escape_utf8($target),
+ $target);
+ }
+ }
+ }
+ for my $type (qw(maint owner submitter correspondent)) {
+ next unless exists $param{$type};
+ for my $target (make_list($param{$type})) {
+ my $addr = getparsedaddrs($target);
+ $addr = defined $addr?$addr->address:'';
+ if ($has_options) {
+ push @links,
+ (munge_url('pkgreport.cgi?',
+ %options,
+ $type => $target),
+ $target);
+ } else {
+ push @links,
+ ('pkgreport.cgi?'.
+ $type.'='.uri_escape_utf8($target),
+ $target);
+ }
+ }
+ }
+ my @return = ();
+ my ($link,$link_name);
+ my $class = '';
+ if (length $param{class}) {
+ $class = q( class=").html_escape($param{class}).q(");
+ }
+ while (($link,$link_name) = splice(@links,0,2)) {
+ if ($param{links_only}) {
+ push @return,$link
+ }
+ else {
+ push @return,
+ qq(<a$class href=").
+ html_escape($link).q(">).
+ html_escape($link_name).q(</a>);
+ }
+ }
+ if (wantarray) {
+ return @return;
+ }
+ else {
+ return join($param{separator},@return);
+ }
+}
+
+=head2 bug_links
+
+ join(', ', bug_links(bug => \@packages))
+
+Given a list of bugs, return a list of html which links to the bugs
+
+=over
+
+=item bug -- arrayref or scalar of bug(s)
+
+=item links_only -- return only links, not htmlized links, defaults to
+returning htmlized links.
+
+=item class -- class of the a href, defaults to ''
+
+=back
+
+=cut
+
+sub bug_links {
+ state $spec = {bug => {type => SCALAR|ARRAYREF,
+ optional => 1,
+ },
+ links_only => {type => BOOLEAN,
+ default => 0,
+ },
+ class => {type => SCALAR,
+ default => '',
+ },
+ separator => {type => SCALAR,
+ default => ', ',
+ },
+ options => {type => HASHREF,
+ default => {},
+ },
+ };
+ my %param = validate_with(params => \@_,
+ spec => $spec,
+ );
+ my %options = %{$param{options}};
+
+ for (qw(bug)) {
+ delete $options{$_} if exists $options{$_};
+ }
+ my $has_options = keys %options;
+ my @links;
+ if ($has_options) {
+ push @links, map {(munge_url('bugreport.cgi?',
+ %options,
+ bug => $_,
+ ),
+ $_);
+ } make_list($param{bug}) if exists $param{bug};
+ } else {
+ push @links,
+ map {my $b = ceil($_);
+ ('bugreport.cgi?bug='.$b,
+ $b)}
+ grep {looks_like_number($_)}
+ make_list($param{bug}) if exists $param{bug};
+ }
+ my @return;
+ my ($link,$link_name);
+ my $class = '';
+ if (length $param{class}) {
+ $class = q( class=").html_escape($param{class}).q(");
+ }
+ while (($link,$link_name) = splice(@links,0,2)) {
+ if ($param{links_only}) {
+ push @return,$link
+ }
+ else {
+ push @return,
+ qq(<a$class href=").
+ html_escape($link).q(">).
+ html_escape($link_name).q(</a>);
+ }
+ }
+ if (wantarray) {
+ return @return;
+ }
+ else {
+ return join($param{separator},@return);
+ }
+}
+
+
+
+=head2 maybelink
+
+ maybelink($in);
+ maybelink('http://foobarbaz,http://bleh',qr/[, ]+/);
+ maybelink('http://foobarbaz,http://bleh',qr/[, ]+/,', ');
+
+
+In the first form, links the link if it looks like a link. In the
+second form, first splits based on the regex, then reassembles the
+link, linking things that look like links. In the third form, rejoins
+the split links with commas and spaces.
+
+=cut
+
+sub maybelink {
+ my ($links,$regex,$join) = @_;
+ if (not defined $regex and not defined $join) {
+ $links =~ s{(.*?)((?:(?:ftp|http|https)://[\S~-]+?/?)?)([\)\'\:\.\,]?(?:\s|\.<|$))}
+ {html_escape($1).(length $2?q(<a href=").html_escape($2).q(">).html_escape($2).q(</a>):'').html_escape($3)}geimo;
+ return $links;
+ }
+ $join = ' ' if not defined $join;
+ my @return;
+ my @segments;
+ if (defined $regex) {
+ @segments = split $regex, $links;
+ }
+ else {
+ @segments = ($links);
+ }
+ for my $in (@segments) {
+ if ($in =~ /^[a-zA-Z0-9+.-]+:/) { # RFC 1738 scheme
+ push @return, qq{<a href="$in">} . html_escape($in) . '</a>';
+ } else {
+ push @return, html_escape($in);
+ }
+ }
+ return @return?join($join,@return):'';
+}
+
+
+=head2 htmlize_addresslinks
+
+ htmlize_addresslinks($prefixfunc,$urlfunc,$addresses,$class);
+
+
+Generate a comma-separated list of HTML links to each address given in
+$addresses, which should be a comma-separated list of RFC822
+addresses. $urlfunc should be a reference to a function like mainturl
+or submitterurl which returns the URL for each individual address.
+
+
+=cut
+
+sub htmlize_addresslinks {
+ my ($prefixfunc, $urlfunc, $addresses,$class) = @_;
+ carp "htmlize_addresslinks is deprecated";
+
+ $class = defined $class?qq(class="$class" ):'';
+ if (defined $addresses and $addresses ne '') {
+ my @addrs = getparsedaddrs($addresses);
+ my $prefix = (ref $prefixfunc) ?
+ $prefixfunc->(scalar @addrs):$prefixfunc;
+ return $prefix .
+ join(', ', map
+ { sprintf qq(<a ${class}).
+ 'href="%s">%s</a>',
+ $urlfunc->($_->address),
+ html_escape($_->format) ||
+ '(unknown)'
+ } @addrs
+ );
+ }
+ else {
+ my $prefix = (ref $prefixfunc) ?
+ $prefixfunc->(1) : $prefixfunc;
+ return sprintf '%s<a '.$class.'href="%s">(unknown)</a>',
+ $prefix, $urlfunc->('');
+ }
+}
+
+sub emailfromrfc822{
+ my $addr = getparsedaddrs($_[0] || "");
+ $addr = defined $addr?$addr->address:'';
+ return $addr;
+}
+
+sub mainturl { package_links(maintainer => $_[0], links_only => 1); }
+sub submitterurl { package_links(submitter => $_[0], links_only => 1); }
+sub htmlize_maintlinks {
+ my ($prefixfunc, $maints) = @_;
+ carp "htmlize_maintlinks is deprecated";
+ return htmlize_addresslinks($prefixfunc, \&mainturl, $maints);
+}
+
+=head2 bug_linklist
+
+ bug_linklist($separator,$class,@bugs)
+
+Creates a set of links to C<@bugs> separated by C<$separator> with
+link class C<$class>.
+
+XXX Use L<Params::Validate>; we want to be able to support query
+arguments here too; we should be able to combine bug_links and this
+function into one.
+
+=cut
+
+
+sub bug_linklist{
+ my ($sep,$class,@bugs) = @_;
+ carp "bug_linklist is deprecated; use bug_links instead";
+ return scalar bug_links(bug=>\@bugs,class=>$class,separator=>$sep);
+}
+
+
+sub add_user {
+ my ($user,$usertags,$bug_usertags,$seen_users,$cats,$hidden) = @_;
+ $seen_users = {} if not defined $seen_users;
+ $bug_usertags = {} if not defined $bug_usertags;
+ $usertags = {} if not defined $usertags;
+ $cats = {} if not defined $cats;
+ $hidden = {} if not defined $hidden;
+ return if exists $seen_users->{$user};
+ $seen_users->{$user} = 1;
+
+ my $u = Debbugs::User::get_user($user);
+
+ my %vis = map { $_, 1 } @{$u->{"visible_cats"}};
+ for my $c (keys %{$u->{"categories"}}) {
+ $cats->{$c} = $u->{"categories"}->{$c};
+ $hidden->{$c} = 1 unless defined $vis{$c};
+ }
+ for my $t (keys %{$u->{"tags"}}) {
+ $usertags->{$t} = [] unless defined $usertags->{$t};
+ push @{$usertags->{$t}}, @{$u->{"tags"}->{$t}};
+ }
+
+ %{$bug_usertags} = ();
+ for my $t (keys %{$usertags}) {
+ for my $b (@{$usertags->{$t}}) {
+ $bug_usertags->{$b} = [] unless defined $bug_usertags->{$b};
+ push @{$bug_usertags->{$b}}, $t;
+ }
+ }
+}
+
+
+
+=head1 Forms
+
+=cut
+
+=head2 form_options_and_normal_param
+
+ my ($form_option,$param) = form_options_and_normal_param(\%param)
+ if $param{form_options};
+ my $form_option = form_options_and_normal_param(\%param)
+ if $param{form_options};
+
+Translates from special form_options to a set of parameters which can
+be used to run the current page.
+
+The idea behind this is to allow complex forms to relatively easily
+cause options that the existing cgi scripts understand to be set.
+
+Currently there are two commands which are understood:
+combine, and concatenate.
+
+=head3 combine
+
+Combine works by entering key,value pairs into the parameters using
+the key field option input field, and the value field option input
+field.
+
+For example, you would have
+
+ <input type="hidden" name="_fo_combine_key_fo_searchkey_value_fo_searchvalue" value="1">
+
+which would combine the _fo_searchkey and _fo_searchvalue input fields, so
+
+ <input type="text" name="_fo_searchkey" value="foo">
+ <input type="text" name="_fo_searchvalue" value="bar">
+
+would yield foo=>'bar' in %param.
+
+=head3 concatenate
+
+Concatenate concatenates values into a single entry in a parameter
+
+For example, you would have
+
+ <input type="hidden" name="_fo_concatentate_into_foo_with_:_fo_blah_fo_bleargh" value="1">