Commit 44a0ae40 authored by Per Cederqvist's avatar Per Cederqvist

Imported Bugzilla 2.4.

parent af90c6f5
......@@ -119,6 +119,54 @@ sub ProcessFormFields {
}
sub ProcessMultipartFormFields {
my ($boundary) = (@_);
$boundary =~ s/^-*//;
my $remaining = $ENV{"CONTENT_LENGTH"};
my $inheader = 1;
my $itemname = "";
# open(DEBUG, ">debug") || die "Can't open debugging thing";
# print DEBUG "Boundary is '$boundary'\n";
while ($remaining > 0 && ($_ = <STDIN>)) {
$remaining -= length($_);
# print DEBUG "< $_";
if ($_ =~ m/^-*$boundary/) {
# print DEBUG "Entered header\n";
$inheader = 1;
$itemname = "";
next;
}
if ($inheader) {
if (m/^\s*$/) {
$inheader = 0;
# print DEBUG "left header\n";
$::FORM{$itemname} = "";
}
if (m/^Content-Disposition:\s*form-data\s*;\s*name\s*=\s*"([^\"]+)"/i) {
$itemname = $1;
# print DEBUG "Found itemname $itemname\n";
if (m/;\s*filename\s*=\s*"([^\"]+)"/i) {
$::FILENAME{$itemname} = $1;
}
}
next;
}
$::FORM{$itemname} .= $_;
}
delete $::FORM{""};
# Get rid of trailing newlines.
foreach my $i (keys %::FORM) {
chomp($::FORM{$i});
$::FORM{$i} =~ s/\r$//;
}
}
sub FormData {
my ($field) = (@_);
return $::FORM{$field};
......@@ -138,19 +186,11 @@ sub value_quote {
$var =~ s/</\&lt;/g;
$var =~ s/>/\&gt;/g;
$var =~ s/"/\&quot;/g;
$var =~ s/\n/\&#010;/g;
$var =~ s/\r/\&#013;/g;
return $var;
}
sub value_unquote {
my ($var) = (@_);
$var =~ s/\&quot/\"/g;
$var =~ s/\&lt/</g;
$var =~ s/\&gt/>/g;
$var =~ s/\&amp/\&/g;
return $var;
}
sub navigation_header {
if (defined $::COOKIE{"BUGLIST"} && $::COOKIE{"BUGLIST"} ne "") {
my @bugs = split(/:/, $::COOKIE{"BUGLIST"});
......@@ -180,17 +220,20 @@ sub make_options {
my $last = "";
my $popup = "";
my $found = 0;
foreach my $item (@$src) {
if ($item eq "-blank-" || $item ne $last) {
if ($item eq "-blank-") {
$item = "";
}
$last = $item;
if ($isregexp ? $item =~ $default : $default eq $item) {
$popup .= "<OPTION SELECTED VALUE=\"$item\">$item";
$found = 1;
} else {
$popup .= "<OPTION VALUE=\"$item\">$item";
if ($src) {
foreach my $item (@$src) {
if ($item eq "-blank-" || $item ne $last) {
if ($item eq "-blank-") {
$item = "";
}
$last = $item;
if ($isregexp ? $item =~ $default : $default eq $item) {
$popup .= "<OPTION SELECTED VALUE=\"$item\">$item";
$found = 1;
} else {
$popup .= "<OPTION VALUE=\"$item\">$item";
}
}
}
}
......@@ -231,6 +274,57 @@ sub PasswordForLogin {
return $result;
}
sub quietly_check_login() {
$::usergroupset = '0';
my $loginok = 0;
if (defined $::COOKIE{"Bugzilla_login"} &&
defined $::COOKIE{"Bugzilla_logincookie"}) {
ConnectToDatabase();
SendSQL("select profiles.groupset, profiles.login_name = " .
SqlQuote($::COOKIE{"Bugzilla_login"}) .
" and profiles.cryptpassword = logincookies.cryptpassword " .
"and logincookies.hostname = " .
SqlQuote($ENV{"REMOTE_HOST"}) .
" from profiles,logincookies where logincookies.cookie = " .
SqlQuote($::COOKIE{"Bugzilla_logincookie"}) .
" and profiles.userid = logincookies.userid");
my @row;
if (@row = FetchSQLData()) {
$loginok = $row[1];
if ($loginok) {
$::usergroupset = $row[0];
}
}
}
if (!$loginok) {
delete $::COOKIE{"Bugzilla_login"};
}
return $loginok;
}
sub CheckEmailSyntax {
my ($addr) = (@_);
if ($addr !~ /^[^@, ]*@[^@, ]*\.[^@, ]*$/) {
print "Content-type: text/html\n\n";
print "<H1>Invalid e-mail address entered.</H1>\n";
print "The e-mail address you entered\n";
print "(<b>$addr</b>) didn't match our minimal\n";
print "syntax checking for a legal email address. A legal\n";
print "address must contain exactly one '\@', and at least one\n";
print "'.' after the \@, and may not contain any commas or.\n";
print "spaces.\n";
print "<p>Please click <b>back</b> and try again.\n";
exit;
}
}
sub confirm_login {
my ($nexturl) = (@_);
......@@ -243,19 +337,8 @@ sub confirm_login {
my $enteredlogin = $::FORM{"Bugzilla_login"};
my $enteredpwd = $::FORM{"Bugzilla_password"};
if ($enteredlogin !~ /^[^@, ]*@[^@, ]*\.[^@, ]*$/) {
print "Content-type: text/html\n\n";
CheckEmailSyntax($enteredlogin);
print "<H1>Invalid e-mail address entered.</H1>\n";
print "The e-mail address you entered\n";
print "(<b>$enteredlogin</b>) didn't match our minimal\n";
print "syntax checking for a legal email address. A legal\n";
print "address must contain exactly one '\@', and at least one\n";
print "'.' after the \@, and may not contain any commas or.\n";
print "spaces.\n";
print "<p>Please click <b>back</b> and try again.\n";
exit;
}
my $realcryptpwd = PasswordForLogin($::FORM{"Bugzilla_login"});
if (defined $::FORM{"PleaseMailAPassword"}) {
......@@ -324,25 +407,9 @@ To use the wonders of bugzilla, you can use the following:
}
my $loginok = 0;
if (defined $::COOKIE{"Bugzilla_login"} &&
defined $::COOKIE{"Bugzilla_logincookie"}) {
SendSQL("select profiles.login_name = " .
SqlQuote($::COOKIE{"Bugzilla_login"}) .
" and profiles.cryptpassword = logincookies.cryptpassword " .
"and logincookies.hostname = " .
SqlQuote($ENV{"REMOTE_HOST"}) .
" from profiles,logincookies where logincookies.cookie = " .
$::COOKIE{"Bugzilla_logincookie"} .
" and profiles.userid = logincookies.userid");
$loginok = FetchOneColumn();
if (!defined $loginok) {
$loginok = 0;
}
}
my $loginok = quietly_check_login();
if ($loginok ne "1") {
if ($loginok != 1) {
print "Content-type: text/html\n\n";
print "<H1>Please log in.</H1>\n";
print "I need a legitimate e-mail address and password to continue.\n";
......@@ -407,7 +474,8 @@ sub PutHeader {
$h2 = "";
}
print "<HTML><HEAD><TITLE>$title</TITLE></HEAD>\n";
print "<HTML><HEAD>\n<TITLE>$title</TITLE>\n";
print Param("headerhtml") . "\n</HEAD>\n";
print "<BODY BGCOLOR=\"#FFFFFF\" TEXT=\"#000000\"\n";
print "LINK=\"#0000EE\" VLINK=\"#551A8B\" ALINK=\"#FF0000\">\n";
......@@ -454,10 +522,18 @@ if (defined $ENV{"REQUEST_METHOD"}) {
} else {
$::buffer = "";
}
ProcessFormFields $::buffer;
} else {
read STDIN, $::buffer, $ENV{"CONTENT_LENGTH"} || die "Couldn't get form data";
if ($ENV{"CONTENT_TYPE"} =~
m@multipart/form-data; boundary=\s*([^; ]+)@) {
ProcessMultipartFormFields($1);
$::buffer = "";
} else {
read STDIN, $::buffer, $ENV{"CONTENT_LENGTH"} ||
die "Couldn't get form data";
ProcessFormFields $::buffer;
}
}
ProcessFormFields $::buffer;
}
......
......@@ -10,6 +10,90 @@ query the CVS tree. For example,
will tell you what has been changed in the last week.
4/22/99 There was a bug where the long descriptions of bugs had a variety of
newline characters at the end, depending on the operating system of the browser
that submitted the text. This bug has been fixed, so that no further changes
like that will happen. But to fix problems that have already crept into your
database, you can run the following perl script (which is slow and ugly, but
does work:)
#!/usr/bonsaitools/bin/perl -w
use diagnostics;
use strict;
require "globals.pl";
$|=1;
ConnectToDatabase();
SendSQL("select bug_id from bugs order by bug_id");
my @list;
while (MoreSQLData()) {
push(@list, FetchOneColumn());
}
foreach my $id (@list) {
if ($id % 50 == 0) {
print "\n$id ";
}
SendSQL("select long_desc from bugs where bug_id = $id");
my $comment = FetchOneColumn();
my $orig = $comment;
$comment =~ s/\r\n/\n/g; # Get rid of windows-style line endings.
$comment =~ s/\r/\n/g; # Get rid of mac-style line endings.
if ($comment ne $orig) {
SendSQL("update bugs set long_desc = " . SqlQuote($comment) .
" where bug_id = $id");
print ".";
} else {
print "-";
}
}
4/8/99 Added ability to store patches with bugs. This requires a new table
to store the data, so you will need to run the "makeattachmenttable.sh" script.
3/25/99 Unfortunately, the HTML::FromText CPAN module had too many bugs, and
so I had to roll my own. We no longer use the HTML::FromText CPAN module.
3/24/99 (This entry has been removed. It used to say that we required the
HTML::FromText CPAN module, but that's no longer true.)
3/22/99 Added the ability to query by fields which have changed within a date
range. To make this perform a bit better, we need a new index:
alter table bugs_activity add index (field);
3/10/99 Added 'groups' stuff, where we have different group bits that we can
put on a person or on a bug. Some of the group bits control access to bugzilla
features. And a person can't access a bug unless he has every group bit set
that is also set on the bug. See the comments in makegroupstable.sh for a bit
more info.
The 'maintainer' param is now used only as an email address for people to send
complaints to. The groups table is what is now used to determine permissions.
You will need to run the new script "makegroupstable.sh". And then you need to
feed the following lines to MySQL (replace XXX with the login name of the
maintainer, the person you wish to be all-powerful).
alter table bugs add column groupset bigint not null;
alter table profiles add column groupset bigint not null;
update profiles set groupset=0x7fffffffffffffff where login_name = XXX;
3/8/99 Added params to control how priorities are set in a new bug. You can
now choose whether to let submitters of new bugs choose a priority, or whether
they should just accept the default priority (which is now no longer hardcoded
to "P2", but is instead a param.) The default value of the params will cause
the same behavior as before.
3/3/99 Added a "disallownew" field to the products table. If non-zero, then
don't let people file new bugs against this product. (This is for when a
product is retired, but you want to keep the bug reports around for posterity.)
Feed this to MySQL:
alter table products add column disallownew tinyint not null;
2/8/99 Added FreeBSD to the list of OS's. Feed this to MySQL:
alter table bugs change column op_sys op_sys enum("All", "Windows 3.1", "Windows 95", "Windows 98", "Windows NT", "Mac System 7", "Mac System 7.5", "Mac System 7.6.1", "Mac System 8.0", "Mac System 8.5", "AIX", "BSDI", "HP-UX", "IRIX", "Linux", "FreeBSD", "OSF/1", "Solaris", "SunOS", "OS/2", "other") not null;
......
/1x1.gif/1.1/Wed Feb 10 22:11:47 1999/-kb/
/CGI.pl/1.6/Wed Feb 10 22:11:47 1999//
/CHANGES/1.16/Wed Feb 10 22:11:47 1999//
/README/1.12/Wed Feb 10 22:11:47 1999//
/ant.jpg/1.2/Wed Feb 10 22:11:47 1999/-kb/
/backdoor.cgi/1.6/Wed Feb 10 22:11:48 1999//
/bug_form.pl/1.8/Wed Feb 10 22:11:48 1999//
/bug_status.html/1.3/Wed Feb 10 22:11:48 1999//
/buglist.cgi/1.23/Wed Feb 10 22:11:48 1999//
/changepassword.cgi/1.6/Wed Feb 10 22:11:48 1999//
/colchange.cgi/1.7/Wed Feb 10 22:11:48 1999//
/collectstats.pl/1.3/Wed Feb 10 22:11:48 1999//
/defparams.pl/1.5/Wed Feb 10 22:11:48 1999//
/describecomponents.cgi/1.1/Wed Feb 10 22:11:48 1999//
/doeditowners.cgi/1.3/Wed Feb 10 22:11:48 1999//
/doeditparams.cgi/1.5/Wed Feb 10 22:11:48 1999//
/editowners.cgi/1.3/Wed Feb 10 22:11:48 1999//
/editparams.cgi/1.7/Wed Feb 10 22:11:48 1999//
/enter_bug.cgi/1.14/Wed Feb 10 22:11:48 1999//
/help.html/1.1/Wed Feb 10 22:11:48 1999//
/helpemailquery.html/1.1/Wed Feb 10 22:11:48 1999//
/how_to_mail.html/1.1/Wed Feb 10 22:11:49 1999//
/index.html/1.3/Wed Feb 10 22:11:49 1999//
/long_list.cgi/1.5/Wed Feb 10 22:11:49 1999//
/makeactivitytable.sh/1.1/Wed Feb 10 22:11:49 1999//
/makebugtable.sh/1.8/Wed Feb 10 22:11:49 1999//
/makecctable.sh/1.2/Wed Feb 10 22:11:49 1999//
/makecomponenttable.sh/1.38/Wed Feb 10 22:11:49 1999//
/makelogincookiestable.sh/1.1/Wed Feb 10 22:11:49 1999//
/makeproducttable.sh/1.8/Wed Feb 10 22:11:49 1999//
/makeprofilestable.sh/1.2/Wed Feb 10 22:11:49 1999//
/makeversiontable.sh/1.10/Wed Feb 10 22:11:49 1999//
/new_comment.cgi/1.2/Wed Feb 10 22:11:49 1999//
/newquip.html/1.2/Wed Feb 10 22:11:49 1999//
/notargetmilestone.html/1.1/Wed Feb 10 22:11:49 1999//
/post_bug.cgi/1.5/Wed Feb 10 22:11:49 1999//
/process_bug.cgi/1.13/Wed Feb 10 22:11:49 1999//
/processmail/1.11/Wed Feb 10 22:11:49 1999//
/query.cgi/1.23/Wed Feb 10 22:11:50 1999//
/relogin.cgi/1.5/Wed Feb 10 22:11:50 1999//
/reports.cgi/1.10/Wed Feb 10 22:11:50 1999//
/sanitycheck.cgi/1.4/Wed Feb 10 22:11:50 1999//
/show_activity.cgi/1.2/Wed Feb 10 22:11:50 1999//
/show_bug.cgi/1.5/Wed Feb 10 22:11:50 1999//
/whineatnews.pl/1.1/Wed Feb 10 22:11:50 1999//
/globals.pl/1.14/Wed Feb 10 22:11:51 1999//
/1x1.gif/1.1/Fri Apr 30 19:42:19 1999/-kb/
/CGI.pl/1.12/Fri Apr 30 19:42:19 1999//
/CHANGES/1.24/Fri Apr 30 19:42:20 1999//
/README/1.20/Fri Apr 30 19:42:20 1999//
/addcomponent.cgi/1.1/Fri Apr 30 19:42:20 1999//
/ant.jpg/1.2/Fri Apr 30 19:42:20 1999/-kb/
/backdoor.cgi/1.7/Fri Apr 30 19:42:20 1999//
/bug_form.pl/1.15/Fri Apr 30 19:42:20 1999//
/bug_status.html/1.4/Fri Apr 30 19:42:20 1999//
/buglist.cgi/1.28/Fri Apr 30 19:42:20 1999//
/changepassword.cgi/1.6/Fri Apr 30 19:42:20 1999//
/colchange.cgi/1.7/Fri Apr 30 19:42:20 1999//
/collectstats.pl/1.5/Fri Apr 30 19:42:20 1999//
/createaccount.cgi/1.1/Fri Apr 30 19:42:20 1999//
/createattachment.cgi/1.2/Fri Apr 30 19:42:20 1999//
/defparams.pl/1.8/Fri Apr 30 19:42:20 1999//
/describecomponents.cgi/1.1/Fri Apr 30 19:42:20 1999//
/doaddcomponent.cgi/1.1/Fri Apr 30 19:42:21 1999//
/doeditcomponents.cgi/1.1/Fri Apr 30 19:42:21 1999//
/doeditowners.cgi/1.4/Fri Apr 30 19:42:21 1999//
/doeditparams.cgi/1.6/Fri Apr 30 19:42:21 1999//
/editcomponents.cgi/1.2/Fri Apr 30 19:42:21 1999//
/editowners.cgi/1.4/Fri Apr 30 19:42:21 1999//
/editparams.cgi/1.8/Fri Apr 30 19:42:21 1999//
/enter_bug.cgi/1.17/Fri Apr 30 19:42:21 1999//
/help.html/1.1/Fri Apr 30 19:42:21 1999//
/helpemailquery.html/1.1/Fri Apr 30 19:42:21 1999//
/how_to_mail.html/1.1/Fri Apr 30 19:42:21 1999//
/index.html/1.4/Fri Apr 30 19:42:21 1999//
/long_list.cgi/1.6/Fri Apr 30 19:42:21 1999//
/makeactivitytable.sh/1.2/Fri Apr 30 19:42:22 1999//
/makeattachmenttable.sh/1.1/Fri Apr 30 19:42:22 1999//
/makebugtable.sh/1.9/Fri Apr 30 19:42:22 1999//
/makecctable.sh/1.2/Fri Apr 30 19:42:22 1999//
/makecomponenttable.sh/1.59/Fri Apr 30 19:42:22 1999//
/makegroupstable.sh/1.1/Fri Apr 30 19:42:22 1999//
/makelogincookiestable.sh/1.1/Fri Apr 30 19:42:22 1999//
/makeproducttable.sh/1.12/Fri Apr 30 19:42:22 1999//
/makeprofilestable.sh/1.3/Fri Apr 30 19:42:22 1999//
/makeversiontable.sh/1.13/Fri Apr 30 19:42:22 1999//
/new_comment.cgi/1.2/Fri Apr 30 19:42:23 1999//
/newquip.html/1.2/Fri Apr 30 19:42:23 1999//
/notargetmilestone.html/1.1/Fri Apr 30 19:42:23 1999//
/post_bug.cgi/1.6/Fri Apr 30 19:42:23 1999//
/process_bug.cgi/1.14/Fri Apr 30 19:42:23 1999//
/processmail/1.13/Fri Apr 30 19:42:23 1999//
/query.cgi/1.33/Fri Apr 30 19:42:23 1999//
/relogin.cgi/1.5/Fri Apr 30 19:42:23 1999//
/reports.cgi/1.10/Fri Apr 30 19:42:23 1999//
/sanitycheck.cgi/1.8/Fri Apr 30 19:42:23 1999//
/show_activity.cgi/1.2/Fri Apr 30 19:42:23 1999//
/show_bug.cgi/1.5/Fri Apr 30 19:42:23 1999//
/showattachment.cgi/1.1/Fri Apr 30 19:42:23 1999//
/whineatnews.pl/1.1/Fri Apr 30 19:42:23 1999//
/globals.pl/1.20/Fri Apr 30 19:42:25 1999//
D
:pserver:terry%netscape.com@cvs.mozilla.org:/cvsroot
:pserver:terry%mozilla.org@cvs.mozilla.org:/cvsroot
This diff is collapsed.
#!/usr/bonsaitools/bin/perl -w
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# The contents of this file are subject to the Mozilla Public License
# Version 1.0 (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS IS"
# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
# License for the specific language governing rights and limitations
# under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Netscape Communications
# Corporation. Portions created by Netscape are Copyright (C) 1998
# Netscape Communications Corporation. All Rights Reserved.
#
# Contributor(s): Sam Ziegler <sam@ziegler.org>
# Terry Weissman <terry@mozilla.org>
# Mark Hamby <mhamby@logicon.com>
# Code derived from editcomponents.cgi, reports.cgi
use diagnostics;
use strict;
require "CGI.pl";
# Shut up misguided -w warnings about "used only once":
use vars @::legal_product;
confirm_login();
print "Content-type: text/html\n\n";
if (!UserInGroup("editcomponents")) {
print "<H1>Sorry, you aren't a member of the 'editcomponents' group.</H1>\n";
print "And so, you aren't allowed to add a new component.\n";
exit;
}
PutHeader("Add Component");
print "This page lets you add a component to bugzilla.\n";
unlink "data/versioncache";
GetVersionTable();
my $prodcode = "P0";
my $product_popup = make_options (\@::legal_product, $::legal_product[0]);
print "
<form method=post action=doaddcomponent.cgi>
<TABLE>
<TR>
<th align=right>Component:</th>
<TD><input size=60 name=\"component\" value=\"\"></TD>
</TR>
<TR>
<TH align=right>Program:</TH>
<TD><SELECT NAME=\"product\">
$product_popup
</SELECT></TD>
</TR>
<TR>
<TH align=right>Description:</TH>
<TD><input size=60 name=\"description\" value=\"\"></TD>
</TR>
<TR>
<TH align=right>Initial owner:</TH>
<TD><input size=60 name=\"initialowner\" value=\"\"></TD>
</TR>
";
if (Param('useqacontact')) {
print "
<TR>
<TH align=right>Initial QA contact:</TH>
<TD><input size=60 name=\"initialqacontact\" value=\"\"></TD>
</TR>
";
}
print "
</table>
<hr>
";
print "<input type=submit value=\"Submit changes\">\n";
print "</form>\n";
print "<p><a href=query.cgi>Skip all this, and go back to the query page</a>\n";
......@@ -81,7 +81,7 @@ can teach me.";
# Do remapping of things from BugSplat world to Bugzilla.
if ($prod eq "Communicator") {
$prod = "Mozilla";
$prod = "Browser";
$version = "other";
}
......@@ -133,7 +133,7 @@ if ($::FORM{'qa_contact'} ne "") {
my @list = ('reporter', 'assigned_to', 'product', 'version', 'rep_platform',
'op_sys', 'bug_status', 'bug_severity', 'priority', 'component',
'short_desc', 'long_desc', 'creation_ts', 'delta_ts',
'bug_file_loc', 'qa_contact');
'bug_file_loc', 'qa_contact', 'groupset');
my @vallist;
foreach my $i (@list) {
......
......@@ -21,6 +21,91 @@
use diagnostics;
use strict;
my %knownattachments;
# This routine quoteUrls contains inspirations from the HTML::FromText CPAN
# module by Gareth Rees <garethr@cre.canon.co.uk>. It has been heavily hacked,
# all that is really recognizable from the original is bits of the regular
# expressions.
sub quoteUrls {
my $text = shift; # Take a copy; don't modify in-place.
return $text unless $text;
my $base = Param('urlbase');
my $protocol = join '|',
qw(afs cid ftp gopher http https mid news nntp prospero telnet wais);
my %options = ( metachars => 1, @_ );
my $count = 0;
# Now, quote any "#" characters so they won't confuse stuff later
$text =~ s/#/%#/g;
# Next, find anything that looks like a URL or an email address and
# pull them out the the text, replacing them with a "##<digits>##
# marker, and writing them into an array. All this confusion is
# necessary so that we don't match on something we've already replaced,
# which can happen if you do multiple s///g operations.
my @things;
while ($text =~ s%((mailto:)?([\w\.\-\+\=]+\@\w+(?:\.\w+)+)\b|
(\b((?:$protocol):\S+[\w/])))%"##$count##"%exo) {
my $item = $&;
$item = value_quote($item);
if ($item !~ m/^$protocol:/o && $item !~ /^mailto:/) {
# We must have grabbed this one because it looks like an email
# address.
$item = qq{<A HREF="mailto:$item">$item</A>};
} else {
$item = qq{<A HREF="$item">$item</A>};
}
$things[$count++] = $item;
}
while ($text =~ s/\bbug(\s|%\#)*(\d+)/"##$count##"/ei) {
my $item = $&;
my $num = $2;
$item = value_quote($item); # Not really necessary, since we know
# there's no special chars in it.
$item = qq{<A HREF="${base}show_bug.cgi?id=$num">$item</A>};
$things[$count++] = $item;
}
while ($text =~ s/\*\*\* This bug has been marked as a duplicate of (\d+) \*\*\*/"##$count##"/ei) {
my $item = $&;
my $num = $1;
$item =~ s@\d+@<A HREF="${base}show_bug.cgi?id=$num">$num</A>@;
$things[$count++] = $item;
}
while ($text =~ s/Created an attachment \(id=(\d+)\)/"##$count##"/e) {
my $item = $&;
my $num = $1;
if (exists $knownattachments{$num}) {
$item = qq{<A HREF="showattachment.cgi?attach_id=$num">$item</A>};