PageRenderTime 43ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/source/cgi-bin/gnatsweb.pl

https://gitlab.com/garrett/gcc-website
Perl | 3704 lines | 3589 code | 37 blank | 78 comment | 21 complexity | 895f4fe6a5c5cb8025aefda8bcc093bc MD5 | raw file
  1. #!/usr/bin/perl -w
  2. #
  3. # Gnatsweb - web front-end to gnats
  4. #
  5. # Copyright 1998-1999 - Matt Gerassimoff
  6. # and Ken Cox
  7. #
  8. # $Id: gnatsweb.pl,v 1.1.1.1.2.31 2001/11/26 10:59:48 yngves Exp $
  9. #
  10. #-----------------------------------------------------------------------------
  11. # Site-specific customization -
  12. #
  13. # WE STRONGLY SUGGEST you don't edit these variables here, but instead
  14. # put them in a file called 'gnatsweb-site.pl' in the same directory.
  15. # That way, when a new version of gnatsweb is released, you won't
  16. # need to edit them again.
  17. #
  18. # Info about your gnats host.
  19. $site_gnats_host = 'localhost';
  20. $site_gnats_port = 1529;
  21. # Set to true if you compiled gnats with GNATS_RELEASE_BASED defined.
  22. $site_release_based = 0;
  23. # Name you want in the page banner and banner color.
  24. $site_banner_text = 'gnatsweb';
  25. $site_banner_background = '#000000';
  26. $site_banner_foreground = '#ffffff';
  27. $site_button_foreground = '#ffffff';
  28. $site_button_background = '#000000';
  29. # Page background color -- not used unless defined.
  30. #$site_background = '#c0c0c0';
  31. #GCC-LOCAL begin.
  32. $site_background = '#ffffff';
  33. #GCC-LOCAL end.
  34. # Uncomment the following line and insert stylesheet URL in order to
  35. # link all generated pages to an external stylesheet. Both absolute
  36. # and relative URLs are supported.
  37. #$site_stylesheet='http://url.of/stylesheet';
  38. # When $include_audit_trail is set to 1, the Audit-Trail will be
  39. # visible by default in the View PR screen. Sites that expect large
  40. # Audit-Trails, i.e. lot of mail back and forth etc., will want to set
  41. # this to 0.
  42. $include_audit_trail = 1;
  43. # Have the HTTP header, start_html, heading already been printed?
  44. my $print_header_done = 0;
  45. my $page_start_html_done = 0;
  46. my $page_heading_done = 0;
  47. # Program to send email notifications.
  48. if (-x '/usr/sbin/sendmail')
  49. {
  50. $site_mailer = '/usr/sbin/sendmail -oi -t';
  51. }
  52. elsif (-x '/usr/lib/sendmail')
  53. {
  54. $site_mailer = '/usr/lib/sendmail -oi -t';
  55. }
  56. else
  57. {
  58. die("Can't locate 'sendmail'; must set \$site_mailer in gnats-site.pl");
  59. }
  60. # site_callback -
  61. #
  62. # If defined, this subroutine gets called at various times. The
  63. # reason it is being called is indicated by the $reason argument.
  64. # It can return undef, in which case gnatsweb does its default
  65. # thing. Or, it can return a piece of HTML to implement
  66. # site-specific behavior or appearance.
  67. #
  68. # Sorry, the reasons are not documented. Either put a call to
  69. # 'warn' into your gnats-site.pl file, or search this file for 'cb('.
  70. # For examples of some of the things you can do with the site_callback
  71. # subroutine, see gnatsweb-site-sente.pl.
  72. #
  73. # arguments:
  74. # $reason - reason for the call. Each reason is unique.
  75. # @args - additional parameters may be provided in @args.
  76. #
  77. # returns:
  78. # undef - take no special action
  79. # string - string is used by gnatsweb according to $reason
  80. #
  81. # example:
  82. # See gnatsweb-site-sente.pl for an extended example.
  83. #
  84. # sub site_callback {
  85. # my($reason, @args) = @_;
  86. # if ($reason eq 'sendpr_description') {
  87. # return 'default description text used in sendpr form';
  88. # }
  89. # undef;
  90. # }
  91. #
  92. # end customization
  93. #-----------------------------------------------------------------------------
  94. # Use CGI::Carp first, so that fatal errors come to the browser, including
  95. # those caused by old versions of CGI.pm.
  96. use CGI::Carp qw/fatalsToBrowser/;
  97. # 8/22/99 kenstir: CGI.pm-2.50's file upload is broken.
  98. # 9/19/99 kenstir: CGI.pm-2.55's file upload is broken.
  99. use CGI 2.56 qw(-oldstyle_urls :all);
  100. use gnats qw/client_init client_exit client_cmd/;
  101. use Text::Tabs;
  102. # Debugging fresh code.
  103. #$gnats::DEBUG_LEVEL = 2;
  104. # Version number + RCS revision number
  105. $VERSION = '2.9.3';
  106. $REVISION = (split(/ /, '$Revision: 1.1.1.1.2.31 $ '))[1];
  107. # width of text fields
  108. $textwidth = 60;
  109. # where to get help -- a web site with translated info documentation
  110. $gnats_info_top = 'http://www.gnu.org/software/gnats/gnats_toc.html';
  111. #GCC-LOCAL begin.
  112. $gnats_info_top = '/gnats.html';
  113. #GCC-LOCAL begin.
  114. # bits in %fieldnames has (set=yes not-set=no)
  115. $MULTILINE = 1; # whether field is multi line
  116. $SENDEXCLUDE = 2; # whether the send command should exclude the field
  117. $REASONCHANGE = 4; # whether change to a field requires reason
  118. $ENUM = 8; # whether field should be displayed as enumerated
  119. $EDITEXCLUDE = 16; # if set, don't display on edit page
  120. $AUDITINCLUDE = 32; # if set, save changes in Audit-Trail
  121. $| = 1; # flush output after each print
  122. # Return true if module MIME::Base64 is available. If available, it's
  123. # loaded the first time this sub is called.
  124. sub can_do_mime
  125. {
  126. return $can_do_mime if (defined($can_do_mime));
  127. # Had to basically implement 'require' myself here, otherwise perl craps
  128. # out into the browser window if you don't have the MIME::Base64 package.
  129. #$can_do_mime = eval 'require MIME::Base64';
  130. ITER: {
  131. foreach my $dir (@INC) {
  132. my $filename = "$dir/MIME/Base64.pm";
  133. if (-f $filename) {
  134. do $filename;
  135. die $@ if $@;
  136. $can_do_mime = 1;
  137. last ITER;
  138. }
  139. }
  140. $can_do_mime = 0;
  141. }
  142. #warn "NOTE: Can't use file upload feature without MIME::Base64 module\n";
  143. return $can_do_mime;
  144. }
  145. # Take the file attachment's file name, and return only the tail. Don't
  146. # want to store any path information, for security and clarity. Support
  147. # both DOS-style and Unix-style paths here, because we have both types of
  148. # clients.
  149. sub attachment_filename_tail
  150. {
  151. my($filename) = @_;
  152. $filename =~ s,.*/,,; # Remove leading Unix path elements.
  153. $filename =~ s,.*\\,,; # Remove leading DOS path elements.
  154. return $filename;
  155. }
  156. # Retrieve uploaded file attachment. Return it as
  157. # ($filename, $content_type, $data). Returns (undef,undef,undef)
  158. # if not present.
  159. #
  160. # See 'perldoc CGI' for details about this code.
  161. sub get_attachment
  162. {
  163. my $upload_param_name = shift;
  164. my $debug = 0;
  165. my $filename = $q->param($upload_param_name);
  166. return (undef, undef, undef) unless $filename;
  167. # 9/6/99 kenstir: My testing reveals that if uploadInfo returns undef,
  168. # then you can't read the file either.
  169. warn "get_attachment: filename=$filename\n" if $debug;
  170. my $hashref = $q->uploadInfo($filename);
  171. if (!defined($hashref)) {
  172. die "Got attachment filename ($filename) but no attachment data! Probably this is a programming error -- the form which submitted this data must be multipart/form-data (start_multipart_form()).";
  173. }
  174. if ($debug) {
  175. while (($k, $v) = each %$hashref) {
  176. warn "get_attachment: uploadInfo($k)=$v\n";
  177. }
  178. }
  179. # 9/6/99 kenstir: When testing locally on Linux, a .gz file yielded
  180. # no Content-Type. Therefore, have to assume binary. Would like to
  181. # check (-B $fh) to see if the stream is binary but that doesn't work.
  182. my $ctype = $hashref->{'Content-Type'} || 'application/octet-stream';
  183. warn "get_attachment: Content-Type=$ctype\n" if $debug;
  184. my $data = '';
  185. my $buf;
  186. my $fh = $q->upload($upload_param_name);
  187. warn "get_attachment: fh=$fh\n" if $debug;
  188. while (read($fh, $buf, 1024)) {
  189. $data .= $buf;
  190. }
  191. close $fh;
  192. return ($filename, $ctype, $data);
  193. }
  194. # Retrieve uploaded file attachment, and encode it so that it's
  195. # printable, for inclusion into the PR text.
  196. #
  197. # Returns the printable text representing the attachment. Returns '' if
  198. # the attachment was not present.
  199. sub encode_attachment
  200. {
  201. my $upload_param_name = shift;
  202. my $debug = 0;
  203. return '' unless can_do_mime();
  204. my ($filename, $ctype, $data) = get_attachment($upload_param_name);
  205. return '' unless $filename;
  206. # Strip off path elements in $filename.
  207. $filename = attachment_filename_tail($filename);
  208. warn "encode_attachment: $filename was ", length($data), " bytes of $ctype\n"
  209. if $debug;
  210. my $att = '';
  211. # Plain text is included inline; all else is encoded.
  212. $att .= "Content-Type: $ctype; name=\"$filename\"\n";
  213. if ($ctype eq 'text/plain') {
  214. $att .= "Content-Disposition: inline; filename=\"$filename\"\n\n";
  215. $att .= $data;
  216. }
  217. else {
  218. $att .= "Content-Transfer-Encoding: base64\n";
  219. $att .= "Content-Disposition: attachment; filename=\"$filename\"\n\n";
  220. $att .= MIME::Base64::encode_base64($data);
  221. }
  222. warn "encode_attachment: done\n" if $debug;
  223. return $att;
  224. }
  225. # Takes the encoded file attachment, decodes it and returns it as a hashref.
  226. sub decode_attachment
  227. {
  228. my $att = shift;
  229. my $debug = 0;
  230. my $hash_ref = {'original_attachment' => $att};
  231. # Split the envelope from the body.
  232. my ($envelope, $body) = split(/\n\n/, $att, 2);
  233. return $hash_ref unless ($envelope && $body);
  234. # Split mbox-like headers into (header, value) pairs, with a leading
  235. # "From_" line swallowed into USELESS_LEADING_ENTRY. Junk the leading
  236. # entry. Chomp all values.
  237. %$hash_ref = (USELESS_LEADING_ENTRY => split /^(\S*?):\s*/m, $envelope);
  238. delete($hash_ref->{USELESS_LEADING_ENTRY});
  239. for (keys %$hash_ref) {
  240. chomp $hash_ref->{$_};
  241. }
  242. # Keep the original_attachment intact.
  243. $$hash_ref{'original_attachment'} = $att;
  244. if (!$$hash_ref{'Content-Type'}
  245. || !$$hash_ref{'Content-Disposition'})
  246. {
  247. die "Unable to parse file attachment";
  248. }
  249. # Parse filename.
  250. # Note: the extra \ before the " is just so that perl-mode can parse it.
  251. if ($$hash_ref{'Content-Disposition'} !~ /(\S+);\s*filename=\"([^\"]+)\"/) {
  252. die "Unable to parse file attachment Content-Disposition";
  253. }
  254. $$hash_ref{'filename'} = attachment_filename_tail($2);
  255. # Decode the data if encoded.
  256. if (exists($$hash_ref{'Content-Transfer-Encoding'})
  257. && $$hash_ref{'Content-Transfer-Encoding'} eq 'base64')
  258. {
  259. $$hash_ref{'data'} = MIME::Base64::decode_base64($body);
  260. }
  261. else {
  262. $$hash_ref{'data'} = $body;
  263. }
  264. return $hash_ref;
  265. }
  266. # Print file attachment browser and buttons to download the attachments.
  267. # Which of these appear depend on the mode.
  268. sub print_attachments
  269. {
  270. my($fields_hash_ref, $mode) = @_;
  271. return unless can_do_mime();
  272. print "<tr><td valign=top><b>File Attachments:</b></td>\n<td>";
  273. # Add file upload button for adding new attachment.
  274. if ($mode eq 'sendpr' || $mode eq 'edit') {
  275. print "Add a file attachment:<br>",
  276. $q->filefield(-name=>'attached_file',
  277. -size=>50);
  278. }
  279. # Print table of existing attachments.
  280. # Add column with delete button in edit mode.
  281. my $array_ref = $$fields_hash_ref{'attachments'};
  282. my $table_rows_aref = [];
  283. my $i = 0;
  284. foreach $hash_ref (@$array_ref) {
  285. my $size = int(length($$hash_ref{'data'}) / 1024.0);
  286. $size = 1 if ($size < 1);
  287. my $row_data = $q->td( [ $q->submit('cmd', "download attachment $i"),
  288. $$hash_ref{'filename'},
  289. "${size}k" ] );
  290. $row_data .= $q->td($q->checkbox(-name=>'delete attachments',
  291. -value=>$i,
  292. -label=>"delete attachment $i"))
  293. if ($mode eq 'edit');
  294. push(@$table_rows_aref, $row_data);
  295. $i++;
  296. }
  297. if (@$table_rows_aref)
  298. {
  299. my $header_row_data = $q->th( ['download','filename','size' ] );
  300. $header_row_data .= $q->th('delete')
  301. if ($mode eq 'edit');
  302. print $q->table({-border=>1},
  303. $q->Tr($header_row_data),
  304. $q->Tr($table_rows_aref));
  305. }
  306. }
  307. # The user has requested download of a particular attachment.
  308. # Serve it up.
  309. sub download_attachment
  310. {
  311. my $attachment_number = shift;
  312. my($pr) = $q->param('pr');
  313. die "download_attachment called with no PR number"
  314. if(!$pr);
  315. my(%fields) = readpr($pr);
  316. my $array_ref = $fields{'attachments'};
  317. my $hash_ref = $$array_ref[$attachment_number];
  318. # Determine the attachment's content type.
  319. my $ct = $$hash_ref{'Content-Type'} || 'application/octet-stream';
  320. $ct =~ s~\s*;.*~~s;
  321. my $disp;
  322. # Internet Explorer 5.5 does not handle "content-disposition: attachment"
  323. # in the expected way. It needs a content-disposition of "file".
  324. ($ENV{'HTTP_USER_AGENT'} =~ "MSIE 5.5") ? ($disp = 'file') : ($disp = 'attachment');
  325. # Now serve the attachment, with the appropriate headers.
  326. print $q->header(-type => $ct,
  327. -content_disposition => "$disp; filename=\"$$hash_ref{'filename'}\""),
  328. $$hash_ref{'data'};
  329. }
  330. # Add the given (gnatsweb-encoded) attachment to the %fields hash.
  331. sub add_encoded_attachment_to_pr
  332. {
  333. my($fields_hash_ref, $encoded_attachment) = @_;
  334. return unless $encoded_attachment;
  335. my $ary_ref = $$fields_hash_ref{'attachments'} || [];
  336. my $hash_ref = { 'original_attachment' => $encoded_attachment };
  337. push(@$ary_ref, $hash_ref);
  338. $$fields_hash_ref{'attachments'} = $ary_ref;
  339. }
  340. # Add the given (gnatsweb-decoded) attachment to the %fields hash.
  341. sub add_decoded_attachment_to_pr
  342. {
  343. my($fields_hash_ref, $decoded_attachment_hash_ref) = @_;
  344. return unless $decoded_attachment_hash_ref;
  345. my $ary_ref = $$fields_hash_ref{'attachments'} || [];
  346. push(@$ary_ref, $decoded_attachment_hash_ref);
  347. $$fields_hash_ref{'attachments'} = $ary_ref;
  348. }
  349. # Remove the given attachments from the %fields hash.
  350. sub remove_attachments_from_pr
  351. {
  352. my($fields_hash_ref, @attachment_numbers) = @_;
  353. return unless @attachment_numbers;
  354. my $ary_ref = $$fields_hash_ref{'attachments'} || [];
  355. foreach my $attachment_number (@attachment_numbers)
  356. {
  357. # Remove the attachment be replacing it with the empty hash.
  358. # The sub unparsepr skips these.
  359. $$ary_ref[$attachment_number] = {};
  360. }
  361. }
  362. # wrapper functions for formstart...
  363. sub multipart_form_start
  364. {
  365. formstart(1, @_);
  366. }
  367. sub form_start
  368. {
  369. formstart(0, @_);
  370. }
  371. # workaround for an exceedingly dumb netscape bug. we hates
  372. # netscape... this bug manifests if you click on the "create"
  373. # button bar link (but not the grey button on the main page), submit a
  374. # PR, then hit the back button (usually because you got an error).
  375. # you're taken "back" to the same error page -- all the stuff you
  376. # entered into the submission form is *gone*. this is kind of annoying...
  377. # (it also manifests if you click the edit link from the query results page.)
  378. sub formstart
  379. {
  380. # this bugfix is mostly lifted from the CGI.pm docs. here's what they
  381. # have to say:
  382. # When you press the "back" button, the same page is loaded, not
  383. # the previous one. Netscape's history list gets confused
  384. # when processing multipart forms. If the script generates
  385. # different pages for the form and the results, hitting the
  386. # "back" button doesn't always return you to the previous page;
  387. # instead Netscape reloads the current page. This happens even
  388. # if you don't use an upload file field in your form.
  389. #
  390. # A workaround for this is to use additional path information to
  391. # trick Netscape into thinking that the form and the response
  392. # have different URLs. I recommend giving each form a sequence
  393. # number and bumping the sequence up by one each time the form
  394. # is accessed:
  395. # should we do multipart?
  396. my $multi = shift;
  397. # in case the caller has some args to pass...
  398. my %args = @_;
  399. # if the caller has given an "action" arg, we don't do any
  400. # subterfuge. let the caller worry about the bug...
  401. if (!exists $args{'-action'})
  402. {
  403. # get sequence number and increment it
  404. my $s = $q->path_info =~ m{/(\d+)/?$};
  405. $s++;
  406. # Trick Netscape into thinking it's loading a new script:
  407. $args{-action} = $q->script_name . "/$s";
  408. }
  409. if ($multi)
  410. {
  411. print $q->start_multipart_form(%args);
  412. }
  413. else
  414. {
  415. print $q->start_form(%args);
  416. }
  417. return;
  418. }
  419. # sendpr -
  420. # The Create PR page.
  421. #
  422. sub sendpr
  423. {
  424. my $page = 'Create PR';
  425. print_header();
  426. page_start_html($page);
  427. page_heading($page, 'Create Problem Report', 1);
  428. # remove "all" from arrays
  429. shift(@category);
  430. shift(@severity);
  431. shift(@priority);
  432. shift(@class);
  433. shift(@confidential);
  434. shift(@responsible);
  435. shift(@state);
  436. shift(@submitter_id);
  437. # Add '<default>' to @responsible, in case the site_callback alows
  438. # Responsible to be set upon submission. This is filtered out in
  439. # &submitnewpr.
  440. unshift(@responsible, '<default>');
  441. print multipart_form_start(-name=>'sendPrForm'), "\n",
  442. hidden_db(),
  443. $q->p($q->submit('cmd', 'submit'),
  444. " or ",
  445. $q->reset(-name=>'reset')),
  446. $q->hidden(-name=>'return_url'),
  447. "<hr>\n",
  448. "<table>";
  449. my $def_email = $global_prefs{'email'}
  450. || cb('get_default_value', 'email') || '';
  451. print "<tr>\n<td><b>Reporter's email:</b></td>\n<td>",
  452. $q->textfield(-name=>'email',
  453. -default=>$def_email,
  454. -size=>$textwidth),
  455. "</td>\n</tr>\n<tr>\n<td><b>CC these people<br>on PR status email:</b></td>\n<td>",
  456. $q->textfield(-name=>'X-GNATS-Notify',
  457. -size=>$textwidth),
  458. # a blank row, to separate header info from PR info
  459. "</td>\n</tr>\n<tr>\n<td>&nbsp;</td>\n<td>&nbsp;</td>\n</tr>\n";
  460. foreach (@fieldnames)
  461. {
  462. next if ($fieldnames{$_} & $SENDEXCLUDE);
  463. my $lc_fieldname = field2param($_);
  464. # Get default value from site_callback if provided, otherwise take
  465. # our defaults.
  466. my $default;
  467. $default = 'serious' if /Severity/;
  468. $default = 'medium' if /Priority/;
  469. $default = $global_prefs{'Submitter-Id'} || 'unknown' if /Submitter-Id/;
  470. #GCC-LOCAL begin.
  471. $default = 'net' if /Submitter-Id/;
  472. #GCC-LOCAL end.
  473. $default = $global_prefs{'Originator'} if /Originator/;
  474. $default = grep(/^unknown$/i, @category) ? "unknown" : $category[0]
  475. if /Category/;
  476. $default = $config{'DEFAULT_RELEASE'} if /Release/;
  477. $default = '' if /Responsible/;
  478. $default = cb("sendpr_$lc_fieldname") || $default;
  479. # The "intro" provides a way for the site callback to print something
  480. # at the top of a given field.
  481. my $intro = cb("sendpr_intro_$lc_fieldname") || '';
  482. if ($fieldnames{$_} & $ENUM)
  483. {
  484. if ($lc_fieldname eq "category")
  485. {
  486. print "<tr>\n<td><b>$_:</b>\n</td>\n<td>",
  487. $intro,
  488. $q->popup_menu(-name=>$_,
  489. -values=>\@$lc_fieldname,
  490. -labels=>\%category_desc,
  491. -default=>$default);
  492. print "</td>\n</tr>\n";
  493. }
  494. elsif ($lc_fieldname eq "responsible")
  495. {
  496. print "<tr>\n<td><b>$_:</b>\n</td>\n<td>",
  497. $intro,
  498. $q->popup_menu(-name=>$_,
  499. -values=>\@$lc_fieldname,
  500. -labels=>\%responsible_fullname,
  501. -default=>$fields{$_});
  502. print "</td>\n</tr>\n";
  503. } else
  504. {
  505. print "<tr>\n<td><b>$_:</b>\n</td>\n<td>",
  506. $intro,
  507. $q->popup_menu(-name=>$_,
  508. -values=>\@$lc_fieldname,
  509. -default=>$default);
  510. print "</td>\n</tr>\n";
  511. }
  512. }
  513. elsif ($fieldnames{$_} & $MULTILINE)
  514. {
  515. my $rows = 4;
  516. $rows = 8 if /Description/;
  517. $rows = 2 if /Environment/;
  518. #GCC-LOCAL begin.
  519. if ($lc_fieldname eq "description")
  520. {
  521. printf "<tr>\n<td></td>\n<td>When you provide (preprocessed) source, "
  522. ."please only insert it into one of the text fields if it is "
  523. ."very small, say below 50 lines. Else <em>please</em> attach "
  524. ."it as a file (see below).";
  525. }
  526. #GCC-LOCAL end.
  527. print "<tr>\n<td valign=top><b>$_:</b></td>\n<td>",
  528. $intro,
  529. $q->textarea(-name=>$_,
  530. -cols=>$textwidth,
  531. -rows=>$rows,
  532. -default=>$default);
  533. # Create file upload button after Description.
  534. print_attachments(\%fields, 'sendpr') if /Description/;
  535. print "</td>\n</tr>\n";
  536. }
  537. else
  538. {
  539. print "<tr>\n<td><b>$_:</b></td>\n<td>",
  540. $intro,
  541. $q->textfield(-name=>$_,
  542. -size=>$textwidth,
  543. -default=>$default);
  544. print "</td>\n</tr>\n";
  545. }
  546. print "\n";
  547. }
  548. print "</table>",
  549. $q->p($q->submit('cmd', 'submit'),
  550. " or ",
  551. $q->reset(-name=>'reset')),
  552. $q->end_form();
  553. page_footer($page);
  554. page_end_html($page);
  555. }
  556. # validate_email_field -
  557. # Used by validate_new_pr to check email address fields in a new PR.
  558. sub validate_email_field
  559. {
  560. my($fieldname, $fieldval, $required) = @_;
  561. my $blank = ($fieldval =~ /^\s*$/);
  562. if ($required && $blank)
  563. {
  564. return "$fieldname is blank";
  565. }
  566. # From rkimball@vgi.com, allows @ only if it's followed by what looks
  567. # more or less like a domain name.
  568. my $email_addr = '[^@\s]+(@\S+\.\S+)?';
  569. if (!$blank && $fieldval !~ /^\s*($email_addr\s*)+$/)
  570. {
  571. return "'$fieldval' doesn't look like a valid email address (xxx\@xxx.xxx)";
  572. }
  573. return '';
  574. }
  575. # validate_new_pr -
  576. # Make sure fields have reasonable values before submitting a new PR.
  577. sub validate_new_pr
  578. {
  579. my(%fields) = @_;
  580. my(@errors) = ();
  581. my $err;
  582. # validate email fields
  583. $err = validate_email_field('E-mail Address', $fields{'email'}, 'required');
  584. push(@errors, $err) if $err;
  585. # $err = validate_email_field('CC', $fields{'cc'});
  586. # push(@errors, $err) if $err;
  587. $err = validate_email_field('X-GNATS-Notify', $fields{'X-GNATS-Notify'});
  588. push(@errors, $err) if $err;
  589. # validate some other fields
  590. push(@errors, "Category is blank or 'unknown'")
  591. if($fields{'Category'} =~ /^\s*$/ || $fields{'Category'} eq "unknown");
  592. push(@errors, "Synopsis is blank")
  593. if($fields{'Synopsis'} =~ /^\s*$/);
  594. push(@errors, "Release is blank")
  595. if($fields{'Release'} =~ /^\s*$/);
  596. push(@errors, "Submitter-Id is 'unknown'")
  597. if($fields{'Submitter-Id'} eq 'unknown');
  598. #GCC-LOCAL begin.
  599. push(@errors, "Priority is 'high'")
  600. if($fields{'Priority'} eq 'high');
  601. #GCC-LOCAL end.
  602. @errors;
  603. }
  604. sub submitnewpr
  605. {
  606. my $page = 'Create PR Results';
  607. my $debug = 0;
  608. my(@values, $key);
  609. my(%fields);
  610. foreach $key ($q->param)
  611. {
  612. my $val = $q->param($key);
  613. if($fieldnames{$key} && ($fieldnames{$key} & $MULTILINE))
  614. {
  615. $val = fix_multiline_val($val);
  616. }
  617. $fields{$key} = $val;
  618. }
  619. # If Responsible is '<default>', delete it; gnats handles that. See
  620. # also &sendpr.
  621. if(defined($fields{'Responsible'}) && $fields{'Responsible'} eq '<default>') {
  622. delete $fields{'Responsible'};
  623. }
  624. # Make sure the pr is valid.
  625. my(@errors) = validate_new_pr(%fields);
  626. if (@errors)
  627. {
  628. print_header();
  629. page_start_html($page);
  630. page_heading($page, 'Error');
  631. print "<h3>Your problem report has not been sent.</h3>\n",
  632. "<p>Fix the following problems, then submit the problem report again:</p>",
  633. $q->ul($q->li(\@errors));
  634. return;
  635. }
  636. # Supply a default value for Originator
  637. $fields{'Originator'} = $fields{'Originator'} || $fields{'email'};
  638. # Handle the attached_file, if any.
  639. add_encoded_attachment_to_pr(\%fields, encode_attachment('attached_file'));
  640. # Compose the message
  641. my $text = unparsepr('send', %fields);
  642. $text = <<EOT . $text;
  643. To: $config{'GNATS_ADDR'}
  644. CC: $fields{'X-GNATS-Notify'}
  645. Subject: $fields{'Synopsis'}
  646. From: $fields{'email'}
  647. Reply-To: $fields{'email'}
  648. X-Send-Pr-Version: gnatsweb-$VERSION ($REVISION)
  649. X-GNATS-Notify: $fields{'X-GNATS-Notify'}
  650. EOT
  651. # Allow debugging
  652. if($debug)
  653. {
  654. print_header();
  655. page_start_html($page);
  656. print "<h3>debugging -- PR NOT SENT</h3>",
  657. $q->pre($q->escapeHTML($text)),
  658. "<hr>";
  659. page_end_html($page);
  660. return;
  661. }
  662. # Send the message
  663. if(!open(MAIL, "|$site_mailer"))
  664. {
  665. print_header();
  666. page_start_html($page);
  667. page_heading($page, 'Error');
  668. print "<h3>Error invoking $site_mailer</h3>";
  669. return;
  670. }
  671. print MAIL $text;
  672. if(!close(MAIL))
  673. {
  674. print_header();
  675. page_start_html($page);
  676. page_heading($page, 'Error');
  677. print "<h3>Bad pipe to $site_mailer</h3>";
  678. exit;
  679. }
  680. # Return the user to the page they were viewing when they pressed
  681. # 'create'.
  682. my $return_url = $q->param('return_url') || get_script_name();
  683. my $refresh = 5;
  684. print_header(-Refresh => "$refresh; URL=$return_url",
  685. -cookie => create_global_cookie());
  686. # Workaround for MSIE:
  687. my @args = (-title=>"$page - $site_banner_text");
  688. push(@args, -bgcolor=>$site_background)
  689. if defined($site_background);
  690. push(@args, -style=>{-src=>$site_stylesheet})
  691. if defined($site_stylesheet);
  692. push(@args, -head=>meta({-http_equiv=>'Refresh',
  693. -content=>"$refresh; URL=$return_url"}));
  694. print $q->start_html(@args);
  695. # Print page banner, with button bar, without the <head> part:
  696. page_start_html($page, 0, 1);
  697. page_heading($page, 'Problem Report Sent');
  698. print "<p>Thank you for your report. It will take a short while for
  699. your report to be processed. When it is, you will receive
  700. an automated message about it, containing the Problem Report
  701. number, and the developer who has been assigned to
  702. investigate the problem.</p>";
  703. print "<p>Page will refresh in $refresh seconds...</p>\n";
  704. page_footer($page);
  705. page_end_html($page);
  706. }
  707. # Return a URL which will take one to the specified $pr and with a
  708. # specified $cmd. For commands such as 'create' that have no
  709. # associated PR number, we pass $pr = 0, and this routine then leaves
  710. # out the pr parameter. For ease of use, when the user makes a
  711. # successful edit, we want to return to the URL he was looking at
  712. # before he decided to edit the PR. The return_url param serves to
  713. # store that info, and is included if $include_return_url is
  714. # specified. Note that the return_url is saved even when going into
  715. # the view page, since the user might go from there to the edit page.
  716. #
  717. sub get_pr_url
  718. {
  719. my($cmd, $pr, $include_return_url) = @_;
  720. my $url = $q->url() . "?cmd=$cmd&database=$global_prefs{'database'}";
  721. $url .= "&pr=$pr" if $pr;
  722. $url .= "&return_url=" . $q->escape($q->self_url())
  723. if $include_return_url;
  724. return $url;
  725. }
  726. # Return a URL to edit the given pr. See get_pr_url().
  727. #
  728. sub get_editpr_url
  729. {
  730. return get_pr_url('edit', @_);
  731. }
  732. # Return a URL to view the given pr. See get_pr_url().
  733. #
  734. sub get_viewpr_url
  735. {
  736. my $viewcmd = $include_audit_trail ? 'view%20audit-trail' : 'view';
  737. return get_pr_url($viewcmd, @_);
  738. }
  739. # Return a URL to create a pr. See get_pr_url().
  740. #
  741. sub get_createpr_url
  742. {
  743. return get_pr_url('create', @_);
  744. }
  745. # Same as script_name(), but includes 'database=xxx' param.
  746. #
  747. sub get_script_name
  748. {
  749. my $url = $q->script_name();
  750. $url .= "?database=$global_prefs{'database'}"
  751. if defined($global_prefs{'database'});
  752. return $url;
  753. }
  754. # Return a link which sends email regarding the current PR.
  755. sub get_mailto_link
  756. {
  757. my($pr,%fields) = @_;
  758. # NOTE: cagney/2003-01-31: Don't escape the interested parties
  759. # e-mail list. MOZILLA has a nasty bug were it doesn't re-adjust
  760. # the e-mail list length after de-escaping it. This causes MOZILLA
  761. # to use "?Sub..." in the list of e-mail addresses.
  762. # my $mailto = $q->escape(scalar(interested_parties($pr, 1, %fields)));
  763. my $mailto = interested_parties($pr, 1, %fields);
  764. my $subject = $q->escape("Re: $fields{'Category'}/$pr: $fields{'Synopsis'}");
  765. my $body = $q->escape(get_viewpr_url($pr));
  766. return "<a href=\"mailto:$mailto?Subject=$subject&Body=$body\">"
  767. . "send email to interested parties</a>\n";
  768. }
  769. # Look for text that looks like URLs and turn it into actual links.
  770. sub mark_urls
  771. {
  772. my ($val) = @_;
  773. # This probably doesn't catch all URLs, but one hopes it catches the
  774. # majority.
  775. $val =~ s/\b((s?https?|ftp):\/\/[-a-zA-Z0-9_.]+(:[0-9]+)?[-a-zA-Z0-9_\$.+\!*\(\),;:\@\&\%\x93\x90=?~\#\/]*)/
  776. \<a href="$1">$1\<\/a\>/g;
  777. return $val;
  778. }
  779. sub view
  780. {
  781. my($viewaudit, $tmp) = @_;
  782. # $pr must be 'local' to be available to site callback
  783. local($pr) = $q->param('pr');
  784. if(!$pr)
  785. {
  786. error_page('Error', 'You must specify a problem report number');
  787. return;
  788. }
  789. if($pr =~ /\D/)
  790. {
  791. error_page('Error', 'Invalid PR number');
  792. return;
  793. }
  794. my $page = "View PR $pr";
  795. print_header();
  796. page_start_html($page);
  797. page_heading($page, "View Problem Report: $pr", 1);
  798. # %fields must be 'local' to be available to site callback
  799. local(%fields) = readpr($pr);
  800. print $q->start_form(),
  801. hidden_db(),
  802. $q->hidden('pr'),
  803. $q->hidden('return_url');
  804. # print 'edit' and 'view audit-trail' buttons as appropriate, mailto link
  805. print "<p>";
  806. print $q->submit('cmd', 'edit') if (can_edit());
  807. print " or " if (can_edit() && !$viewaudit);
  808. print $q->submit('cmd', 'view audit-trail') if (!$viewaudit);
  809. print " or ",
  810. get_mailto_link($pr, %fields), "</p>";
  811. print $q->hr(),
  812. "<table>\n";
  813. print "<tr>\n<td><b>Reporter's email:</b></td>\n<td>",
  814. $q->tt($fields{'Reply-To'}),
  815. # "<tr><td><b>Others to notify<br>of updates to this PR:</b><td>",
  816. "</td>\n</tr>\n<tr>\n<td><b>CC these people<br>on PR status email:</b></td>\n<td>",
  817. $q->tt($fields{'X-GNATS-Notify'}),
  818. # a blank row, to separate header info from PR info
  819. "</td>\n</tr>\n<tr>\n<td>&nbsp;</td>\n<td>&nbsp;</td>\n</tr>\n";
  820. foreach (@fieldnames)
  821. {
  822. next if $_ eq 'Audit-Trail';
  823. my $val = $q->escapeHTML($fields{$_}) || ''; # to avoid -w warning
  824. my $valign = '';
  825. if ($fieldnames{$_} & $MULTILINE)
  826. {
  827. $valign = 'valign=top';
  828. $val = expand($val);
  829. $val =~ s/$/<br>/gm;
  830. $val =~ s/<br>$//; # previous substitution added one too many <br>'s
  831. $val =~ s/ /&nbsp; /g;
  832. $val =~ s/&nbsp; /&nbsp; &nbsp;/g;
  833. $val = mark_urls($val);
  834. }
  835. print "<tr><td $valign nowrap><b>$_:</b></td>\n<td>",
  836. $q->tt($val), "\n";
  837. # Print attachments after Description.
  838. if (/Description/) {
  839. print "</td>\n</tr>\n";
  840. print_attachments(\%fields, 'view');
  841. }
  842. print "</td>\n</tr>\n"
  843. }
  844. print "</table>",
  845. $q->hr();
  846. # print 'edit' and 'view audit-trail' buttons as appropriate, mailto link
  847. print "\n<p>";
  848. print $q->submit('cmd', 'edit') if (can_edit());
  849. print " or " if (can_edit() && !$viewaudit);
  850. print $q->submit('cmd', 'view audit-trail') if (!$viewaudit);
  851. print " or ",
  852. get_mailto_link($pr, %fields);
  853. print "</p>\n";
  854. print $q->end_form();
  855. # Footer comes before the audit-trail.
  856. page_footer($page);
  857. if($viewaudit)
  858. {
  859. print "<h3>Audit Trail:</h3>\n",
  860. mark_urls($q->pre($q->escapeHTML($fields{'Audit-Trail'})));
  861. }
  862. page_end_html($page);
  863. }
  864. # edit -
  865. # The Edit PR page.
  866. #
  867. sub edit
  868. {
  869. #my $debug = 0; # no debug code in here
  870. my($pr) = $q->param('pr');
  871. if(!$pr)
  872. {
  873. error_page('Error', 'You must specify a problem report number');
  874. return;
  875. }
  876. if($pr =~ /\D/)
  877. {
  878. error_page('Error', 'Invalid PR number');
  879. return;
  880. }
  881. my $page = "Edit PR $pr";
  882. print_header();
  883. page_start_html($page);
  884. page_heading($page, "Edit Problem Report: $pr", 1);
  885. # Read the PR.
  886. my(%fields) = readpr($pr);
  887. # Trim Responsible for compatibility.
  888. $fields{'Responsible'} = trim_responsible($fields{'Responsible'});
  889. # remove "all" from arrays
  890. shift(@category);
  891. shift(@severity);
  892. shift(@priority);
  893. shift(@class);
  894. shift(@confidential);
  895. shift(@responsible);
  896. shift(@state);
  897. shift(@submitter_id);
  898. print multipart_form_start(-name=>'editPrForm'), "\n",
  899. hidden_db(),
  900. $q->p($q->submit('cmd', 'submit edit'),
  901. " or ",
  902. $q->reset(-name=>'reset'),
  903. " or ",
  904. get_mailto_link($pr, %fields)),
  905. $q->hidden(-name=>'Editor',
  906. -value=>$db_prefs{'user'},
  907. -override=>1),
  908. $q->hidden(-name=>'Last-Modified',
  909. -value=>$fields{'Last-Modified'},
  910. -override=>1),
  911. $q->hidden(-name=>'pr'),
  912. $q->hidden(-name=>'return_url'),
  913. "<hr>\n";
  914. print "<table>\n";
  915. print "<tr>\n<td><b>Reporter's email:</b></td>\n<td>",
  916. $q->textfield(-name=>'Reply-To',
  917. -default=>$fields{'Reply-To'},
  918. -size=>$textwidth),
  919. # "<tr><td><b>Others to notify<br>of updates to this PR:</b><td>",
  920. "</td>\n</tr>\n<tr>\n<td><b>CC these people<br>on PR status email:</b></td>\n<td>",
  921. $q->textfield(-name=>'X-GNATS-Notify',
  922. -default=>$fields{'X-GNATS-Notify'},
  923. -size=>$textwidth),
  924. # a blank row, to separate header info from PR info
  925. "</td>\n</tr>\n<tr>\n<td>&nbsp;</td>\n<td>&nbsp;</td>\n</tr>\n";
  926. foreach (@fieldnames)
  927. {
  928. next if ($fieldnames{$_} && ($fieldnames{$_} & $EDITEXCLUDE));
  929. my $lc_fieldname = field2param($_);
  930. # The "intro" provides a way for the site callback to print something
  931. # at the top of a given field.
  932. my $intro = cb("edit_intro_$lc_fieldname") || '';
  933. if ($fieldnames{$_} && ($fieldnames{$_} & $ENUM))
  934. {
  935. my @values = cb('edit_pr', $fields{'Category'}, $lc_fieldname);
  936. @values = @$lc_fieldname unless (defined($values[0]));
  937. if ($lc_fieldname eq "category")
  938. {
  939. print "<tr>\n<td><b>$_:</b>\n</td>\n<td>",
  940. $intro,
  941. $q->popup_menu(-name=>$_,
  942. -values=>\@values,
  943. -labels=>\%category_desc,
  944. -default=>$fields{$_});
  945. print "</td>\n</tr>\n";
  946. print "</td>\n</tr>\n";
  947. }
  948. elsif ($lc_fieldname eq "responsible")
  949. {
  950. print "<tr>\n<td><b>$_:</b>\n</td>\n<td>",
  951. $intro,
  952. $q->popup_menu(-name=>$_,
  953. -values=>\@values,
  954. -labels=>\%responsible_fullname,
  955. -default=>$fields{$_});
  956. print "</td>\n</tr>\n";
  957. }
  958. else
  959. {
  960. print "<tr>\n<td><b>$_:</b>\n</td>\n<td>",
  961. $intro,
  962. $q->popup_menu(-name=>$_,
  963. -values=>\@$lc_fieldname,
  964. -default=>$fields{$_});
  965. print "</td>\n</tr>\n";
  966. }
  967. }
  968. elsif ($fieldnames{$_} && ($fieldnames{$_} & $MULTILINE))
  969. {
  970. my $rows = 4;
  971. $rows = 8 if /Description/;
  972. $rows = 2 if /Environment/;
  973. print "<tr>\n<td valign=top><b>$_:</b></td>\n<td>",
  974. $intro,
  975. $q->textarea(-name=>$_,
  976. -cols=>$textwidth,
  977. -rows=>$rows,
  978. -default=>$fields{$_});
  979. # Print attachments after Description.
  980. if (/Description/) {
  981. print "</td>\n</tr>\n";
  982. print_attachments(\%fields, 'edit');
  983. }
  984. print "</td>\n</tr>\n";
  985. }
  986. else
  987. {
  988. print "<tr>\n<td><b>$_:</b></td>\n<td>",
  989. $intro,
  990. $q->textfield(-name=>$_,
  991. -size=>$textwidth,
  992. -default=>$fields{$_});
  993. print "</td>\n</tr>\n";
  994. }
  995. if ($fieldnames{$_} && $fieldnames{$_} & $REASONCHANGE)
  996. {
  997. print "<tr>\n<td valign=top><b>Reason Changed:</b></td>\n<td>",
  998. $q->textarea(-name=>"$_-Why",
  999. -default=>'',
  1000. -override=>1,
  1001. -cols=>$textwidth,
  1002. -rows=>2);
  1003. print "</td>\n</tr>\n";
  1004. }
  1005. print "\n";
  1006. }
  1007. print "</table>",
  1008. $q->p($q->submit('cmd', 'submit edit'),
  1009. " or ",
  1010. $q->reset(-name=>'reset'),
  1011. " or ",
  1012. get_mailto_link($pr, %fields)),
  1013. $q->end_form(),
  1014. $q->hr();
  1015. # Footer comes before the audit-trail.
  1016. page_footer($page);
  1017. print "<h3>Audit-Trail:</h3>\n",
  1018. mark_urls($q->pre($q->escapeHTML($fields{'Audit-Trail'})));
  1019. page_end_html($page);
  1020. }
  1021. # Print out the %fields hash for debugging.
  1022. sub debug_print_fields
  1023. {
  1024. my $fields_hash_ref = shift;
  1025. print "<table cellspacing=0 cellpadding=0 border=1>\n";
  1026. foreach my $f (sort keys %$fields_hash_ref)
  1027. {
  1028. print "<tr valign=top><td>$f</td><td>",
  1029. $q->pre($q->escapeHTML($$fields_hash_ref{$f})),
  1030. "</td></tr>\n";
  1031. }
  1032. my $aref = $$fields_hash_ref{'attachments'} || [];
  1033. my $i=0;
  1034. foreach my $href (@$aref) {
  1035. print "<tr valign=top><td>attachment $i</td><td>",
  1036. ($$href{'original_attachment'}
  1037. ? $$href{'original_attachment'} : "--- empty ---"),
  1038. "</td></tr>\n";
  1039. $i++;
  1040. }
  1041. print "</table><hr>\n";
  1042. }
  1043. # submitedit -
  1044. # User pressed 'submit' on the edit page. If the edits are applied
  1045. # successfully, give a message then return the user to the URL
  1046. # specified in param('return_url') so that he can continue doing what
  1047. # he was previously doing (e.g. looking at query results). If the
  1048. # edits are not successful, print and error and stay put.
  1049. #
  1050. sub submitedit
  1051. {
  1052. local($page) = 'Edit PR Results'; # local so visible to &$err_sub
  1053. my $debug = 0;
  1054. my $mail_sent = 0;
  1055. # Local sub to report errors while editing.
  1056. # This allows us to postpone calling print_header().
  1057. my $err_sub = sub {
  1058. my($err_heading, $err_text) = @_;
  1059. print_header();
  1060. page_start_html($page);
  1061. page_heading($page, 'Error');
  1062. print "<h3>$err_heading</h3>";
  1063. print "<p>$err_text</p>" if $err_text;
  1064. page_footer($page);
  1065. page_end_html($page);
  1066. return;
  1067. };
  1068. my($pr) = $q->param('pr');
  1069. if(!$pr)
  1070. {
  1071. &$err_sub("You must specify a problem report number");
  1072. return;
  1073. }
  1074. my(%fields, %mailto, $adr);
  1075. my $audittrail = '';
  1076. my $err = '';
  1077. my $ok = 1;
  1078. # Retrieve new attachment (if any) before locking PR, in case it fails.
  1079. my $encoded_attachment = encode_attachment('attached_file');
  1080. my(%oldfields) = lockpr($pr, $db_prefs{'user'});
  1081. if ($gnats::ERRSTR) {
  1082. &$err_sub("$gnats::ERRSTR", "The PR has not been changed. "
  1083. . "If this problem persists, please contact a "
  1084. . "GNATS administrator.");
  1085. client_exit();
  1086. exit();
  1087. }
  1088. LOCKED:
  1089. {
  1090. # Trim Responsible for compatibility.
  1091. $oldfields{'Responsible'} = trim_responsible($oldfields{'Responsible'});
  1092. # Merge %oldfields and CGI params to get %fields. Not all gnats
  1093. # fields have to be present in the CGI params; the ones which are
  1094. # not specified default to their old values.
  1095. %fields = %oldfields;
  1096. foreach my $key ($q->param)
  1097. {
  1098. my $val = $q->param($key);
  1099. if($key =~ /-Why/
  1100. || ($fieldnames{$key} && ($fieldnames{$key} & $MULTILINE)))
  1101. {
  1102. $val = fix_multiline_val($val);
  1103. }
  1104. $fields{$key} = $val;
  1105. }
  1106. # Add the attached file, if any, to the new PR.
  1107. add_encoded_attachment_to_pr(\%fields, $encoded_attachment);
  1108. # Delete any attachments, if directed.
  1109. my(@deleted_attachments) = $q->param('delete attachments');
  1110. remove_attachments_from_pr(\%fields, @deleted_attachments);
  1111. if($fields{'Last-Modified'} ne $oldfields{'Last-Modified'})
  1112. {
  1113. &$err_sub("PR $pr has been modified since you started editing it.",
  1114. "Please return to the edit form, press the Reload button, "
  1115. . "then make your edits again.\n"
  1116. . "<pre>Last-Modified was '$fields{'Last-Modified'}'\n"
  1117. . "Last-Modified is now '$oldfields{'Last-Modified'}'</pre>\n");
  1118. last LOCKED;
  1119. }
  1120. if($db_prefs{'user'} eq "" || $fields{'Responsible'} eq "")
  1121. {
  1122. &$err_sub("Can't make the edit",
  1123. "Responsible is '$fields{'Responsible'}', user is '$db_prefs{'user'}'");
  1124. last LOCKED;
  1125. }
  1126. # If X-GNATS-Notify or Reply-To changed, we need to splice the
  1127. # change into the envelope.
  1128. foreach ('Reply-To', 'X-GNATS-Notify')
  1129. {
  1130. if($fields{$_} ne $oldfields{$_})
  1131. {
  1132. if ($fields{'envelope'} =~ /^$_:/m)
  1133. {
  1134. # Replace existing header with new one.
  1135. $fields{'envelope'} =~ s/^$_:.*$/$_: $fields{$_}/m;
  1136. }
  1137. else
  1138. {
  1139. # Insert new header at end (blank line). Keep blank line at end.
  1140. $fields{'envelope'} =~ s/^$/$_: $fields{$_}\n/m;
  1141. }
  1142. }
  1143. }
  1144. if ($debug)
  1145. {
  1146. &$err_sub("debugging -- PR edits not submitted");
  1147. debug_print_fields(\%fields);
  1148. last LOCKED;
  1149. }
  1150. # Leave an Audit-Trail
  1151. foreach (@fieldnames)
  1152. {
  1153. if($_ ne "Audit-Trail")
  1154. {
  1155. $oldfields{$_} = '' if !defined($oldfields{$_}); # avoid -w warning
  1156. $fields{$_} = '' if !defined($fields{$_}); # avoid -w warning
  1157. if($fields{$_} ne $oldfields{$_})
  1158. {
  1159. next unless ($fieldnames{$_} & $AUDITINCLUDE);
  1160. if($fieldnames{$_} & $MULTILINE)
  1161. {
  1162. # For multitext fields, indent the values.
  1163. my $tmp = $oldfields{$_};
  1164. $tmp =~ s/^/ /gm;
  1165. $audittrail .= "$_-Changed-From:\n$tmp";
  1166. $tmp = $fields{$_};
  1167. $tmp =~ s/^/ /gm;
  1168. $audittrail .= "$_-Changed-To:\n$tmp";
  1169. }
  1170. else
  1171. {
  1172. $audittrail .= "$_-Changed-From-To: $oldfields{$_}->$fields{$_}\n";
  1173. }
  1174. $audittrail .= "$_-Changed-By: $db_prefs{'user'}\n";
  1175. $audittrail .= "$_-Changed-When: " . scalar(localtime()) . "\n";
  1176. if($fieldnames{$_} & $REASONCHANGE)
  1177. {
  1178. if($fields{"$_-Why"} =~ /^\s*$/)
  1179. {
  1180. if ($ok) {
  1181. $ok = 0;
  1182. print_header();
  1183. page_start_html($page);
  1184. page_heading($page, 'Error');
  1185. }
  1186. print "<h3>Field '$_' must have a reason for change</h3>",
  1187. "Old $_: $oldfields{$_}<br>",
  1188. "New $_: $fields{$_}";
  1189. }
  1190. else
  1191. {
  1192. # Indent the "Why" value.
  1193. my $tmp = $fields{"$_-Why"};
  1194. $tmp =~ s/^/ /gm;
  1195. $audittrail .= "$_-Changed-Why:\n" . $tmp;
  1196. }
  1197. }
  1198. }
  1199. }
  1200. }
  1201. $fields{'Audit-Trail'} = $oldfields{'Audit-Trail'} . $audittrail;
  1202. last LOCKED unless $ok;
  1203. # Get list of people to notify, then add old responsible person.
  1204. # If that person doesn't exist, don't worry about it.
  1205. %mailto = interested_parties($pr, 0, %fields);
  1206. if(defined($adr = praddr($oldfields{'Responsible'})))
  1207. {
  1208. $mailto{$adr} = 1;
  1209. }
  1210. my($newpr) = unparsepr('gnatsd', %fields);
  1211. $newpr =~ s/\r//g;
  1212. #print $q->pre($q->escapeHTML($newpr));
  1213. #last LOCKED; # debug
  1214. # Submit the edits.
  1215. client_cmd("edit $fields{'Number'}");
  1216. my $error = $gnats::ERRSTR;
  1217. client_cmd("$newpr\n.");
  1218. $error ||= $gnats::ERRSTR;
  1219. if ($error) {
  1220. my $page = 'Error';
  1221. print_header();
  1222. page_start_html($page);
  1223. page_heading($page, $page);
  1224. print $q->h2("$error");
  1225. print $q->p("The PR has not been changed. "
  1226. . "If this problem persists, please contact a "
  1227. . "GNATS administrator.");
  1228. last LOCKED;
  1229. }
  1230. # Now send mail to all concerned parties,
  1231. # but only if there's something interesting to say.
  1232. my($mailto);
  1233. delete $mailto{''};
  1234. $mailto = join(", ", sort(keys(%mailto)));
  1235. #print $q->pre($q->escapeHTML("mailto->$mailto<-\n"));
  1236. #last LOCKED; # debug
  1237. if($mailto ne "" && $audittrail ne "")
  1238. {
  1239. # Use email address in responsible file as From, if present.
  1240. my $from = $responsible_address{$db_prefs{'user'}} || $db_prefs{'user'};
  1241. if(!open(MAILER, "|$site_mailer"))
  1242. {
  1243. &$err_sub("Edit successful, but email notification failed",
  1244. "Can't open pipe to $site_mailer: $!");
  1245. last LOCKED;
  1246. }
  1247. else
  1248. {
  1249. print MAILER "To: $mailto\n";
  1250. print MAILER "From: $from\n";
  1251. print MAILER "Reply-To: $from, $mailto, $config{'GNATS_ADDR'}\n";
  1252. print MAILER "X-Mailer: gnatsweb $VERSION\n";
  1253. #GCC-LOCAL begin.
  1254. #print MAILER "Subject: Re: $fields{'Category'}/$pr\n\n";
  1255. print MAILER "Subject: Re: $fields{'Category'}/$pr: $fields{'Synopsis'}\n\n";
  1256. #GCC-LOCAL end.
  1257. if ($oldfields{'Synopsis'} eq $fields{'Synopsis'})
  1258. {
  1259. print MAILER "Synopsis: $fields{'Synopsis'}\n\n";
  1260. }
  1261. else
  1262. {
  1263. print MAILER "Old Synopsis: $oldfields{'Synopsis'}\n";
  1264. print MAILER "New Synopsis: $fields{'Synopsis'}\n\n";
  1265. }
  1266. print MAILER "$audittrail\n";
  1267. # Print URL so that HTML-enabled mail readers can jump to the PR.
  1268. print MAILER get_viewpr_url($pr), "\n";
  1269. if(!close(MAILER))
  1270. {
  1271. &$err_sub("Edit successful, but email notification failed",
  1272. "Can't close pipe to $site_mailer: $!");
  1273. last LOCKED;
  1274. }
  1275. $mail_sent = 1;
  1276. }
  1277. }
  1278. $lock_end_reached = 1;
  1279. }
  1280. unlockpr($fields{'Number'});
  1281. if ($lock_end_reached) {
  1282. # We print out the "Edit successful" message after unlocking the
  1283. # PR. If the user hits the Stop button of the browser while
  1284. # submitting, the web server won't terminate the script until the
  1285. # next time the server attempts to output something to the
  1286. # browser. Since this is the first output after the PR was
  1287. # locked, we print it after the unlocking. Let user know the edit
  1288. # was successful. After a 2s delay, refresh back to where the user
  1289. # was before the edit. Internet Explorer does not honor the HTTP
  1290. # Refresh header, so we have to complement the "clean" CGI.pm
  1291. # method with the ugly hack below, with a HTTP-EQUIV in the HEAD
  1292. # to make things work.
  1293. my $return_url = $q->param('return_url') || get_script_name();
  1294. my $refresh = 2;
  1295. print_header(-Refresh => "$refresh; URL=$return_url",
  1296. -cookie => create_global_cookie());
  1297. # Workaround for MSIE:
  1298. my @args = (-title=>"$page - $site_banner_text");
  1299. push(@args, -bgcolor=>$site_background)
  1300. if defined($site_background);
  1301. push(@args, -style=>{-src=>$site_stylesheet})
  1302. if defined($site_stylesheet);
  1303. push(@args, -head=>meta({-http_equiv=>'Refresh',
  1304. -content=>"$refresh; URL=$return_url"}));
  1305. print $q->start_html(@args);
  1306. # Print page banner, with button bar, without the <head> part:
  1307. page_start_html($page, 0, 1);
  1308. page_heading($page, ($mail_sent ? 'Edit successful; mail sent'
  1309. : 'Edit successful'));
  1310. print "<p>Page will refresh in $refresh seconds...</p>\n";
  1311. }
  1312. page_footer($page);
  1313. page_end_html($page);
  1314. }
  1315. sub query_page
  1316. {
  1317. my $page = 'Query PR';
  1318. page_start_html($page);
  1319. page_heading($page, 'Query Problem Reports', 1);
  1320. print_stored_queries();
  1321. print $q->start_form(),
  1322. hidden_db(),
  1323. $q->submit('cmd', 'submit query'),
  1324. "<hr>",
  1325. "<table>\n",
  1326. "<tr>\n<td>Category:</td>\n<td>",
  1327. $q->popup_menu(-name=>'category',
  1328. -values=>\@category,
  1329. -labels=>\%category_desc,
  1330. -default=>$category[0]),
  1331. "</td>\n</tr>\n<tr>\n<td>Severity:</td>\n<td>",
  1332. $q->popup_menu(-name=>'severity',
  1333. -values=>\@severity,
  1334. -default=>$severity[0]),
  1335. "</td>\n</tr>\n<tr>\n<td>Priority:</td>\n<td>",
  1336. $q->popup_menu(-name=>'priority',
  1337. -values=>\@priority,
  1338. -default=>$priority[0]),
  1339. "</td>\n</tr>\n<tr>\n<td>Responsible:</td>\n<td>",
  1340. $q->popup_menu(-name=>'responsible',
  1341. -values=>\@responsible,
  1342. -labels=>\%responsible_fullname,
  1343. -default=>$responsible[0]),
  1344. "</td>\n</tr>\n<tr>\n<td>Submitter-ID:</td>\n<td>",
  1345. $q->popup_menu(-name=>'submitter_id',
  1346. -values=>\@submitter_id,
  1347. -labels=>\%submitter_fullname,
  1348. -default=>$submitter_id[0]),
  1349. "</td>\n</tr>\n<tr>\n<td>State:</td>\n<td>",
  1350. $q->popup_menu(-name=>'state',
  1351. -values=>\@state,
  1352. -default=>$state[0]),
  1353. "</td>\n</tr>\n<tr>\n<td>\n</td>\n<td>",
  1354. $q->checkbox_group(-name=>'ignoreclosed',
  1355. -values=>['Ignore Closed'],
  1356. -defaults=>['Ignore Closed']),
  1357. "</td>\n</tr>\n<tr>\n<td>Class:</td>\n<td>",
  1358. $q->popup_menu(-name=>'class',
  1359. -values=>\@class,
  1360. -default=>$class[0]),
  1361. "</td>\n</tr>\n<tr>\n<td>Synopsis Search:</td>\n<td>",
  1362. $q->textfield(-name=>'synopsis',-size=>25),
  1363. "</td>\n</tr>\n<tr>\n<td>Multi-line Text Search:</td>\n<td>",
  1364. $q->textfield(-name=>'multitext',-size=>25),
  1365. "</td>\n</tr>\n<tr>\n<td>\n</td>\n<td>",
  1366. $q->checkbox_group(-name=>'originatedbyme',
  1367. -values=>['Originated by You'],
  1368. -defaults=>[]),
  1369. "</td>\n</tr>\n<tr valign=top>\n<td>Column Display:</td>\n<td>";
  1370. my(@columns) = split(' ', $global_prefs{'columns'});
  1371. @columns = @deffields unless @columns;
  1372. print $q->scrolling_list(-name=>'columns',
  1373. -values=>\@fields,
  1374. -defaults=>\@columns,
  1375. -multiple=>1,
  1376. -size=>5),
  1377. "</td>\n</tr>\n<tr>\n<td>\n</td>\n<td>",
  1378. $q->checkbox_group(-name=>'displaydate',
  1379. -values=>['Display Current Date'],
  1380. -defaults=>['Display Current Date']),
  1381. "</td>\n</tr>\n</table>",
  1382. "<hr>",
  1383. $q->submit('cmd', 'submit query'),
  1384. $q->end_form();
  1385. page_footer($page);
  1386. page_end_html($page);
  1387. }
  1388. sub advanced_query_page
  1389. {
  1390. my $page = 'Advanced Query';
  1391. page_start_html($page);
  1392. page_heading($page, 'Query Problem Reports', 1);
  1393. print_stored_queries();
  1394. print $q->start_form(),
  1395. hidden_db();
  1396. my $width = 30;
  1397. my $heading_bg = '#9fbdf9';
  1398. my $cell_bg = '#d0d0d0';
  1399. print $q->p($q->submit('cmd', 'submit query'),
  1400. " or ",
  1401. $q->reset(-name=>'reset'));
  1402. print "<hr>";
  1403. print "<center>";
  1404. ### Text and multitext queries
  1405. print "<table border=1 cellspacing=0 bgcolor=$cell_bg>",
  1406. "<caption>Search All Text</caption>",
  1407. "<tr bgcolor=$heading_bg>\n",
  1408. "<th nowrap>Search these text fields</th>\n",
  1409. "<th nowrap>using regular expression</th>\n",
  1410. "</tr>\n";
  1411. print "<tr>\n<td>Single-line text fields:</td>\n<td>",
  1412. $q->textfield(-name=>'text', -size=>$width),
  1413. "</td>\n</tr>\n",
  1414. "<tr>\n<td>Multi-line text fields:</td>\n<td>",
  1415. $q->textfield(-name=>'multitext', -size=>$width),
  1416. "</td>\n</tr>\n",
  1417. "</table>\n";
  1418. print "<div>&nbsp;</div>\n";
  1419. ### Date queries
  1420. print "<table border=1 cellspacing=0 bgcolor=$cell_bg>",
  1421. "<caption>Search By Date</caption>",
  1422. "<tr bgcolor=$heading_bg>\n",
  1423. "<th nowrap>Date Search</th>\n",
  1424. "<th nowrap>Example: <tt>1999-04-01 05:00 GMT</tt></th>\n",
  1425. "</tr>\n";
  1426. my(@date_queries) = ('Arrived After', 'Arrived Before',
  1427. 'Modified After', 'Modified Before',
  1428. 'Closed After', 'Closed Before');
  1429. push(@date_queries, 'Required After', 'Required Before')
  1430. if $site_release_based;
  1431. foreach (@date_queries)
  1432. {
  1433. my $param_name = lc($_);
  1434. $param_name =~ s/ //;
  1435. print "<tr>\n<td>$_:</td>\n<td>",
  1436. $q->textfield(-name=>$param_name, -size=>$width),
  1437. "</td>\n</tr>\n";
  1438. }
  1439. print $q->Tr( $q->td({-colspan=>2},
  1440. $q->small( $q->b("NOTE:"), "If your search includes 'Closed After'
  1441. or 'Closed Before', uncheck 'Ignore Closed' below.")));
  1442. print "</table>\n";
  1443. print "<div>&nbsp;</div>\n";
  1444. ### Field queries
  1445. print "<table border=1 cellspacing=0 bgcolor=$cell_bg>",
  1446. "<caption>Search Individual Fields</caption>",
  1447. "<tr bgcolor=$heading_bg>\n",
  1448. "<th nowrap>Search this field</th>\n",
  1449. "<th nowrap>using regular expression, or</th>\n",
  1450. "<th nowrap>using multi-selection</th>\n",
  1451. "</tr>\n";
  1452. foreach (@fieldnames)
  1453. {
  1454. my $lc_fieldname = field2param($_);
  1455. next unless ($gnatsd_query{$lc_fieldname});
  1456. print "<tr valign=top>\n";
  1457. # 1st column is field name
  1458. print "<td>$_:</td>\n";
  1459. # 2nd column is regexp search field
  1460. print "<td>",
  1461. $q->textfield(-name=>$lc_fieldname,
  1462. -size=>$width);
  1463. if ($_ eq 'State')
  1464. {
  1465. print "<br>",
  1466. $q->checkbox_group(-name=>'ignoreclosed',
  1467. -values=>['Ignore Closed'],
  1468. -defaults=>['Ignore Closed']),
  1469. }
  1470. print "</td>\n";
  1471. # 3rd column is blank or scrolling multi-select list
  1472. print "<td>";
  1473. if ($fieldnames{$_} & $ENUM)
  1474. {
  1475. my $ary_ref = \@$lc_fieldname;
  1476. my $size = scalar(@$ary_ref);
  1477. $size = 4 if $size > 4;
  1478. print $q->scrolling_list(-name=>$lc_fieldname,
  1479. -values=>$ary_ref,
  1480. -multiple=>1,
  1481. -size=>$size);
  1482. }
  1483. else
  1484. {
  1485. print "&nbsp;";
  1486. }
  1487. print "</td>\n";
  1488. print "</tr>\n";
  1489. }
  1490. print "</table>\n";
  1491. print "<div>&nbsp;</div>\n";
  1492. ### Column selection
  1493. my(@columns) = split(' ', $global_prefs{'columns'});
  1494. @columns = @deffields unless @columns;
  1495. print "<table border=1 cellspacing=0 bgcolor=$cell_bg>",
  1496. "<caption>Display</caption>",
  1497. "<tr valign=top><td>Display these columns:</td>\n<td>",
  1498. $q->scrolling_list(-name=>'columns',
  1499. -values=>\@fields,
  1500. -defaults=>\@columns,
  1501. -multiple=>1),
  1502. "</td>\n</tr>\n<tr>\n<td colspan=2>",
  1503. $q->checkbox_group(-name=>'displaydate',
  1504. -values=>['Display Current Date'],
  1505. -defaults=>['Display Current Date']),
  1506. "</td>\n</tr>\n</table>\n";
  1507. ### Wrapup
  1508. print "</center>\n";
  1509. print "<hr>",
  1510. $q->p($q->submit('cmd', 'submit query'),
  1511. " or ",
  1512. $q->reset(-name=>'reset')),
  1513. $q->end_form();
  1514. page_footer($page);
  1515. page_end_html($page);
  1516. }
  1517. sub print_gnatsd_error
  1518. {
  1519. my($errstr) = @_;
  1520. print $q->h2("Error: $errstr");
  1521. }
  1522. sub error_page
  1523. {
  1524. my($err_heading, $err_text) = @_;
  1525. my $page = 'Error';
  1526. print_header();
  1527. page_start_html($page);
  1528. page_heading($page, $err_heading);
  1529. print $q->p($err_text) if $err_text;
  1530. page_footer($page);
  1531. page_end_html($page);
  1532. }
  1533. sub submitquery
  1534. {
  1535. my $page = 'Query Results';
  1536. my $queryname = $q->param('queryname');
  1537. my $originatedbyme = $q->param('originatedbyme');
  1538. my $ignoreclosed = $q->param('ignoreclosed');
  1539. my $debug = 0;
  1540. my $heading = 'Query Results';
  1541. $heading .= ": $queryname" if $queryname;
  1542. page_start_html($page);
  1543. page_heading($page, $heading, 1, 1);
  1544. local($gnats::DEBUG_LEVEL) = 1 if $debug;
  1545. client_cmd("rset");
  1546. client_cmd("orig $db_prefs{'user'}") if($originatedbyme);
  1547. client_cmd("nocl") if($ignoreclosed);
  1548. # Submit client_cmd for each param which specifies a query.
  1549. my($param, $regexp, @val);
  1550. foreach $param ($q->param())
  1551. {
  1552. next unless $gnatsd_query{$param};
  1553. # Turn multiple param values into regular expression.
  1554. @val = $q->param($param);
  1555. $regexp = join('|', @val);
  1556. # Discard trailing '|all', or leading '|'.
  1557. $regexp =~ s/\|all$//;
  1558. $regexp =~ s/^\|//;
  1559. # If there's still a query here, make it.
  1560. client_cmd("$gnatsd_query{$param} $regexp")
  1561. if($regexp && $regexp ne 'all');
  1562. }
  1563. my(@query_results) = client_cmd("sql2");
  1564. if ($gnats::ERRSTR) {
  1565. print_gnatsd_error($gnats::ERRSTR);
  1566. }
  1567. else {
  1568. display_query_results(@query_results);
  1569. }
  1570. page_footer($page);
  1571. page_end_html($page);
  1572. }
  1573. # by_field -
  1574. # Sort routine called by display_query_results.
  1575. #
  1576. # Assumes $sortby is set by caller.
  1577. #
  1578. sub by_field
  1579. {
  1580. my($val);
  1581. # Handle common cases first.
  1582. if (!$sortby || $sortby eq 'PR')
  1583. {
  1584. $val = $b->[0] <=> $a->[0];
  1585. }
  1586. elsif ($sortby eq 'Category')
  1587. {
  1588. $val = $a->[1] cmp $b->[1];
  1589. }
  1590. elsif ($sortby eq 'Confidential')
  1591. {
  1592. $val = $a->[3] cmp $b->[3];
  1593. }
  1594. elsif ($sortby eq 'Severity')
  1595. {
  1596. # sort by Severity then Priority then Class
  1597. $val = $a->[4] <=> $b->[4]
  1598. ||
  1599. $a->[5] <=> $b->[5]
  1600. ||
  1601. $a->[8] <=> $b->[8]
  1602. ;
  1603. }
  1604. elsif ($sortby eq 'Priority')
  1605. {
  1606. # sort by Priority then Severity then Class
  1607. $val = $a->[5] <=> $b->[5]
  1608. ||
  1609. $a->[4] <=> $b->[4]
  1610. ||
  1611. $a->[8] <=> $b->[8]
  1612. ;
  1613. }
  1614. elsif ($sortby eq 'Responsible')
  1615. {
  1616. $val = $a->[6] cmp $b->[6];
  1617. }
  1618. elsif ($sortby eq 'State')
  1619. {
  1620. $val = $a->[7] <=> $b->[7];
  1621. }
  1622. elsif ($sortby eq 'Class')
  1623. {
  1624. $val = $a->[8] <=> $b->[8];
  1625. }
  1626. elsif ($sortby eq 'Submitter-Id')
  1627. {
  1628. $val = $a->[9] cmp $b->[9];
  1629. }
  1630. elsif ($sortby eq 'Release')
  1631. {
  1632. $val = $a->[12] cmp $b->[12];
  1633. }
  1634. elsif ($sortby eq 'Arrival-Date')
  1635. {
  1636. $val = $a->[10] cmp $b->[10];
  1637. }
  1638. elsif ($sortby eq 'Closed-Date')
  1639. {
  1640. $val = $a->[14] cmp $b->[14];
  1641. }
  1642. elsif ($sortby eq 'Last-Modified')
  1643. {
  1644. $val = $a->[13] cmp $b->[13];
  1645. }
  1646. else
  1647. {
  1648. $val = $a->[0] <=> $b->[0];
  1649. }
  1650. $val;
  1651. }
  1652. # nonempty -
  1653. # Turn empty strings into "&nbsp;" so that Netscape tables won't
  1654. # look funny.
  1655. #
  1656. sub nonempty
  1657. {
  1658. my $str = shift;
  1659. $str = '&nbsp;' if !$str;
  1660. $str;
  1661. }
  1662. # field2param -
  1663. # Convert gnats field name into parameter name, e.g.
  1664. # "Submitter-Id" => "submitter_id". It's done this crazy way for
  1665. # compatibility with queries stored by gnatsweb 2.1.
  1666. #
  1667. sub field2param
  1668. {
  1669. my $name = shift;
  1670. $name =~ s/-/_/g;
  1671. $name = lc($name);
  1672. }
  1673. # param2field -
  1674. # Convert parameter name into gnats field name, e.g.
  1675. # "submitter_id" => "Submitter-Id". It's done this crazy way for
  1676. # compatibility with queries stored by gnatsweb 2.1.
  1677. #
  1678. sub param2field
  1679. {
  1680. my $name = shift;
  1681. my @words = split(/_/, $name);
  1682. map { $_ = ucfirst($_); } @words;
  1683. $name = join('-', @words);
  1684. }
  1685. # display_query_results -
  1686. # Display the query results, and the "store query" form.
  1687. sub display_query_results
  1688. {
  1689. my(@query_results) = @_;
  1690. my(@fields) = $q->param('columns');
  1691. my $displaydate = $q->param('displaydate');
  1692. my($field);
  1693. my(%vis); # hash of displayed fields
  1694. # Print number of matches found, and return if that number is 0.
  1695. my $num_matches = scalar(@query_results);
  1696. my $heading = sprintf("%s %s found",
  1697. $num_matches ? $num_matches : "No",
  1698. ($num_matches == 1) ? "match" : "matches");
  1699. my $heading2 = $displaydate ? $q->small("( Query executed ",
  1700. (scalar localtime), ")") : '';
  1701. print $q->table({cellpadding=>0, cellspacing=>0, border=>0},
  1702. $q->Tr($q->td($q->font({size=>'+2'},
  1703. $q->strong($heading)))), $q->Tr($q->td($heading2))),
  1704. '&nbsp;';
  1705. return if ($num_matches == 0);
  1706. #warn "--------- query results ---------\n";
  1707. #foreach $p (@query_results) {
  1708. # warn "$p\n";
  1709. #}
  1710. # If there's a site callback to sort, provide a link to do it.
  1711. if(cb('sort_query', 'custom', 'checking_if_custom_sort_exists')) {
  1712. my $href = $q->self_url();
  1713. $href =~ s/&sortby=[^&]+//;
  1714. $href .= "&sortby=custom";
  1715. # 6/25/99 kenstir: CEL claims this avoids a problem w/ apache+mod_perl.
  1716. $href =~ s/^[^?]*\?/$sn\?/; #CEL
  1717. print "Site-specific <a href=\"$href\">sort</a>";
  1718. }
  1719. # Sort @query_results according to the rules in by_field().
  1720. # Using the "map, sort" idiom allows us to perform the expensive
  1721. # split() only once per item, as opposed to during every comparison.
  1722. # Note that $sortby must be 'local'...it's used in by_field().
  1723. local($sortby) = $q->param('sortby');
  1724. my(@sortable) = ('PR','Category','Confidential',
  1725. 'Severity','Priority','Responsible',
  1726. 'State','Class','Release','Submitter-Id',
  1727. 'Arrival-Date', 'Closed-Date', 'Last-Modified');
  1728. my(@presplit_prs) = map { [ (split /\|/) ] } @query_results;
  1729. my(@sorted_prs) = cb('sort_query', $sortby, @presplit_prs);
  1730. if(!defined($sorted_prs[0])) {
  1731. @sorted_prs = sort by_field @presplit_prs;
  1732. }
  1733. print "\n<table border=1 cellspacing=0 cellpadding=1>\n";
  1734. # Print table header which allows sorting by some columns.
  1735. # While printing the headers, temporarily override the 'sortby' param
  1736. # so that self_url() works right.
  1737. print "<tr>\n";
  1738. my(@cols) = ('PR', @fields);
  1739. for $field (@cols)
  1740. {
  1741. $ufield = param2field($field);
  1742. if (grep(/$ufield/, @sortable))
  1743. {
  1744. $q->param(-name=>'sortby', -value=>$ufield);
  1745. my $href = $q->self_url();
  1746. # 6/25/99 kenstir: CEL claims this avoids a problem w/ apache+mod_perl.
  1747. $href =~ s/^[^?]*\?/$sn\?/; #CEL
  1748. print "<th><a href=\"$href\">$ufield</a></th>\n";
  1749. }
  1750. else
  1751. {
  1752. print "<th>$ufield</th>\n";
  1753. }
  1754. $vis{$field}++;
  1755. }
  1756. # Reset param 'sortby' to its original value, so that 'store query' works.
  1757. $q->param(-name=>'sortby', -value=>$sortby);
  1758. print "</tr>\n";
  1759. # Print the PR's.
  1760. my $myurl = $q->url();
  1761. foreach (@sorted_prs)
  1762. {
  1763. print "<tr valign=top>\n";
  1764. my($id, $cat, $syn, $conf, $sev,
  1765. $pri, $resp, $state, $class, $sub,
  1766. $arrival, $orig, $release, $lastmoddate, $closeddate,
  1767. $quarter, $keywords, $daterequired) = @{$_};
  1768. print "<td nowrap><a href=\"" . get_viewpr_url($id, 1) . "\">$id</a>";
  1769. print " <a href=\"" . get_editpr_url($id, 1) .
  1770. "\"><font size=-1>edit</font></a>"
  1771. if can_edit();
  1772. print "</td>\n";
  1773. # CGI.pm does not like boolean attributes like nowrap. We add =>'1' to avoid a -w warning.
  1774. print $q->td({-nowrap=>'1'}, $q->escapeHTML($cat)) if $vis{'category'};
  1775. print $q->td({-nowrap=>'1'}, $q->escapeHTML($conf)) if $vis{'confidential'};
  1776. print $q->td({-nowrap=>'1'}, $q->escapeHTML($state[$state])) if $vis{'state'};
  1777. print $q->td({-nowrap=>'1'}, $q->escapeHTML($class[$class])) if $vis{'class'};
  1778. print $q->td({-nowrap=>'1'}, $q->escapeHTML($severity[$sev])) if $vis{'severity'};
  1779. print $q->td({-nowrap=>'1'}, $q->escapeHTML($priority[$pri])) if $vis{'priority'};
  1780. print $q->td({-nowrap=>'1'}, nonempty($q->escapeHTML($release))) if $vis{'release'};
  1781. print $q->td({-nowrap=>'1'}, nonempty($q->escapeHTML($quarter))) if($site_release_based
  1782. && $vis{'quarter'});
  1783. print $q->td(nonempty($q->escapeHTML($keywords))) if($site_release_based
  1784. && $vis{'keywords'});
  1785. print $q->td({-nowrap=>'1'}, $q->escapeHTML($resp)) if $vis{'responsible'};
  1786. print $q->td({-nowrap=>'1'}, nonempty($q->escapeHTML($sub))) if $vis{'submitter_id'};
  1787. print $q->td({-nowrap=>'1'}, nonempty($q->escapeHTML($orig))) if $vis{'originator'};
  1788. print $q->td({-nowrap=>'1'}, $q->escapeHTML($arrival)) if $vis{'arrival_date'};
  1789. print $q->td({-nowrap=>'1'}, nonempty($q->escapeHTML($daterequired))) if($site_release_based
  1790. && $vis{'date_required'});
  1791. print $q->td({-nowrap=>'1'}, nonempty($q->escapeHTML($lastmoddate))) if $vis{'last_modified'};
  1792. print $q->td({-nowrap=>'1'}, nonempty($q->escapeHTML($closeddate))) if $vis{'closed_date'};
  1793. print $q->td($q->escapeHTML($syn)) if $vis{'synopsis'};
  1794. print "</tr>\n";
  1795. }
  1796. print "</table>\n";
  1797. # Provide a URL which someone can use to bookmark this query.
  1798. my $url = $q->self_url();
  1799. print $q->p(qq{<a href="$url">View for bookmarking</a>\n});
  1800. # Allow the user to store this query. Need to repeat params as hidden
  1801. # fields so they are available to the 'store query' handler.
  1802. print $q->start_form();
  1803. foreach ($q->param())
  1804. {
  1805. # Ignore certain params.
  1806. next if /^(cmd|queryname)$/;
  1807. print $q->hidden($_);
  1808. }
  1809. print "<table>\n",
  1810. "<tr>\n",
  1811. "<td>Remember this query as:</td>\n",
  1812. "<td>",
  1813. $q->textfield(-name=>'queryname', -size=>25),
  1814. "</td>\n<td>";
  1815. # Note: include hidden 'cmd' so user can simply press Enter w/o clicking.
  1816. print $q->hidden(-name=>'cmd', -value=>'store query', -override=>1),
  1817. $q->submit('cmd', 'store query'),
  1818. "</td>\n</tr>\n</table>",
  1819. $q->end_form();
  1820. }
  1821. # store_query -
  1822. # Save the current query in a cookie.
  1823. #
  1824. # Queries are stored as individual cookies named
  1825. # 'gnatsweb-query-$queryname'.
  1826. #
  1827. sub store_query
  1828. {
  1829. my $debug = 0;
  1830. my $queryname = $q->param('queryname');
  1831. # First make sure we don't already have too many cookies.
  1832. # See http://home.netscape.com/newsref/std/cookie_spec.html for
  1833. # limitations -- 20 cookies; 4k per cookie.
  1834. my(@cookie_names) = $q->cookie();
  1835. if (@cookie_names >= 20) {
  1836. error_page('Cannot store query -- too many cookies',
  1837. "Gnatsweb cannot store the query as another cookie because"
  1838. . "there already are "
  1839. . scalar(@cookie_names)
  1840. . " cookies being passed to gnatsweb. There is a maximum"
  1841. . " of 20 cookies per server or domain as specified in"
  1842. . " http://home.netscape.com/newsref/std/cookie_spec.html");
  1843. exit();
  1844. }
  1845. # Don't save certain params.
  1846. $q->delete('cmd');
  1847. # Have to generate the cookie before printing the header.
  1848. my $query_string = $q->query_string();
  1849. my $new_cookie = $q->cookie(-name => "gnatsweb-query-$queryname",
  1850. -value => $query_string,
  1851. -path => $global_cookie_path,
  1852. -expires => '+10y');
  1853. print $q->header(-cookie => $new_cookie);
  1854. # Now print the page.
  1855. my $page = 'Query Saved';
  1856. page_start_html($page);
  1857. page_heading($page, 'Query Saved');
  1858. print "<h2>debugging</h2><pre>",
  1859. "query_string: $query_string",
  1860. "cookie: $new_cookie\n",
  1861. "</pre><hr>\n"
  1862. if $debug;
  1863. print "<p>Your query \"$queryname\" has been saved. It will be available ",
  1864. "the next time you reload the Query page.";
  1865. page_footer($page);
  1866. page_end_html($page);
  1867. }
  1868. # print_stored_queries -
  1869. # Retrieve any stored queries and print out a short form allowing
  1870. # the submission of these queries.
  1871. #
  1872. # Queries are stored as individual cookies named
  1873. # 'gnatsweb-query-$queryname'.
  1874. #
  1875. # side effects:
  1876. # Sets global %stored_queries.
  1877. #
  1878. sub print_stored_queries
  1879. {
  1880. %stored_queries = ();
  1881. foreach my $cookie ($q->cookie())
  1882. {
  1883. if ($cookie =~ /gnatsweb-query-(.*)/)
  1884. {
  1885. $stored_queries{$1} = $q->cookie($cookie);
  1886. }
  1887. }
  1888. if (%stored_queries)
  1889. {
  1890. print "<table cellspacing=0 cellpadding=0 border=0>",
  1891. "<tr valign=top>",
  1892. $q->start_form(),
  1893. "<td>",
  1894. hidden_db(),
  1895. $q->submit('cmd', 'submit stored query'),
  1896. "<td>&nbsp;<td>",
  1897. $q->popup_menu(-name=>'queryname',
  1898. -values=>[ sort(keys %stored_queries) ]),
  1899. $q->end_form(),
  1900. $q->start_form(),
  1901. "<td>",
  1902. $q->hidden('return_url', $q->self_url()),
  1903. hidden_db(),
  1904. $q->submit('cmd', 'delete stored query'),
  1905. "<td>&nbsp;<td>",
  1906. $q->popup_menu(-name=>'queryname',
  1907. -values=>[ sort(keys %stored_queries) ]),
  1908. $q->end_form(),
  1909. "</tr></table>";
  1910. }
  1911. }
  1912. # submit_stored_query -
  1913. # Submit the query named in the param 'queryname'.
  1914. #
  1915. # Queries are stored as individual cookies named
  1916. # 'gnatsweb-query-$queryname'.
  1917. #
  1918. sub submit_stored_query
  1919. {
  1920. my $debug = 0;
  1921. my $queryname = $q->param('queryname');
  1922. my $query_string;
  1923. my $err = '';
  1924. if (!$queryname)
  1925. {
  1926. $err = "Internal error: no 'queryname' parameter";
  1927. }
  1928. elsif (!($query_string = $q->cookie("gnatsweb-query-$queryname")))
  1929. {
  1930. $err = "No such named query: $queryname";
  1931. }
  1932. if ($err)
  1933. {
  1934. print $q->header(),
  1935. $q->start_html('Error'),
  1936. $q->h3($err),
  1937. $q->end_html();
  1938. }
  1939. else
  1940. {
  1941. # 9/10/99 kenstir: Must use full (not relative) URL in redirect.
  1942. # Patch by Elgin Lee <ehl@terisa.com>.
  1943. my $query_url = $q->url() . '?cmd=' . $q->escape('submit query')
  1944. . '&' . $query_string;
  1945. if ($debug)
  1946. {
  1947. print $q->header(),
  1948. $q->start_html(),
  1949. $q->pre("debug: query_url: $query_url\n");
  1950. }
  1951. else
  1952. {
  1953. print $q->redirect($query_url);
  1954. }
  1955. }
  1956. }
  1957. # delete_stored_query -
  1958. # Delete the query named in the param 'queryname'.
  1959. #
  1960. # Queries are stored as individual cookies named
  1961. # 'gnatsweb-query-$queryname'.
  1962. #
  1963. sub delete_stored_query
  1964. {
  1965. my $debug = 0;
  1966. my $queryname = $q->param('queryname');
  1967. my $query_string;
  1968. my $err = '';
  1969. if (!$queryname)
  1970. {
  1971. $err = "Internal error: no 'queryname' parameter";
  1972. }
  1973. elsif (!($query_string = $q->cookie("gnatsweb-query-$queryname")))
  1974. {
  1975. $err = "No such named query: $queryname";
  1976. }
  1977. if ($err)
  1978. {
  1979. print $q->header(),
  1980. $q->start_html('Error'),
  1981. $q->h3($err),
  1982. $q->end_html();
  1983. }
  1984. else
  1985. {
  1986. # The negative -expire causes the old cookie to expire immediately.
  1987. my $expire_cookie_with_path =
  1988. $q->cookie(-name => "gnatsweb-query-$queryname",
  1989. -value => 'does not matter',
  1990. -path => $global_cookie_path,
  1991. -expires => '-1d');
  1992. my $expire_cookies = $expire_cookie_with_path;
  1993. # If we're using a non-empty $global_cookie_path, then we need to
  1994. # create two expire cookies. One or the other will delete the stored
  1995. # query, depending on whether the query was created with this version
  1996. # of gnatsweb, or with an older version.
  1997. if ($global_cookie_path)
  1998. {
  1999. my $expire_cookie_no_path =
  2000. $q->cookie(-name => "gnatsweb-query-$queryname",
  2001. -value => 'does not matter',
  2002. # No -path here!
  2003. -expires => '-1d');
  2004. $expire_cookies = [ $expire_cookie_with_path, $expire_cookie_no_path ];
  2005. }
  2006. # Return the user to the page they were viewing when they pressed
  2007. # 'delete stored query'.
  2008. my $return_url = $q->param('return_url') || get_script_name();
  2009. print $q->header(-Refresh => "0; URL=$return_url",
  2010. -cookie => $expire_cookies);
  2011. # Workaround for MSIE:
  2012. print "<HTML><HEAD><TITLE></TITLE>"
  2013. , "<META HTTP-EQUIV=\"Refresh\" CONTENT=\"0; URL=$return_url\"></HEAD>";
  2014. }
  2015. }
  2016. # send_html -
  2017. # Send HTML help file, after first trimming out everything but
  2018. # <body>..</body>. This is done in this way for convenience of
  2019. # installation. If the gnatsweb.html is installed into the cgi-bin
  2020. # directory along with the gnatsweb.pl file, then it can't be loaded
  2021. # directly by Apache. So, we send it indirectly through gnatsweb.pl.
  2022. # This approach has the benefit that the resulting page has the
  2023. # customized gnatsweb look.
  2024. #
  2025. sub send_html
  2026. {
  2027. my $file = shift;
  2028. open(HTML, "<$file") || die "Can't open $file: $!";
  2029. undef $/; # slurp file whole
  2030. my $html = <HTML>;
  2031. close(HTML);
  2032. # send just the stuff inside <body>..</body>
  2033. $html =~ s/.*<body>//is;
  2034. $html =~ s/<\/body>.*//is;
  2035. print $html;
  2036. }
  2037. sub help_page
  2038. {
  2039. my $html_file = 'gnatsweb.html';
  2040. my $page = $q->param('help_title') || 'Help';
  2041. my $heading = $page;
  2042. page_start_html($page);
  2043. page_heading($page, $heading);
  2044. # If send_html doesn't work, print some default, very limited, help text.
  2045. if (!send_html($html_file))
  2046. {
  2047. print p('Welcome to our problem report database. ',
  2048. 'You\'ll notice that here we call them "problem reports" ',
  2049. 'or "PR\'s", not "bugs".');
  2050. print p('This web interface is called "gnatsweb". ',
  2051. 'The database system itself is called "gnats".',
  2052. 'You may want to peruse ',
  2053. a({-href=>"$gnats_info_top"}, 'the gnats manual'),
  2054. 'to read about bug lifecycles and the like, ',
  2055. 'but then again, you may not.');
  2056. }
  2057. page_footer($page);
  2058. page_end_html($page);
  2059. }
  2060. # hidden_db -
  2061. # Return hidden form element to maintain current database. This
  2062. # enables people to keep two browser windows open to two databases.
  2063. #
  2064. sub hidden_db
  2065. {
  2066. return $q->hidden(-name=>'database', -value=>$global_prefs{'database'},
  2067. -override=>1);
  2068. }
  2069. # one_line_form -
  2070. # One line, two column form used for main page.
  2071. #
  2072. sub one_line_form
  2073. {
  2074. my($label, @form_body) = @_;
  2075. return one_line_layout($label,
  2076. $q->start_form(-name=>$label),
  2077. hidden_db(),
  2078. @form_body,
  2079. $q->end_form());
  2080. }
  2081. # one_line_layout -
  2082. # One line, two column layout used by forms on main page.
  2083. #
  2084. sub one_line_layout
  2085. {
  2086. my($label, @rhs) = @_;
  2087. my $valign = 'baseline';
  2088. return $q->Tr({-valign=>$valign},
  2089. $q->td($q->b($label)),
  2090. $q->td('&nbsp;'),
  2091. $q->td(@rhs));
  2092. }
  2093. # one_line_submit -
  2094. # Submit button which takes up less vertical space.
  2095. # Used by callers to one_line_form().
  2096. #
  2097. sub one_line_submit
  2098. {
  2099. my($name, $value, $exclude_hidden_input) = @_;
  2100. my $html = '';
  2101. # This is a basic implementation which doesn't use Javascript, but
  2102. # takes up more vertical space. Requiring JavaScript is unacceptable
  2103. # for many sites. For sites where this is OK, comment the following
  2104. # line, then uncomment the subsequent 4 lines.
  2105. $html .= $q->submit($name,$value);
  2106. # $html .= $q->hidden($name, $value)
  2107. # unless $exclude_hidden_input;
  2108. # $html .= qq{<input type="button" value="$value"
  2109. # onclick="this.form.cmd.value = '$value'; submit()">};
  2110. return $html;
  2111. }
  2112. # can_edit -
  2113. # Return true if the user has edit priviledges or better.
  2114. #
  2115. sub can_edit
  2116. {
  2117. return ($access_level =~ /edit|admin/);
  2118. }
  2119. sub main_page
  2120. {
  2121. my $page = 'Main';
  2122. my $viewcmd = $include_audit_trail ? 'view audit-trail' : 'view';
  2123. print_header();
  2124. page_start_html($page);
  2125. page_heading($page, 'Main Page', 1);
  2126. print '<p><table cellspacing=0 cellpadding=0 border=0>';
  2127. my $top_buttons_html = cb('main_page_top_buttons') || '';
  2128. print $top_buttons_html;
  2129. print one_line_form('Create Problem Report:',
  2130. one_line_submit('cmd', 'create'));
  2131. # Only include Edit action if user is allowed to edit PRs.
  2132. # Note: include hidden 'cmd' so user can simply type into the textfield
  2133. # and press Enter w/o clicking.
  2134. print one_line_form('Edit Problem Report:',
  2135. hidden(-name=>'cmd', -value=>'edit', -override=>1),
  2136. one_line_submit('unused', 'edit', 1),
  2137. '#',
  2138. textfield(-size=>6, -name=>'pr'))
  2139. if can_edit();
  2140. print one_line_form('View Problem Report:',
  2141. hidden(-name=>'cmd', -value=>$viewcmd, -override=>1),
  2142. one_line_submit('unused', 'view', 1),
  2143. '#',
  2144. textfield(-size=>6, -name=>'pr'));
  2145. print one_line_form('Query Problem Reports:',
  2146. one_line_submit('cmd', 'query', 1),
  2147. '&nbsp;',
  2148. one_line_submit('cmd', 'advanced query', 1));
  2149. print one_line_form('Login Again:',
  2150. one_line_submit('cmd', 'login again'));
  2151. print one_line_form('Get Help:',
  2152. one_line_submit('cmd', 'help'));
  2153. my $bot_buttons_html = cb('main_page_bottom_buttons') || '';
  2154. print $bot_buttons_html;
  2155. print '</table>';
  2156. page_footer($page);
  2157. print '<hr><small>'
  2158. . 'Gnatsweb by Matt Gerassimoff and Kenneth H. Cox<br>'
  2159. . "Gnatsweb v$VERSION, Gnats v$GNATS_VERS"
  2160. . '</small>';
  2161. page_end_html($page);
  2162. }
  2163. # cb -
  2164. #
  2165. # Calls site_callback subroutine if defined.
  2166. #
  2167. # usage:
  2168. # $something = cb($reason, @args) || 'default_value';
  2169. # # -or-
  2170. # $something = cb($reason, @args)
  2171. # $something = 'default_value' unless defined($something);
  2172. #
  2173. # arguments:
  2174. # $reason - reason for the call. Each reason is unique.
  2175. # @args - additional parameters may be provided in @args.
  2176. #
  2177. # returns:
  2178. # undef if &site_callback is not defined,
  2179. # else value returned by &site_callback.
  2180. #
  2181. sub cb
  2182. {
  2183. my($reason, @args) = @_;
  2184. my @val = undef;
  2185. if (defined &site_callback)
  2186. {
  2187. (@val) = site_callback($reason, @args);
  2188. }
  2189. # Do not include spaces. Avoids having to encode them - encoded
  2190. # spaces confuse MOZILLA.
  2191. return wantarray ? %addrs : join(',', keys(%addrs));
  2192. }
  2193. # print_header -
  2194. # Print HTTP header unless it's been printed already.
  2195. #
  2196. sub print_header
  2197. {
  2198. # Protect against multiple calls.
  2199. return if $print_header_done;
  2200. $print_header_done = 1;
  2201. print $q->header(@_);
  2202. }
  2203. # page_start_html -
  2204. #
  2205. # Print the HTML which starts off each page (<html><head>...</head>).
  2206. #
  2207. # By default, print a banner containing $site_banner_text, followed
  2208. # by the given page $title.
  2209. #
  2210. # The starting HTML can be overridden by &site_callback.
  2211. #
  2212. # Supports debugging.
  2213. #
  2214. # arguments:
  2215. # $title - title of page
  2216. # $no_button_bar - do not print the button bar
  2217. # $head_already_done - the <head> part has already been printed
  2218. sub page_start_html
  2219. {
  2220. my $title = shift;
  2221. my $no_button_bar = shift;
  2222. my $head_already_done = shift;
  2223. my $debug = 0;
  2224. # Protect against multiple calls.
  2225. return if $page_start_html_done;
  2226. $page_start_html_done = 1;
  2227. # Allow site callback to override html.
  2228. my $html = cb('page_start_html', $title);
  2229. if ($html)
  2230. {
  2231. print $html;
  2232. return;
  2233. }
  2234. # Call start_html, with -bgcolor if we need to override that.
  2235. unless ($head_already_done)
  2236. {
  2237. my @args = (-title=>"$title - $site_banner_text");
  2238. push(@args, -bgcolor=>$site_background)
  2239. if defined($site_background);
  2240. push(@args, -style=>{-src=>$site_stylesheet})
  2241. if defined($site_stylesheet);
  2242. print $q->start_html(@args);
  2243. }
  2244. # Add the page banner. This banner is a string slammed to the right
  2245. # of a 100% width table. The data is a link back to the main page.
  2246. #
  2247. # Note that the banner uses inline style, rather than a GIF; this
  2248. # makes installation easier by eliminating the need to install GIFs
  2249. # into a separate directory. At least for Apache, you can't serve
  2250. # GIFs out of your CGI directory.
  2251. # Add the page banner. The $site_banner_text is linked back to the
  2252. # main page.
  2253. #
  2254. # Note that the banner uses inline style, rather than a GIF; this
  2255. # makes installation easier by eliminating the need to install GIFs
  2256. # into a separate directory. At least for Apache, you can't serve
  2257. # GIFs out of your CGI directory.
  2258. #
  2259. my $bannerstyle = <<EOF;
  2260. color: $site_banner_foreground;
  2261. font-family: 'Verdana', 'Arial', 'Helvetica', 'sans';
  2262. font-weight: light;
  2263. text-decoration: none;
  2264. EOF
  2265. my $buttonstyle = <<EOF;
  2266. color: $site_button_foreground;
  2267. font-family: 'Verdana', 'Arial', 'Helvetica', 'sans';
  2268. font-size: 8pt;
  2269. font-weight: normal;
  2270. text-decoration: none;
  2271. EOF
  2272. my $banner_fontsize1 = "font-size: 14pt; ";
  2273. my $banner_fontsize2 = "font-size: 8pt; ";
  2274. my($row, $row2, $banner);
  2275. my $url = $sn;
  2276. $url .= "?database=$global_prefs{'database'}"
  2277. if defined($global_prefs{'database'});
  2278. $createurl = get_createpr_url(0,1);
  2279. $row = qq(<TR>\n<TD><TABLE BORDER="0" CELLSPACING="0" CELLPADDING="3" WIDTH="100%">);
  2280. $row .= qq(<TR STYLE="background-color: $site_banner_background">\n<TD ALIGN="LEFT">);
  2281. $row .= qq(<SPAN STYLE="$bannerstyle $banner_fontsize1">$global_prefs{'database'}&nbsp;&nbsp;</SPAN>)
  2282. if $global_prefs{'database'};
  2283. $row .= qq(<SPAN STYLE="$bannerstyle $banner_fontsize2">User: $db_prefs{'user'}&nbsp;&nbsp;</SPAN>)
  2284. if $db_prefs{'user'};
  2285. $row .= qq(<SPAN STYLE="$bannerstyle $banner_fontsize2">Access: $access_level</SPAN>)
  2286. if $access_level;
  2287. $row .= qq(\n</TD>\n<TD ALIGN="RIGHT">
  2288. <A HREF="$url" STYLE="$bannerstyle $banner_fontsize1">$site_banner_text</A>
  2289. </TD>\n</TR>\n</TABLE></TD></TR>\n);
  2290. $row2 = qq(<TR>\n<TD COLSPAN="2">);
  2291. $row2 .= qq(<TABLE BORDER="1" CELLSPACING="0" BGCOLOR="$site_button_background" CELLPADDING="3">);
  2292. $row2 .= qq(<TR>\n);
  2293. $row2 .= qq(<TD><A HREF="$url" STYLE="$buttonstyle">MAIN PAGE</A></TD>);
  2294. $row2 .= qq(<TD><A HREF="$createurl" STYLE="$buttonstyle">CREATE</A></TD>);
  2295. $row2 .= qq(<TD><A HREF="$url&cmd=query" STYLE="$buttonstyle">QUERY</A></TD>);
  2296. $row2 .= qq(<TD><A HREF="$url&cmd=advanced%20query" STYLE="$buttonstyle">ADV. QUERY</A></TD>);
  2297. $row2 .= qq(<TD><A HREF="$url&cmd=login%20again" STYLE="$buttonstyle">LOGIN AGAIN</A></TD>);
  2298. $row2 .= qq(<TD><A HREF="$url&cmd=help" STYLE="$buttonstyle">HELP</A></TD>);
  2299. $row2 .= qq(</TR>\n);
  2300. $row2 .= qq(</TABLE>\n</TD>\n</TR>);
  2301. $banner = qq(<TABLE WIDTH="100%" BORDER="0" CELLPADDING="0" CELLSPACING="0">$row);
  2302. $banner .= qq($row2) unless $no_button_bar;
  2303. $banner .= qq(</TABLE>);
  2304. print $banner;
  2305. # debugging
  2306. if ($debug)
  2307. {
  2308. print "<h3>debugging params</h3><font size=1><pre>";
  2309. my($param,@val);
  2310. foreach $param (sort $q->param())
  2311. {
  2312. @val = $q->param($param);
  2313. printf "%-12s %s\n", $param, $q->escapeHTML(join(' ', @val));
  2314. }
  2315. print "</pre></font><hr>\n";
  2316. }
  2317. }
  2318. # page_heading -
  2319. #
  2320. # Print the HTML which starts off a page. Basically a fancy <h1>
  2321. # plus user + database names.
  2322. #
  2323. sub page_heading
  2324. {
  2325. my($title, $heading, $display_user_info, $display_date) = @_;
  2326. # Protect against multiple calls.
  2327. return if $page_heading_done;
  2328. $page_heading_done = 1;
  2329. # Allow site callback to override html.
  2330. my $html = cb('page_heading', $title, $heading);
  2331. if ($html)
  2332. {
  2333. print $html;
  2334. return;
  2335. }
  2336. my $leftcol = $heading ? $heading : '&nbsp;';
  2337. my $rightcol;
  2338. print $q->h1($leftcol);
  2339. }
  2340. # page_footer -
  2341. #
  2342. # Allow the site_callback to take control before the end of the
  2343. # page.
  2344. #
  2345. sub page_footer
  2346. {
  2347. my $title = shift;
  2348. my $html = cb('page_footer', $title);
  2349. print $html if ($html);
  2350. }
  2351. # page_end_html -
  2352. #
  2353. # Print the HTML which ends a page. Allow the site_callback to
  2354. # take control here too.
  2355. #
  2356. sub page_end_html
  2357. {
  2358. my $title = shift;
  2359. # Allow site callback to override html.
  2360. my $html = cb('page_end_html', $title);
  2361. if ($html)
  2362. {
  2363. print $html;
  2364. return;
  2365. }
  2366. print $q->end_html();
  2367. }
  2368. # fix_multiline_val -
  2369. # Modify text of multitext field so that it contains \n separators
  2370. # (not \r\n or \n as some platforms use), and so that it has a \n
  2371. # at the end.
  2372. #
  2373. sub fix_multiline_val
  2374. {
  2375. my $val = shift;
  2376. $val =~ s/^>([^ ])/> $1/gm;
  2377. $val =~ s/\r\n?/\n/g;
  2378. $val .= "\n" unless $val =~ /\n$/;
  2379. $val;
  2380. }
  2381. # parse_config -
  2382. # Parse the config file, storing the name/value pairs in the global
  2383. # hash %config.
  2384. sub parse_config
  2385. {
  2386. my(@lines) = @_;
  2387. %config = ();
  2388. # Default value for GNATS_ADDR is 'bugs'.
  2389. $config{'GNATS_ADDR'} = 'bugs';
  2390. # Note that the values may be quoted, as the config file uses
  2391. # Bourne-shell syntax.
  2392. foreach $_ (@lines)
  2393. {
  2394. if (/(\S+)\s*=\s*['"]?([^'"]*)['"]?/)
  2395. {
  2396. $config{$1} = $2;
  2397. }
  2398. }
  2399. }
  2400. # parse_categories -
  2401. # Parse the categories file.
  2402. sub parse_categories
  2403. {
  2404. my(@lines) = @_;
  2405. @category = ("all");
  2406. %category_notify = ();
  2407. %category_responsible = ();
  2408. %category_desc = ();
  2409. foreach $_ (sort @lines)
  2410. {
  2411. my($cat, $desc, $resp, $notify) = split(/:/);
  2412. # Uncomment to exclude administrative category 'pending'.
  2413. # next if($cat eq 'pending');
  2414. push(@category, $cat);
  2415. $category_responsible{$cat} = $resp;
  2416. $category_notify{$cat} = $notify;
  2417. $category_desc{$cat} = $cat . ' - ' . $desc;
  2418. }
  2419. }
  2420. # parse_submitters -
  2421. # Parse the submitters file.
  2422. sub parse_submitters
  2423. {
  2424. my(@lines) = @_;
  2425. @submitter_id = ("all");
  2426. %submitter_contact = ();
  2427. %submitter_notify = ();
  2428. foreach $_ (sort @lines)
  2429. {
  2430. my($submitter, $full_name, $type, $response_time, $contact, $notify)
  2431. = split(/:/);
  2432. push(@submitter_id, $submitter);
  2433. $submitter_fullname{$submitter} = $submitter . ' - ' . $full_name;
  2434. $submitter_contact{$submitter} = $contact;
  2435. $submitter_notify{$submitter} = $notify;
  2436. }
  2437. }
  2438. # parse_responsible -
  2439. # Parse the responsible file.
  2440. sub parse_responsible
  2441. {
  2442. my(@lines) = @_;
  2443. @responsible = ("all");
  2444. %responsible_fullname = ();
  2445. %responsible_address = ();
  2446. foreach $_ (sort @lines)
  2447. {
  2448. my($person, $fullname, $address) = split(/:/);
  2449. push(@responsible, $person);
  2450. $responsible_fullname{$person} = $person . ' - ' . $fullname;
  2451. $responsible_address{$person} = $address || $person;
  2452. }
  2453. }
  2454. # connect_to_gnatsd -
  2455. # Connect to gnatsd.
  2456. #
  2457. sub connect_to_gnatsd
  2458. {
  2459. my($response) = client_init($site_gnats_host, $site_gnats_port);
  2460. if (!$response) {
  2461. error_page("Error: Couldn't connect to gnats server",
  2462. "host $site_gnats_host, port $site_gnats_port<br>"
  2463. . $gnats::ERRSTR);
  2464. exit();
  2465. }
  2466. return $response;
  2467. }
  2468. # initialize -
  2469. # Initialize gnatsd-related globals and login to gnatsd.
  2470. #
  2471. sub initialize
  2472. {
  2473. my $regression_testing = shift;
  2474. @severity = ("all", "critical", "serious", "non-critical");
  2475. @priority = ("all", "high", "medium", "low");
  2476. @confidential = ("all", "no", "yes");
  2477. # @fields - param names of columns displayable in query results
  2478. # @deffields - default displayed columns
  2479. @deffields = ("category", "state", "responsible", "synopsis");
  2480. @fields = ("category", "confidential", "state", "class",
  2481. "severity", "priority",
  2482. "release", "quarter", "responsible", "submitter_id", "originator",
  2483. "arrival_date", "date_required",
  2484. "last_modified", "closed_date", "synopsis");
  2485. #GCC-LOCAL begin.
  2486. @confidential = ("all", "no");
  2487. @deffields = ("category", "state", "class", "responsible", "synopsis");
  2488. #GCC-LOCAL end.
  2489. # @fieldnames - fields appear in the standard order, defined by pr.h
  2490. @fieldnames = (
  2491. "Number",
  2492. "Category",
  2493. "Synopsis",
  2494. "Confidential",
  2495. "Severity",
  2496. "Priority",
  2497. "Responsible",
  2498. "State",
  2499. "Quarter",
  2500. "Keywords",
  2501. "Date-Required",
  2502. "Class",
  2503. "Submitter-Id",
  2504. "Arrival-Date",
  2505. "Closed-Date",
  2506. "Last-Modified",
  2507. "Originator",
  2508. "Release",
  2509. "Organization",
  2510. "Environment",
  2511. "Description",
  2512. "How-To-Repeat",
  2513. "Fix",
  2514. "Release-Note",
  2515. "Audit-Trail",
  2516. "Unformatted",
  2517. );
  2518. # %fieldnames maps the field name to a flag value composed of bits.
  2519. # See $MULTILINE above for bit definitions.
  2520. %fieldnames = (
  2521. "Number" => $SENDEXCLUDE | $EDITEXCLUDE,
  2522. "Category" => $ENUM,
  2523. "Synopsis" => 0,
  2524. "Confidential" => $ENUM,
  2525. "Severity" => $ENUM,
  2526. "Priority" => $ENUM,
  2527. "Responsible" => $ENUM | $REASONCHANGE | $SENDEXCLUDE | $AUDITINCLUDE,
  2528. "State" => $ENUM | $REASONCHANGE | $SENDEXCLUDE | $AUDITINCLUDE,
  2529. "Quarter" => 0,
  2530. "Keywords" => 0,
  2531. "Date-Required" => 0,
  2532. "Class" => $ENUM,
  2533. "Submitter-Id" => $ENUM | $EDITEXCLUDE,
  2534. "Arrival-Date" => $SENDEXCLUDE | $EDITEXCLUDE,
  2535. "Closed-Date" => $SENDEXCLUDE | $EDITEXCLUDE,
  2536. "Last-Modified" => $SENDEXCLUDE | $EDITEXCLUDE,
  2537. "Originator" => $EDITEXCLUDE,
  2538. "Release" => 0,
  2539. "Organization" => $MULTILINE | $SENDEXCLUDE | $EDITEXCLUDE, # => $MULTILINE
  2540. "Environment" => $MULTILINE,
  2541. "Description" => $MULTILINE,
  2542. "How-To-Repeat" => $MULTILINE,
  2543. "Fix" => $MULTILINE,
  2544. "Release-Note" => $MULTILINE | $SENDEXCLUDE,
  2545. "Audit-Trail" => $MULTILINE | $SENDEXCLUDE | $EDITEXCLUDE,
  2546. "Unformatted" => $MULTILINE | $SENDEXCLUDE | $EDITEXCLUDE,
  2547. );
  2548. $attachment_delimiter = "----gnatsweb-attachment----\n";
  2549. # gnatsd query commands: maps param name to gnatsd command
  2550. %gnatsd_query = (
  2551. "category" => 'catg',
  2552. "synopsis" => 'synp',
  2553. "confidential" => 'conf',
  2554. "severity" => 'svty',
  2555. "priority" => 'prio',
  2556. "responsible" => 'resp',
  2557. "state" => 'stat',
  2558. "class" => 'clss',
  2559. "submitter_id" => 'subm',
  2560. "originator" => 'orig',
  2561. "release" => 'rlse',
  2562. "text" => 'text',
  2563. "multitext" => 'mtxt',
  2564. "arrivedbefore" => 'abfr',
  2565. "arrivedafter" => 'araf',
  2566. "modifiedbefore" => 'mbfr',
  2567. "modifiedafter" => 'maft',
  2568. "closedbefore" => 'cbfr',
  2569. "closedafter" => 'caft',
  2570. "quarter" => 'qrtr',
  2571. "keywords" => 'kywd',
  2572. "requiredbefore" => 'bfor',
  2573. "requiredafter" => 'aftr',
  2574. );
  2575. # clear out some unused fields if not used
  2576. if (!$site_release_based)
  2577. {
  2578. @fields = grep(!/quarter|keywords|date_required/, @fields);
  2579. @fieldnames = grep(!/Quarter|Keywords|Date-Required/, @fieldnames);
  2580. }
  2581. my(@lines);
  2582. my($response);
  2583. # Get gnatsd version from initial server connection text.
  2584. $GNATS_VERS = 999.0;
  2585. $response = connect_to_gnatsd();
  2586. if ($response =~ /GNATS server (.*) ready/)
  2587. {
  2588. $GNATS_VERS = $1;
  2589. }
  2590. # Login to selected database.
  2591. LOGIN:
  2592. {
  2593. # Issue CHDB command; revert to login page if it fails.
  2594. ($response) = client_cmd("chdb $global_prefs{'database'}");
  2595. if (!$response)
  2596. {
  2597. login_page($q->self_url(), $gnats::ERRSTR);
  2598. exit();
  2599. }
  2600. # Get user permission level from USER command. Revert to the
  2601. # login page if the command fails.
  2602. ($response) = client_cmd("user $db_prefs{'user'} $db_prefs{'password'}");
  2603. if (!$response)
  2604. {
  2605. login_page($q->self_url(), $gnats::ERRSTR);
  2606. exit();
  2607. }
  2608. $access_level = 'edit';
  2609. if ($response =~ /User access level set to (\w*)/)
  2610. {
  2611. $access_level = $1;
  2612. }
  2613. }
  2614. # Get some enumerated lists
  2615. my($x, $dummy);
  2616. @state = ("all");
  2617. foreach $_ (client_cmd("lsta"))
  2618. {
  2619. ($x, $dummy) = split(/:/);
  2620. push(@state, $x);
  2621. }
  2622. @class = ("all");
  2623. foreach $_ (client_cmd("lcla"))
  2624. {
  2625. ($x, $dummy) = split(/:/);
  2626. push(@class, $x);
  2627. }
  2628. # List various gnats-adm files, and parse their contents for data we
  2629. # will need later. Each parse subroutine stashes information away in
  2630. # its own global vars. The call to client_cmd() happens here to
  2631. # enable regression testing of the parse subs using fixed files.
  2632. @lines = client_cmd("lcfg");
  2633. parse_config(@lines);
  2634. @lines = client_cmd("lcat");
  2635. parse_categories(@lines);
  2636. @lines = client_cmd("lsub");
  2637. parse_submitters(@lines);
  2638. @lines = client_cmd("lres");
  2639. parse_responsible(@lines);
  2640. # Now that everything's all set up, let the site_callback have at it.
  2641. # It's return value doesn't matter, but here it can muck with our defaults.
  2642. cb('initialize');
  2643. }
  2644. # trim_responsible -
  2645. # Trim the value of the Responsible field to get a
  2646. # valid responsible person. This exists here, and in gnats itself
  2647. # (modify_pr(), check_pr(), gnats(), append_report()), for
  2648. # compatibility with old databases, which had 'person (Full Name)'
  2649. # in the Responsible field.
  2650. sub trim_responsible
  2651. {
  2652. my $resp = shift;
  2653. $resp =~ s/ .*//;
  2654. $resp;
  2655. }
  2656. # fix_email_addrs -
  2657. # Trim email addresses as they appear in an email From or Reply-To
  2658. # header into a comma separated list of just the addresses.
  2659. #
  2660. # Delete everything inside ()'s and outside <>'s, inclusive.
  2661. #
  2662. sub fix_email_addrs
  2663. {
  2664. my $addrs = shift;
  2665. my @addrs = split_csl ($addrs);
  2666. my @trimmed_addrs;
  2667. my $addr;
  2668. foreach $addr (@addrs)
  2669. {
  2670. $addr =~ s/\(.*\)//;
  2671. $addr =~ s/.*<(.*)>.*/$1/;
  2672. $addr =~ s/^\s+//;
  2673. $addr =~ s/\s+$//;
  2674. push(@trimmed_addrs, $addr);
  2675. }
  2676. $addrs = join(', ', @trimmed_addrs);
  2677. $addrs;
  2678. }
  2679. sub parsepr
  2680. {
  2681. # 9/18/99 kenstir: This two-liner can almost replace the next 30 or so
  2682. # lines of code, but not quite. It strips leading spaces from multiline
  2683. # fields.
  2684. #my $prtext = join("\n", @_);
  2685. #my(%fields) = ('envelope' => split /^>(\S*?):\s*/m, $prtext);
  2686. # my $prtext = join("\n", @_);
  2687. # my(%fields) = ('envelope' => split /^>(\S*?):(?: *|\n)/m, $prtext);
  2688. my $debug = 0;
  2689. my($hdrmulti) = "envelope";
  2690. my(%fields);
  2691. foreach (@_)
  2692. {
  2693. chomp($_);
  2694. $_ .= "\n";
  2695. if(!/^([>\w\-]+):\s*(.*)\s*$/)
  2696. {
  2697. if($hdrmulti ne "")
  2698. {
  2699. $fields{$hdrmulti} .= $_;
  2700. }
  2701. next;
  2702. }
  2703. local($hdr, $arg, $ghdr) = ($1, $2, "*not valid*");
  2704. if($hdr =~ /^>(.*)$/)
  2705. {
  2706. $ghdr = $1;
  2707. }
  2708. if(exists($fieldnames{$ghdr}))
  2709. {
  2710. if($fieldnames{$ghdr} & $MULTILINE)
  2711. {
  2712. $hdrmulti = $ghdr;
  2713. $fields{$ghdr} = "";
  2714. }
  2715. else
  2716. {
  2717. $hdrmulti = "";
  2718. $fields{$ghdr} = $arg;
  2719. }
  2720. }
  2721. elsif($hdrmulti ne "")
  2722. {
  2723. $fields{$hdrmulti} .= $_;
  2724. }
  2725. # Grab a few fields out of the envelope as it flies by
  2726. # 8/25/99 ehl: Grab these fields only out of the envelope, not
  2727. # any other multiline field.
  2728. if($hdrmulti eq "envelope" &&
  2729. ($hdr eq "Reply-To" || $hdr eq "From" || $hdr eq "X-GNATS-Notify"))
  2730. {
  2731. $arg = fix_email_addrs($arg);
  2732. $fields{$hdr} = $arg;
  2733. #print "storing, hdr = $hdr, arg = $arg\n";
  2734. }
  2735. }
  2736. # 5/8/99 kenstir: To get the reporter's email address, only
  2737. # $fields{'Reply-to'} is consulted. Initialized it from the 'From'
  2738. # header if it's not set, then discard the 'From' header.
  2739. $fields{'Reply-To'} = $fields{'Reply-To'} || $fields{'From'};
  2740. delete $fields{'From'};
  2741. # Ensure that the pseudo-fields are initialized to avoid perl warnings.
  2742. $fields{'X-GNATS-Notify'} ||= '';
  2743. # 3/30/99 kenstir: For some reason Unformatted always ends up with an
  2744. # extra newline here.
  2745. $fields{'Unformatted'} =~ s/\n$//;
  2746. # Decode attachments stored in Unformatted field.
  2747. my $any_attachments = 0;
  2748. if (can_do_mime()) {
  2749. my(@attachments) = split(/$attachment_delimiter/, $fields{'Unformatted'});
  2750. # First element is any random text which precedes delimited attachments.
  2751. $fields{'Unformatted'} = shift(@attachments);
  2752. foreach $attachment (@attachments) {
  2753. warn "att=>$attachment<=\n" if $debug;
  2754. $any_attachments = 1;
  2755. add_decoded_attachment_to_pr(\%fields, decode_attachment($attachment));
  2756. }
  2757. }
  2758. if ($debug) {
  2759. warn "--- parsepr fields ----\n";
  2760. my %fields_copy = %fields;
  2761. foreach (@fieldnames)
  2762. {
  2763. warn "$_ =>$fields_copy{$_}<=\n";
  2764. delete $fields_copy{$_}
  2765. }
  2766. warn "--- parsepr pseudo-fields ----\n";
  2767. foreach (sort keys %fields_copy) {
  2768. warn "$_ =>$fields_copy{$_}<=\n";
  2769. }
  2770. warn "--- parsepr attachments ---\n";
  2771. my $aref = $fields{'attachments'} || [];
  2772. foreach $href (@$aref) {
  2773. warn " ----\n";
  2774. while (($k,$v) = each %$href) {
  2775. warn " $k =>$v<=\n";
  2776. }
  2777. }
  2778. }
  2779. return %fields;
  2780. }
  2781. # unparsepr -
  2782. # Turn PR fields hash into a multi-line string.
  2783. #
  2784. # The $purpose arg controls how things are done. The possible values
  2785. # are:
  2786. # 'send' - PR will be submitted as a new PR via email
  2787. # 'gntasd' - PR will be filed using gnatsd; proper '.' escaping done
  2788. # 'test' - we're being called from the regression tests
  2789. sub unparsepr
  2790. {
  2791. my($purpose, %fields) = @_;
  2792. my($tmp, $text);
  2793. my $debug = 0;
  2794. # First create or reconstruct the Unformatted field containing the
  2795. # attachments, if any.
  2796. $fields{'Unformatted'} ||= ''; # Default to empty.
  2797. warn "unparsepr 1 =>$fields{'Unformatted'}<=\n" if $debug;
  2798. my $array_ref = $fields{'attachments'};
  2799. foreach $hash_ref (@$array_ref) {
  2800. my $attachment_data = $$hash_ref{'original_attachment'};
  2801. # Deleted attachments leave empty hashes behind.
  2802. next unless defined($attachment_data);
  2803. $fields{'Unformatted'} .= $attachment_delimiter . $attachment_data;
  2804. }
  2805. warn "unparsepr 2 =>$fields{'Unformatted'}<=\n" if $debug;
  2806. # Reconstruct the text of the PR into $text.
  2807. $text = $fields{'envelope'};
  2808. foreach (@fieldnames)
  2809. {
  2810. # Do include Unformatted field in 'send' operation, even though
  2811. # it's excluded. We need it to hold the file attachment.
  2812. #next if($purpose eq "send" && $fieldnames{$_} & $SENDEXCLUDE);
  2813. next if(($purpose eq 'send')
  2814. && ($fieldnames{$_} & $SENDEXCLUDE)
  2815. && ($_ ne 'Unformatted'));
  2816. if($fieldnames{$_} & $MULTILINE)
  2817. {
  2818. # Lines which begin with a '.' need to be escaped by another '.'
  2819. # if we're feeding it to gnatsd.
  2820. $tmp = $fields{$_};
  2821. $tmp =~ s/^[.]/../gm
  2822. if ($purpose eq 'gnatsd');
  2823. $text .= sprintf(">$_:\n%s", $tmp);
  2824. }
  2825. else
  2826. {
  2827. # Format string derived from gnats/pr.c.
  2828. $text .= sprintf("%-16s %s\n", ">$_:", $fields{$_});
  2829. }
  2830. }
  2831. return $text;
  2832. }
  2833. sub lockpr
  2834. {
  2835. my($pr, $user) = @_;
  2836. #print "<pre>locking $pr $user\n</pre>";
  2837. return parsepr(client_cmd("lock $pr $user"));
  2838. }
  2839. sub unlockpr
  2840. {
  2841. my($pr) = @_;
  2842. #print "<pre>unlocking $pr\n</pre>";
  2843. client_cmd("unlk $pr");
  2844. }
  2845. sub readpr
  2846. {
  2847. my($pr) = @_;
  2848. my(@result) = client_cmd("full $pr");
  2849. if ($gnats::ERRSTR) {
  2850. print_gnatsd_error($gnats::ERRSTR);
  2851. client_exit();
  2852. exit();
  2853. }
  2854. return parsepr(@result);
  2855. }
  2856. # interested_parties -
  2857. # Get list of parties to notify about a PR change.
  2858. #
  2859. # Returns hash in array context; string of email addrs otherwise.
  2860. sub interested_parties
  2861. {
  2862. my($pr, $include_gnats_addr, %fields) = @_;
  2863. # Gnats 3.110 has some problems in MLPR --
  2864. # * it includes the category's responsible person (even if that person
  2865. # is not responsible for this PR)
  2866. # * it does not include the PR's responsible person
  2867. # * it does not include the Reply-To or From
  2868. #
  2869. # So for now, don't use it. However, for versions after 3.110 my
  2870. # patch to the MLPR command should be there and this can be fixed.
  2871. my(@people);
  2872. my $person;
  2873. my $list;
  2874. ## Get list from MLPR command.
  2875. #@people = client_cmd("mlpr $pr");
  2876. # Ignore intro message
  2877. #@people = grep(!/Addresses to notify/, @people);
  2878. # Get list of people by constructing it ourselves.
  2879. @people = ();
  2880. my(@prospect_list) = ($fields{'Reply-To'},
  2881. $fields{'Responsible'},
  2882. $fields{'X-GNATS-Notify'},
  2883. $category_notify{$fields{'Category'}},
  2884. $submitter_contact{$fields{'Submitter-Id'}},
  2885. $submitter_notify{$fields{'Submitter-Id'}});
  2886. push(@prospect_list, $config{'GNATS_ADDR'})
  2887. if $include_gnats_addr;
  2888. foreach $list (@prospect_list) {
  2889. if (defined($list)) {
  2890. foreach $person (split_csl ($list)) {
  2891. push(@people, $person) if $person;
  2892. }
  2893. }
  2894. }
  2895. # Expand any unexpanded addresses, and build up the %addrs hash.
  2896. my(%addrs) = ();
  2897. my $addr;
  2898. foreach $person (@people)
  2899. {
  2900. $addr = praddr($person) || $person;
  2901. $addrs{$addr} = 1;
  2902. }
  2903. return wantarray ? %addrs : join(', ', keys(%addrs));
  2904. }
  2905. # Split comma-separated list.
  2906. # Commas in quotes are not separators!
  2907. sub split_csl {
  2908. my ($list) = @_;
  2909. # Substitute commas in quotes with \002.
  2910. while ($list =~ m~"([^"]*)"~g) {
  2911. my $pos = pos($list);
  2912. my $str = $1;
  2913. $str =~ s~,~\002~g;
  2914. $list =~ s~"[^"]*"~"$str"~;
  2915. pos($list) = $pos;
  2916. }
  2917. my @res;
  2918. foreach $person (split(/\s*,\s*/, $list)) {
  2919. $person =~ s/\002/,/g;
  2920. push(@res, $person) if $person;
  2921. }
  2922. return @res;
  2923. }
  2924. # praddr -
  2925. # Return email address of responsible person, or undef if not found.
  2926. sub praddr
  2927. {
  2928. my $person = shift;
  2929. # Done this way to avoid -w warning
  2930. my $addr = exists($responsible_address{$person})
  2931. ? $responsible_address{$person} : undef;
  2932. }
  2933. # login_page_javascript -
  2934. # Returns some Javascript code to test if cookies are being accepted.
  2935. #
  2936. sub login_page_javascript
  2937. {
  2938. my $ret = q{
  2939. <SCRIPT LANGUAGE="JavaScript" TYPE="text/javascript">
  2940. //<!--
  2941. // JavaScript courtesy of webcoder.com.
  2942. function getCookie(name) {
  2943. var cname = name + "=";
  2944. var dc = document.cookie;
  2945. if (dc.length > 0) {
  2946. begin = dc.indexOf(cname);
  2947. if (begin != -1) {
  2948. begin += cname.length;
  2949. end = dc.indexOf(";", begin);
  2950. if (end == -1) end = dc.length;
  2951. return unescape(dc.substring(begin, end));
  2952. }
  2953. }
  2954. return null;
  2955. }
  2956. function setCookie(name, value, expires) {
  2957. document.cookie = name + "=" + escape(value) + "; path=/" +
  2958. ((expires == null) ? "" : "; expires=" + expires.toGMTString());
  2959. }
  2960. function delCookie(name) {
  2961. document.cookie = name + "=; path=/; expires=Thu, 01-Jan-70 00:00:01 GMT";
  2962. }
  2963. exp = new Date();
  2964. exp.setTime(exp.getTime() + (1000 * 60 * 60)); // +1 hour
  2965. setCookie("gnatsweb-test-cookie", "whatever", exp);
  2966. val = getCookie("gnatsweb-test-cookie");
  2967. delCookie("gnatsweb-test-cookie");
  2968. if (val == null) {
  2969. document.write(
  2970. "<p><strong>Warning: your browser is not accepting cookies!</strong> "
  2971. +"Unfortunately, Gnatsweb requires cookies to keep track of your "
  2972. +"login and other information. "
  2973. +"Please enable cookies before logging in.</p>");
  2974. }
  2975. //-->
  2976. </SCRIPT>
  2977. <noscript>
  2978. <p>(Due to the fact that your browser does not support Javascript,
  2979. there is no way of telling whether it can accept cookies.)
  2980. Unfortunately, Gnatsweb requires cookies to keep track of your
  2981. login and other information.
  2982. Please enable cookies before logging in.</p>
  2983. </noscript>
  2984. };
  2985. }
  2986. # login_page -
  2987. # Show the login page.
  2988. #
  2989. # If $return_url passed in, then we are showing the login page because
  2990. # the user failed to login. In that case, when the login is
  2991. # successful, we want to redirect to the given url. For example, if a
  2992. # user follows a ?cmd=view url, but hasn't logged in yet, then we want
  2993. # to forward him to the originally requested url after logging in.
  2994. #
  2995. sub login_page
  2996. {
  2997. my($return_url, $err_msg) = @_;
  2998. my $page = 'Login';
  2999. print_header();
  3000. page_start_html($page, 1);
  3001. page_heading($page, 'Login');
  3002. # A previous error gets first billing.
  3003. if ($err_msg) {
  3004. print_gnatsd_error($err_msg);
  3005. }
  3006. # Inside the javascript a cookie warning can be printed.
  3007. print login_page_javascript();
  3008. # Connect to server.
  3009. connect_to_gnatsd();
  3010. # Get list of database aliases.
  3011. my(@dbs) = client_cmd("dbla");
  3012. my(@mydbs) = cb('list_databases', @dbs);
  3013. if(defined($mydbs[0])) {
  3014. @dbs = @mydbs;
  3015. #GCC-LOCAL begin: Do not offer all database, just "gcc".
  3016. @dbs = ("gcc");
  3017. #GCC-LOCAL end.
  3018. }
  3019. # Get a default username and password.
  3020. # Lousy assumption alert! Assume that if the site is requiring browser
  3021. # authentication (REMOTE_USER is defined), then their gnats passwords
  3022. # are not really needed; use the username as the default.
  3023. my $def_user = $db_prefs{'user'} || $ENV{'REMOTE_USER'};
  3024. my $def_password = $db_prefs{'password'} || $ENV{'REMOTE_USER'};
  3025. # Print the login form.
  3026. print $q->start_form(),
  3027. #GCC-LOCAL begin.
  3028. "<p>Use username `<em>guest</em>' and password `<em>guest</em>' ",
  3029. "for read-only and bug reporting access.</p>",
  3030. #GCC-LOCAL end.
  3031. "<table>",
  3032. "<tr>\n<td>User Name:</td>\n<td>",
  3033. $q->textfield(-name=>'user',
  3034. -size=>20,
  3035. -default=>$def_user),
  3036. "</td>\n</tr>\n<tr>\n<td>Password:</td>\n<td>",
  3037. $q->password_field(-name=>'password',
  3038. -value=>$def_password,
  3039. -size=>20),
  3040. "</td>\n</tr>\n<tr>\n<td>Database:</td>\n<td>",
  3041. $q->popup_menu(-name=>'database',
  3042. -values=>\@dbs,
  3043. -default=>$global_prefs{'database'}),
  3044. "</td>\n</tr>\n</table>";
  3045. print $q->hidden('return_url', $return_url)
  3046. if $return_url;
  3047. print $q->submit('cmd','login'),
  3048. $q->end_form();
  3049. page_footer($page);
  3050. page_end_html($page);
  3051. }
  3052. sub debug_print_all_cookies
  3053. {
  3054. # Debug: print all our cookies into server log.
  3055. warn "================= all cookies ===================================\n";
  3056. my @c;
  3057. $i = 0;
  3058. foreach my $y ($q->cookie())
  3059. {
  3060. @c = $q->cookie($y);
  3061. warn "got cookie: length=", scalar(@c), ": $y =>@c<=\n";
  3062. $i += length($y);
  3063. }
  3064. @c = $q->raw_cookie();
  3065. warn "debug 0.5: @c:\n";
  3066. warn "debug 0.5: total size of raw cookies: ", length("@c"), "\n";
  3067. }
  3068. # set_pref -
  3069. # Set the named preference. Param values override cookie values, and
  3070. # don't set it if we end up with an undefined value.
  3071. #
  3072. sub set_pref
  3073. {
  3074. my($pref_name, $pref_hashref, $cval_hashref) = @_;
  3075. my $val = $q->param($pref_name) || $$cval_hashref{$pref_name};
  3076. $$pref_hashref{$pref_name} = $val
  3077. if defined($val);
  3078. }
  3079. # init_prefs -
  3080. # Initialize global_prefs and db_prefs from cookies and params.
  3081. #
  3082. sub init_prefs
  3083. {
  3084. my $debug = 0;
  3085. if ($debug) {
  3086. debug_print_all_cookies();
  3087. # Don't 'use Data::Dumper' because that always loads and causes
  3088. # compile-time errors for those who don't have this module.
  3089. require Data::Dumper;
  3090. $Data::Dumper::Terse = $Data::Dumper::Terse = 1; # avoid -w warning
  3091. warn "-------------- init_prefs -------------------\n";
  3092. }
  3093. # Global prefs.
  3094. my %cvals = $q->cookie('gnatsweb-global');
  3095. %global_prefs = ();
  3096. set_pref('database', \%global_prefs, \%cvals);
  3097. set_pref('email', \%global_prefs, \%cvals);
  3098. set_pref('Originator', \%global_prefs, \%cvals);
  3099. set_pref('Submitter-Id', \%global_prefs, \%cvals);
  3100. # columns is treated differently because it's an array which is stored
  3101. # in the cookie as a joined string.
  3102. if ($q->param('columns')) {
  3103. my(@columns) = $q->param('columns');
  3104. $global_prefs{'columns'} = join(' ', @columns);
  3105. }
  3106. elsif (defined($cvals{'columns'})) {
  3107. $global_prefs{'columns'} = $cvals{'columns'} || '';
  3108. }
  3109. # DB prefs.
  3110. my $database = $global_prefs{'database'} || '';
  3111. %cvals = $q->cookie("gnatsweb-db-$database");
  3112. %db_prefs = ();
  3113. set_pref('user', \%db_prefs, \%cvals);
  3114. set_pref('password', \%db_prefs, \%cvals);
  3115. # Debug.
  3116. warn "global_prefs = ", Data::Dumper::Dumper(\%global_prefs) if $debug;
  3117. warn "db_prefs = ", Data::Dumper::Dumper(\%db_prefs) if $debug;
  3118. }
  3119. # create_global_cookie -
  3120. # Create cookie from %global_prefs.
  3121. #
  3122. sub create_global_cookie
  3123. {
  3124. my $debug = 0;
  3125. # As of gnatsweb-2.6beta, the name of this cookie changed. This was
  3126. # done so that the old cookie would not be read.
  3127. my $cookie = $q->cookie(-name => 'gnatsweb-global',
  3128. -value => \%global_prefs,
  3129. -path => $global_cookie_path,
  3130. -expires => $global_cookie_expires);
  3131. warn "storing cookie: $cookie\n" if $debug;
  3132. return $cookie;
  3133. }
  3134. #
  3135. # MAIN starts here:
  3136. #
  3137. sub main
  3138. {
  3139. # Load gnatsweb-site.pl if present. Die if there are errors;
  3140. # otherwise the person who wrote gnatsweb-site.pl will never know it.
  3141. do './gnatsweb-site.pl' if (-e './gnatsweb-site.pl');
  3142. die $@ if $@;
  3143. # Make sure nobody tries to swamp our server with a huge file attachment.
  3144. # Has to happen before 'new CGI'.
  3145. $CGI::POST_MAX = $site_post_max if defined($site_post_max);
  3146. # Create the query object. Check to see if there was an error, which
  3147. # happens if the post exceeds POST_MAX.
  3148. $q = new CGI;
  3149. if ($q->cgi_error())
  3150. {
  3151. print $q->header(-status=>$q->cgi_error());
  3152. $q->start_html('Error');
  3153. page_heading('Initialization failed', 'Error');
  3154. print $q->h3('Request not processed: ', $q->cgi_error());
  3155. exit();
  3156. }
  3157. $sn = $q->script_name;
  3158. my $cmd = $q->param('cmd') || ''; # avoid perl -w warning
  3159. ### Cookie-related code must happen before we print the HTML header.
  3160. # What to use as the -path argument in cookies. Using '' (or omitting
  3161. # -path) causes CGI.pm to pass the basename of the script. With that
  3162. # setup, moving the location of gnatsweb.pl causes it to see the old
  3163. # cookies but not be able to delete them.
  3164. $global_cookie_path = '/';
  3165. $global_cookie_expires = '+30d';
  3166. init_prefs();
  3167. #GCC-LOCAL begin: Enforce the "gcc" database.
  3168. $global_prefs{'database'}="gcc";
  3169. #GCC-LOCAL end.
  3170. #GCC-LOCAL begin: No username/pass provided? Default to guest/guest
  3171. if (!$db_prefs{'user'} || !$db_prefs{'password'}) {
  3172. $db_prefs{'user'} = "guest";
  3173. $db_prefs{'password'} = "guest";
  3174. }
  3175. #GCC-LOCAL end.
  3176. # Big old switch to handle commands.
  3177. if($cmd eq 'store query')
  3178. {
  3179. store_query();
  3180. exit();
  3181. }
  3182. elsif($cmd eq 'delete stored query')
  3183. {
  3184. delete_stored_query();
  3185. exit();
  3186. }
  3187. elsif($cmd eq 'submit stored query')
  3188. {
  3189. submit_stored_query();
  3190. exit();
  3191. }
  3192. elsif($cmd eq 'login')
  3193. {
  3194. # User came from login page; store user/password/database in cookies,
  3195. # and proceed to the appropriate page.
  3196. my $global_cookie = create_global_cookie();
  3197. my $db = $global_prefs{'database'};
  3198. my $db_cookie = $q->cookie(-name => "gnatsweb-db-$db",
  3199. -value => \%db_prefs,
  3200. -path => $global_cookie_path,
  3201. -expires => $global_cookie_expires);
  3202. my $expire_old_cookie = $q->cookie(-name => 'gnatsweb',
  3203. -value => 'does not matter',
  3204. -path => $global_cookie_path,
  3205. #-path was not used for gnatsweb 2.5 cookies
  3206. -expires => '-1d');
  3207. my $url = $q->param('return_url') || $q->url();
  3208. # 11/14/99 kenstir: For some reason setting cookies during a redirect
  3209. # didn't work; got a 'page contained no data' error from NS 4.7. This
  3210. # technique did work for me in a small test case but not in gnatsweb.
  3211. # 11/27/99 kenstir: Use zero-delay refresh all the time.
  3212. # 1/15/2000 kenstir: Note that the CGI.pm book says that -cookie may
  3213. # be ignored during a redirect.
  3214. #print $q->redirect(-location => $url,
  3215. # -cookie => [$global_cookie, $db_cookie]);
  3216. # So, this is sort of a lame replacement; a zero-delay refresh.
  3217. print $q->header(-Refresh => "0; URL=$url",
  3218. -cookie => [$global_cookie, $db_cookie,
  3219. $expire_old_cookie]),
  3220. $q->start_html();
  3221. my $debug = 0;
  3222. if ($debug) {
  3223. print "<h3>debugging params</h3><font size=1><pre>";
  3224. my($param,@val);
  3225. foreach $param (sort $q->param()) {
  3226. @val = $q->param($param);
  3227. printf "%-12s %s\n", $param, $q->escapeHTML(join(' ', @val));
  3228. }
  3229. print "</pre></font><hr>\n";
  3230. }
  3231. # Add a link to the new URL. In case the refresh/redirect above did not
  3232. # work, at least the user can select the link manually.
  3233. print $q->h3("Hold on... Redirecting...<br>".
  3234. "In case it does not work automatically, please follow ".
  3235. "<a href=\"$url\">this link</a>."),
  3236. $q->end_html();
  3237. exit();
  3238. }
  3239. elsif($cmd eq 'login again')
  3240. {
  3241. # User is specifically requesting to login again.
  3242. login_page();
  3243. exit();
  3244. }
  3245. elsif(!$global_prefs{'database'}
  3246. || !$db_prefs{'user'} || !$db_prefs{'password'})
  3247. {
  3248. # We don't have username/password/database; give login page then
  3249. # redirect to the url they really want (self_url).
  3250. login_page($q->self_url());
  3251. exit();
  3252. }
  3253. elsif($cmd eq 'submit')
  3254. {
  3255. # User is submitting a new PR. Store cookie because email address may
  3256. # have changed. This facilitates entering bugs the next time.
  3257. initialize();
  3258. submitnewpr();
  3259. exit();
  3260. }
  3261. elsif($cmd eq 'submit query')
  3262. {
  3263. # User is querying. Store cookie because column display list may
  3264. # have changed.
  3265. print $q->header(-cookie => create_global_cookie());
  3266. initialize();
  3267. submitquery();
  3268. exit();
  3269. }
  3270. elsif($cmd =~ /download attachment (\d+)/)
  3271. {
  3272. # User is downloading an attachment. Must initialize but not print header.
  3273. initialize();
  3274. download_attachment($1);
  3275. exit();
  3276. }
  3277. elsif($cmd eq 'create')
  3278. {
  3279. initialize();
  3280. sendpr();
  3281. }
  3282. elsif($cmd eq 'view')
  3283. {
  3284. initialize();
  3285. #GCC LOCAL begin: Always display the audit trail.
  3286. view(1);
  3287. #GCC-LOCAL end.
  3288. }
  3289. elsif($cmd eq 'view audit-trail')
  3290. {
  3291. initialize();
  3292. view(1);
  3293. }
  3294. elsif($cmd eq 'edit')
  3295. {
  3296. initialize();
  3297. edit();
  3298. }
  3299. elsif($cmd eq 'submit edit')
  3300. {
  3301. initialize();
  3302. submitedit();
  3303. }
  3304. elsif($cmd eq 'query')
  3305. {
  3306. print $q->header();
  3307. initialize();
  3308. query_page();
  3309. }
  3310. elsif($cmd eq 'advanced query')
  3311. {
  3312. print $q->header();
  3313. initialize();
  3314. advanced_query_page();
  3315. }
  3316. elsif($cmd eq 'help')
  3317. {
  3318. print $q->header();
  3319. help_page();
  3320. }
  3321. elsif (cb('cmd', $cmd)) {
  3322. ; # cmd was handled by callback
  3323. }
  3324. else {
  3325. initialize();
  3326. main_page();
  3327. }
  3328. client_exit();
  3329. exit();
  3330. }
  3331. # To make this code callable from another source file, set $suppress_main.
  3332. $suppress_main ||= 0;
  3333. main() unless $suppress_main;
  3334. # Emacs stuff -
  3335. #
  3336. # Local Variables:
  3337. # perl-indent-level:2
  3338. # perl-continued-brace-offset:-6
  3339. # perl-continued-statement-offset:6
  3340. # End: