blob: b021233011eb519fb6b396a2a330819108284233 [file] [log] [blame]
timothy@apple.comf42518d2008-02-06 20:19:16 +00001# -*- Mode: perl; indent-tabs-mode: nil -*-
2#
3# The contents of this file are subject to the Mozilla Public
4# License Version 1.1 (the "License"); you may not use this file
5# except in compliance with the License. You may obtain a copy of
6# the License at http://www.mozilla.org/MPL/
7#
8# Software distributed under the License is distributed on an "AS
9# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
10# implied. See the License for the specific language governing
11# rights and limitations under the License.
12#
13# The Original Code is the Bugzilla Bug Tracking System.
14#
15# The Initial Developer of the Original Code is Netscape Communications
16# Corporation. Portions created by Netscape are
17# Copyright (C) 1998 Netscape Communications Corporation. All
18# Rights Reserved.
19#
20# Contributor(s): Terry Weissman <terry@mozilla.org>
21# Dan Mosedale <dmose@mozilla.org>
22# Jacob Steenhagen <jake@bugzilla.org>
23# Bradley Baetz <bbaetz@student.usyd.edu.au>
24# Christopher Aillon <christopher@aillon.com>
25# Joel Peshkin <bugreport@peshkin.net>
26# Dave Lawrence <dkl@redhat.com>
27# Max Kanat-Alexander <mkanat@bugzilla.org>
28
29# Contains some global variables and routines used throughout bugzilla.
30
31use strict;
32
33use Bugzilla::DB qw(:DEFAULT :deprecated);
34use Bugzilla::Constants;
35use Bugzilla::Util;
36# Bring ChmodDataFile in until this is all moved to the module
37use Bugzilla::Config qw(:DEFAULT ChmodDataFile $localconfig $datadir);
38use Bugzilla::BugMail;
39use Bugzilla::User;
40
41# Shut up misguided -w warnings about "used only once". For some reason,
42# "use vars" chokes on me when I try it here.
43
44sub globals_pl_sillyness {
45 my $zz;
46 $zz = @main::default_column_list;
47 $zz = @main::enterable_products;
48 $zz = %main::keywordsbyname;
49 $zz = @main::legal_bug_status;
50 $zz = @main::legal_components;
51 $zz = @main::legal_keywords;
52 $zz = @main::legal_opsys;
53 $zz = @main::legal_platform;
54 $zz = @main::legal_priority;
55 $zz = @main::legal_product;
56 $zz = @main::legal_severity;
57 $zz = @main::legal_target_milestone;
58 $zz = @main::legal_versions;
59 $zz = @main::milestoneurl;
60 $zz = %main::proddesc;
61 $zz = %main::classdesc;
62 $zz = @main::prodmaxvotes;
63 $zz = $main::template;
64 $zz = $main::userid;
65 $zz = $main::vars;
66}
67
68#
69# Here are the --LOCAL-- variables defined in 'localconfig' that we'll use
70# here
71#
72
73# XXX - Move this to Bugzilla::Config once code which uses these has moved out
74# of globals.pl
75do $localconfig;
76
77use DBI;
78
79use Date::Format; # For time2str().
80use Date::Parse; # For str2time().
81
82# Use standard Perl libraries for cross-platform file/directory manipulation.
83use File::Spec;
84
85# Some environment variables are not taint safe
86delete @::ENV{'PATH', 'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
87
88# Cwd.pm in perl 5.6.1 gives a warning if $::ENV{'PATH'} isn't defined
89# Set this to '' so that we don't get warnings cluttering the logs on every
90# system call
91$::ENV{'PATH'} = '';
92
93# Ignore SIGTERM and SIGPIPE - this prevents DB corruption. If the user closes
94# their browser window while a script is running, the webserver sends these
95# signals, and we don't want to die half way through a write.
96$::SIG{TERM} = 'IGNORE';
97$::SIG{PIPE} = 'IGNORE';
98
99# The following subroutine is for debugging purposes only.
100# Uncommenting this sub and the $::SIG{__DIE__} trap underneath it will
101# cause any fatal errors to result in a call stack trace to help track
102# down weird errors.
103#sub die_with_dignity {
104# use Carp; # for confess()
105# my ($err_msg) = @_;
106# print $err_msg;
107# confess($err_msg);
108#}
109#$::SIG{__DIE__} = \&die_with_dignity;
110
111sub GetFieldID {
112 my ($f) = (@_);
113 SendSQL("SELECT fieldid FROM fielddefs WHERE name = " . SqlQuote($f));
114 my $fieldid = FetchOneColumn();
115 die "Unknown field id: $f" if !$fieldid;
116 return $fieldid;
117}
118
119# XXXX - this needs to go away
120sub GenerateVersionTable {
121 my $dbh = Bugzilla->dbh;
122
123 SendSQL("SELECT versions.value, products.name " .
124 "FROM versions, products " .
125 "WHERE products.id = versions.product_id " .
126 "ORDER BY versions.value");
127 my @line;
128 my %varray;
129 my %carray;
130 while (@line = FetchSQLData()) {
131 my ($v,$p1) = (@line);
132 if (!defined $::versions{$p1}) {
133 $::versions{$p1} = [];
134 }
135 push @{$::versions{$p1}}, $v;
136 $varray{$v} = 1;
137 }
138 SendSQL("SELECT components.name, products.name " .
139 "FROM components, products " .
140 "WHERE products.id = components.product_id " .
141 "ORDER BY components.name");
142 while (@line = FetchSQLData()) {
143 my ($c,$p) = (@line);
144 if (!defined $::components{$p}) {
145 $::components{$p} = [];
146 }
147 my $ref = $::components{$p};
148 push @$ref, $c;
149 $carray{$c} = 1;
150 }
151
152 SendSQL("SELECT products.name, classifications.name " .
153 "FROM products, classifications " .
154 "WHERE classifications.id = products.classification_id " .
155 "ORDER BY classifications.name");
156 while (@line = FetchSQLData()) {
157 my ($p,$c) = (@line);
158 if (!defined $::classifications{$c}) {
159 $::classifications{$c} = [];
160 }
161 my $ref = $::classifications{$c};
162 push @$ref, $p;
163 }
164
165 my $dotargetmilestone = 1; # This used to check the param, but there's
166 # enough code that wants to pretend we're using
167 # target milestones, even if they don't get
168 # shown to the user. So we cache all the data
169 # about them anyway.
170
171 my $mpart = $dotargetmilestone ? ", milestoneurl" : "";
172
173 SendSQL("SELECT name, description FROM classifications ORDER BY name");
174 while (@line = FetchSQLData()) {
175 my ($n, $d) = (@line);
176 $::classdesc{$n} = $d;
177 }
178
179 SendSQL("SELECT name, description, votesperuser, disallownew$mpart " .
180 "FROM products ORDER BY name");
181 while (@line = FetchSQLData()) {
182 my ($p, $d, $votesperuser, $dis, $u) = (@line);
183 $::proddesc{$p} = $d;
184 if (!$dis && scalar($::components{$p})) {
185 push @::enterable_products, $p;
186 }
187 if ($dotargetmilestone) {
188 $::milestoneurl{$p} = $u;
189 }
190 $::prodmaxvotes{$p} = $votesperuser;
191 }
192
193 @::log_columns = $dbh->bz_table_columns('bugs');
194
195 foreach my $i ("bug_id", "creation_ts", "delta_ts", "lastdiffed") {
196 my $w = lsearch(\@::log_columns, $i);
197 if ($w >= 0) {
198 splice(@::log_columns, $w, 1);
199 }
200 }
201 @::log_columns = (sort(@::log_columns));
202
203 @::legal_priority = get_legal_field_values("priority");
204 @::legal_severity = get_legal_field_values("bug_severity");
205 @::legal_platform = get_legal_field_values("rep_platform");
206 @::legal_opsys = get_legal_field_values("op_sys");
207 @::legal_bug_status = get_legal_field_values("bug_status");
208 @::legal_resolution = get_legal_field_values("resolution");
209
210 # 'settable_resolution' is the list of resolutions that may be set
211 # directly by hand in the bug form. Start with the list of legal
212 # resolutions and remove 'MOVED' and 'DUPLICATE' because setting
213 # bugs to those resolutions requires a special process.
214 #
215 @::settable_resolution = @::legal_resolution;
216 my $w = lsearch(\@::settable_resolution, "DUPLICATE");
217 if ($w >= 0) {
218 splice(@::settable_resolution, $w, 1);
219 }
220 my $z = lsearch(\@::settable_resolution, "MOVED");
221 if ($z >= 0) {
222 splice(@::settable_resolution, $z, 1);
223 }
224
225 my @list = sort { uc($a) cmp uc($b)} keys(%::versions);
226 @::legal_product = @list;
227
228 require File::Temp;
229 my ($fh, $tmpname) = File::Temp::tempfile("versioncache.XXXXX",
230 DIR => "$datadir");
231
232 print $fh "#\n";
233 print $fh "# DO NOT EDIT!\n";
234 print $fh "# This file is automatically generated at least once every\n";
235 print $fh "# hour by the GenerateVersionTable() sub in globals.pl.\n";
236 print $fh "# Any changes you make will be overwritten.\n";
237 print $fh "#\n";
238
239 require Data::Dumper;
240 print $fh (Data::Dumper->Dump([\@::log_columns, \%::versions],
241 ['*::log_columns', '*::versions']));
242
243 foreach my $i (@list) {
244 if (!defined $::components{$i}) {
245 $::components{$i} = [];
246 }
247 }
248 @::legal_versions = sort {uc($a) cmp uc($b)} keys(%varray);
249 print $fh (Data::Dumper->Dump([\@::legal_versions, \%::components],
250 ['*::legal_versions', '*::components']));
251 @::legal_components = sort {uc($a) cmp uc($b)} keys(%carray);
252
253 print $fh (Data::Dumper->Dump([\@::legal_components, \@::legal_product,
254 \@::legal_priority, \@::legal_severity,
255 \@::legal_platform, \@::legal_opsys,
256 \@::legal_bug_status, \@::legal_resolution],
257 ['*::legal_components', '*::legal_product',
258 '*::legal_priority', '*::legal_severity',
259 '*::legal_platform', '*::legal_opsys',
260 '*::legal_bug_status', '*::legal_resolution']));
261
262 print $fh (Data::Dumper->Dump([\@::settable_resolution, \%::proddesc,
263 \%::classifications, \%::classdesc,
264 \@::enterable_products, \%::prodmaxvotes],
265 ['*::settable_resolution', '*::proddesc',
266 '*::classifications', '*::classdesc',
267 '*::enterable_products', '*::prodmaxvotes']));
268
269 if ($dotargetmilestone) {
270 # reading target milestones in from the database - matthew@zeroknowledge.com
271 SendSQL("SELECT milestones.value, products.name " .
272 "FROM milestones, products " .
273 "WHERE products.id = milestones.product_id " .
274 "ORDER BY milestones.sortkey, milestones.value");
275 my @line;
276 my %tmarray;
277 @::legal_target_milestone = ();
278 while(@line = FetchSQLData()) {
279 my ($tm, $pr) = (@line);
280 if (!defined $::target_milestone{$pr}) {
281 $::target_milestone{$pr} = [];
282 }
283 push @{$::target_milestone{$pr}}, $tm;
284 if (!exists $tmarray{$tm}) {
285 $tmarray{$tm} = 1;
286 push(@::legal_target_milestone, $tm);
287 }
288 }
289
290 print $fh (Data::Dumper->Dump([\%::target_milestone,
291 \@::legal_target_milestone,
292 \%::milestoneurl],
293 ['*::target_milestone',
294 '*::legal_target_milestone',
295 '*::milestoneurl']));
296 }
297
298 SendSQL("SELECT id, name FROM keyworddefs ORDER BY name");
299 while (MoreSQLData()) {
300 my ($id, $name) = FetchSQLData();
301 push(@::legal_keywords, $name);
302 $name = lc($name);
303 $::keywordsbyname{$name} = $id;
304 }
305
306 print $fh (Data::Dumper->Dump([\@::legal_keywords, \%::keywordsbyname],
307 ['*::legal_keywords', '*::keywordsbyname']));
308
309 print $fh "1;\n";
310 close $fh;
311
312 rename ($tmpname, "$datadir/versioncache")
313 || die "Can't rename $tmpname to versioncache";
314 ChmodDataFile("$datadir/versioncache", 0666);
315}
316
317
318sub GetKeywordIdFromName {
319 my ($name) = (@_);
320 $name = lc($name);
321 return $::keywordsbyname{$name};
322}
323
324
325$::VersionTableLoaded = 0;
326sub GetVersionTable {
327 return if $::VersionTableLoaded;
328 my $mtime = file_mod_time("$datadir/versioncache");
329 if (!defined $mtime || $mtime eq "" || !-r "$datadir/versioncache") {
330 $mtime = 0;
331 }
332 if (time() - $mtime > 3600) {
333 use Bugzilla::Token;
334 Bugzilla::Token::CleanTokenTable() if Bugzilla->dbwritesallowed;
335 GenerateVersionTable();
336 }
337 require "$datadir/versioncache";
338 if (!defined %::versions) {
339 GenerateVersionTable();
340 do "$datadir/versioncache";
341
342 if (!defined %::versions) {
343 die "Can't generate file $datadir/versioncache";
344 }
345 }
346 $::VersionTableLoaded = 1;
347}
348
349sub GenerateRandomPassword {
350 my $size = (shift or 10); # default to 10 chars if nothing specified
351 return join("", map{ ('0'..'9','a'..'z','A'..'Z')[rand 62] } (1..$size));
352}
353
354#
355# This function checks if there are any entry groups defined.
356# If called with no arguments, it identifies
357# entry groups for all products. If called with a product
358# id argument, it checks for entry groups associated with
359# one particular product.
360sub AnyEntryGroups {
361 my $product_id = shift;
362 $product_id = 0 unless ($product_id);
363 return $::CachedAnyEntryGroups{$product_id}
364 if defined($::CachedAnyEntryGroups{$product_id});
365 my $dbh = Bugzilla->dbh;
366 PushGlobalSQLState();
367 my $query = "SELECT 1 FROM group_control_map WHERE entry != 0";
368 $query .= " AND product_id = $product_id" if ($product_id);
369 $query .= " " . $dbh->sql_limit(1);
370 SendSQL($query);
371 if (MoreSQLData()) {
372 $::CachedAnyEntryGroups{$product_id} = MoreSQLData();
373 FetchSQLData();
374 PopGlobalSQLState();
375 return $::CachedAnyEntryGroups{$product_id};
376 } else {
377 return undef;
378 }
379}
380#
381# This function checks if there are any default groups defined.
382# If so, then groups may have to be changed when bugs move from
383# one bug to another.
384sub AnyDefaultGroups {
385 return $::CachedAnyDefaultGroups if defined($::CachedAnyDefaultGroups);
386 my $dbh = Bugzilla->dbh;
387 PushGlobalSQLState();
388 SendSQL("SELECT 1 FROM group_control_map, groups WHERE " .
389 "groups.id = group_control_map.group_id " .
390 "AND isactive != 0 AND " .
391 "(membercontrol = " . CONTROLMAPDEFAULT .
392 " OR othercontrol = " . CONTROLMAPDEFAULT .
393 ") " . $dbh->sql_limit(1));
394 $::CachedAnyDefaultGroups = MoreSQLData();
395 FetchSQLData();
396 PopGlobalSQLState();
397 return $::CachedAnyDefaultGroups;
398}
399
400#
401# This function checks if, given a product id, the user can edit
402# bugs in this product at all.
403sub CanEditProductId {
404 my ($productid) = @_;
405 my $dbh = Bugzilla->dbh;
406 my $query = "SELECT group_id FROM group_control_map " .
407 "WHERE product_id = $productid " .
408 "AND canedit != 0 ";
409 if (%{Bugzilla->user->groups}) {
410 $query .= "AND group_id NOT IN(" .
411 join(',', values(%{Bugzilla->user->groups})) . ") ";
412 }
413 $query .= $dbh->sql_limit(1);
414 PushGlobalSQLState();
415 SendSQL($query);
416 my ($result) = FetchSQLData();
417 PopGlobalSQLState();
418 return (!defined($result));
419}
420
421sub IsInClassification {
422 my ($classification,$productname) = @_;
423
424 if (! Param('useclassification')) {
425 return 1;
426 } else {
427 my $query = "SELECT classifications.name " .
428 "FROM products,classifications " .
429 "WHERE products.classification_id=classifications.id ";
430 $query .= "AND products.name = " . SqlQuote($productname);
431 PushGlobalSQLState();
432 SendSQL($query);
433 my ($ret) = FetchSQLData();
434 PopGlobalSQLState();
435 return ($ret eq $classification);
436 }
437}
438
439# This function determines whether or not a user can enter
440# bugs into the named product.
441sub CanEnterProduct {
442 my ($productname, $verbose) = @_;
443 my $dbh = Bugzilla->dbh;
444
445 return unless defined($productname);
446 trick_taint($productname);
447
448 # First check whether or not the user has access to that product.
449 my $query = "SELECT group_id IS NULL " .
450 "FROM products " .
451 "LEFT JOIN group_control_map " .
452 "ON group_control_map.product_id = products.id " .
453 "AND group_control_map.entry != 0 ";
454 if (%{Bugzilla->user->groups}) {
455 $query .= "AND group_id NOT IN(" .
456 join(',', values(%{Bugzilla->user->groups})) . ") ";
457 }
458 $query .= "WHERE products.name = ? " .
459 $dbh->sql_limit(1);
460
461 my $has_access = $dbh->selectrow_array($query, undef, $productname);
462 if (!$has_access) {
463 # Do we require the exact reason why we cannot enter
464 # bugs into that product? Returning -1 explicitely
465 # means the user has no access to the product or the
466 # product does not exist.
467 return (defined($verbose)) ? -1 : 0;
468 }
469
470 # Check if the product is open for new bugs and has
471 # at least one component and has at least one version.
472 my ($allow_new_bugs, $has_version) =
473 $dbh->selectrow_array('SELECT CASE WHEN disallownew = 0 THEN 1 ELSE 0 END, ' .
474 'versions.value IS NOT NULL ' .
475 'FROM products INNER JOIN components ' .
476 'ON components.product_id = products.id ' .
477 'LEFT JOIN versions ' .
478 'ON versions.product_id = products.id ' .
479 'WHERE products.name = ? ' .
480 $dbh->sql_limit(1), undef, $productname);
481
482
483 if (defined $verbose) {
484 # Return (undef, undef) if the product has no components,
485 # Return (?, 0) if the product has no versions,
486 # Return (0, ?) if the product is closed for new bug entry,
487 # Return (1, 1) if the user can enter bugs into the product,
488 return ($allow_new_bugs, $has_version);
489 } else {
490 # Return undef if the product has no components
491 # Return 0 if the product has no versions, or is closed for bug entry
492 # Return 1 if the user can enter bugs into the product
493 return ($allow_new_bugs && $has_version);
494 }
495}
496
497# Call CanEnterProduct() and display an error message
498# if the user cannot enter bugs into that product.
499sub CanEnterProductOrWarn {
500 my ($product) = @_;
501
502 if (!defined($product)) {
503 ThrowUserError("no_products");
504 }
505 my ($allow_new_bugs, $has_version) = CanEnterProduct($product, 1);
506 trick_taint($product);
507
508 if (!defined $allow_new_bugs) {
509 ThrowUserError("missing_component", { product => $product });
510 } elsif (!$allow_new_bugs) {
511 ThrowUserError("product_disabled", { product => $product});
512 } elsif ($allow_new_bugs < 0) {
513 ThrowUserError("entry_access_denied", { product => $product});
514 } elsif (!$has_version) {
515 ThrowUserError("missing_version", { product => $product });
516 }
517 return 1;
518}
519
520sub GetEnterableProducts {
521 my @products;
522 # XXX rewrite into pure SQL instead of relying on legal_products?
523 foreach my $p (@::legal_product) {
524 if (CanEnterProduct($p)) {
525 push @products, $p;
526 }
527 }
528 return (@products);
529}
530
531
532#
533# This function returns an alphabetical list of product names to which
534# the user can enter bugs. If the $by_id parameter is true, also retrieves IDs
535# and pushes them onto the list as id, name [, id, name...] for easy slurping
536# into a hash by the calling code.
537sub GetSelectableProducts {
538 my ($by_id,$by_classification) = @_;
539
540 my $extra_sql = $by_id ? "id, " : "";
541
542 my $extra_from_sql = $by_classification ? " INNER JOIN classifications"
543 . " ON classifications.id = products.classification_id" : "";
544
545 my $query = "SELECT $extra_sql products.name " .
546 "FROM products $extra_from_sql " .
547 "LEFT JOIN group_control_map " .
548 "ON group_control_map.product_id = products.id ";
549 if (Param('useentrygroupdefault')) {
550 $query .= "AND group_control_map.entry != 0 ";
551 } else {
552 $query .= "AND group_control_map.membercontrol = " .
553 CONTROLMAPMANDATORY . " ";
554 }
555 if (%{Bugzilla->user->groups}) {
556 $query .= "AND group_id NOT IN(" .
557 join(',', values(%{Bugzilla->user->groups})) . ") ";
558 }
559 $query .= "WHERE group_id IS NULL ";
560 if ($by_classification) {
561 $query .= "AND classifications.name = ";
562 $query .= SqlQuote($by_classification) . " ";
563 }
564 $query .= "ORDER BY name";
565 PushGlobalSQLState();
566 SendSQL($query);
567 my @products = ();
568 push(@products, FetchSQLData()) while MoreSQLData();
569 PopGlobalSQLState();
570 return (@products);
571}
572
573# GetSelectableProductHash
574# returns a hash containing
575# legal_products => an enterable product list
576# legal_(components|versions|milestones) =>
577# the list of components, versions, and milestones of enterable products
578# (components|versions|milestones)_by_product
579# => a hash of component lists for each enterable product
580# Milestones only get returned if the usetargetmilestones parameter is set.
581sub GetSelectableProductHash {
582 # The hash of selectable products and their attributes that gets returned
583 # at the end of this function.
584 my $selectables = {};
585
586 my %products = GetSelectableProducts(1);
587
588 $selectables->{legal_products} = [sort values %products];
589
590 # Run queries that retrieve the list of components, versions,
591 # and target milestones (if used) for the selectable products.
592 my @tables = qw(components versions);
593 push(@tables, 'milestones') if Param('usetargetmilestone');
594
595 PushGlobalSQLState();
596 foreach my $table (@tables) {
597 my %values;
598 my %values_by_product;
599
600 if (scalar(keys %products)) {
601 # Why oh why can't we standardize on these names?!?
602 my $fld = ($table eq "components" ? "name" : "value");
603
604 my $query = "SELECT $fld, product_id FROM $table WHERE product_id " .
605 "IN (" . join(",", keys %products) . ") ORDER BY $fld";
606 SendSQL($query);
607
608 while (MoreSQLData()) {
609 my ($name, $product_id) = FetchSQLData();
610 next unless $name;
611 $values{$name} = 1;
612 push @{$values_by_product{$products{$product_id}}}, $name;
613 }
614 }
615
616 $selectables->{"legal_$table"} = [sort keys %values];
617 $selectables->{"${table}_by_product"} = \%values_by_product;
618 }
619 PopGlobalSQLState();
620
621 return $selectables;
622}
623
624#
625# This function returns an alphabetical list of classifications that has products the user can enter bugs.
626sub GetSelectableClassifications {
627 my @selectable_classes = ();
628
629 foreach my $c (sort keys %::classdesc) {
630 if ( scalar(GetSelectableProducts(0,$c)) > 0) {
631 push(@selectable_classes,$c);
632 }
633 }
634 return (@selectable_classes);
635}
636
637
638sub ValidatePassword {
639 # Determines whether or not a password is valid (i.e. meets Bugzilla's
640 # requirements for length and content).
641 # If a second password is passed in, this function also verifies that
642 # the two passwords match.
643 my ($password, $matchpassword) = @_;
644
645 if (length($password) < 3) {
646 ThrowUserError("password_too_short");
647 } elsif (length($password) > 16) {
648 ThrowUserError("password_too_long");
649 } elsif ((defined $matchpassword) && ($password ne $matchpassword)) {
650 ThrowUserError("passwords_dont_match");
651 }
652}
653
654sub DBID_to_name {
655 my ($id) = (@_);
656 return "__UNKNOWN__" if !defined $id;
657 # $id should always be a positive integer
658 if ($id =~ m/^([1-9][0-9]*)$/) {
659 $id = $1;
660 } else {
661 $::cachedNameArray{$id} = "__UNKNOWN__";
662 }
663 if (!defined $::cachedNameArray{$id}) {
664 PushGlobalSQLState();
665 SendSQL("SELECT login_name FROM profiles WHERE userid = $id");
666 my $r = FetchOneColumn();
667 PopGlobalSQLState();
668 if (!defined $r || $r eq "") {
669 $r = "__UNKNOWN__";
670 }
671 $::cachedNameArray{$id} = $r;
672 }
673 return $::cachedNameArray{$id};
674}
675
676sub DBNameToIdAndCheck {
677 my ($name) = (@_);
678 my $result = login_to_id($name);
679 if ($result > 0) {
680 return $result;
681 }
682
683 ThrowUserError("invalid_username", { name => $name });
684}
685
686sub get_classification_id {
687 my ($classification) = @_;
688 PushGlobalSQLState();
689 SendSQL("SELECT id FROM classifications WHERE name = " . SqlQuote($classification));
690 my ($classification_id) = FetchSQLData();
691 PopGlobalSQLState();
692 return $classification_id;
693}
694
695sub get_classification_name {
696 my ($classification_id) = @_;
697 die "non-numeric classification_id '$classification_id' passed to get_classification_name"
698 unless ($classification_id =~ /^\d+$/);
699 PushGlobalSQLState();
700 SendSQL("SELECT name FROM classifications WHERE id = $classification_id");
701 my ($classification) = FetchSQLData();
702 PopGlobalSQLState();
703 return $classification;
704}
705
706
707
708sub get_product_id {
709 my ($prod) = @_;
710 PushGlobalSQLState();
711 SendSQL("SELECT id FROM products WHERE name = " . SqlQuote($prod));
712 my ($prod_id) = FetchSQLData();
713 PopGlobalSQLState();
714 return $prod_id;
715}
716
717sub get_product_name {
718 my ($prod_id) = @_;
719 die "non-numeric prod_id '$prod_id' passed to get_product_name"
720 unless ($prod_id =~ /^\d+$/);
721 PushGlobalSQLState();
722 SendSQL("SELECT name FROM products WHERE id = $prod_id");
723 my ($prod) = FetchSQLData();
724 PopGlobalSQLState();
725 return $prod;
726}
727
728sub get_component_id {
729 my ($prod_id, $comp) = @_;
730 return undef unless ($prod_id && ($prod_id =~ /^\d+$/));
731 PushGlobalSQLState();
732 SendSQL("SELECT id FROM components " .
733 "WHERE product_id = $prod_id AND name = " . SqlQuote($comp));
734 my ($comp_id) = FetchSQLData();
735 PopGlobalSQLState();
736 return $comp_id;
737}
738
739sub get_component_name {
740 my ($comp_id) = @_;
741 die "non-numeric comp_id '$comp_id' passed to get_component_name"
742 unless ($comp_id =~ /^\d+$/);
743 PushGlobalSQLState();
744 SendSQL("SELECT name FROM components WHERE id = $comp_id");
745 my ($comp) = FetchSQLData();
746 PopGlobalSQLState();
747 return $comp;
748}
749
750# This routine quoteUrls contains inspirations from the HTML::FromText CPAN
751# module by Gareth Rees <garethr@cre.canon.co.uk>. It has been heavily hacked,
752# all that is really recognizable from the original is bits of the regular
753# expressions.
754# This has been rewritten to be faster, mainly by substituting 'as we go'.
755# If you want to modify this routine, read the comments carefully
756
757sub quoteUrls {
758 my ($text, $curr_bugid) = (@_);
759 return $text unless $text;
760
761 # We use /g for speed, but uris can have other things inside them
762 # (http://foo/bug#3 for example). Filtering that out filters valid
763 # bug refs out, so we have to do replacements.
764 # mailto can't contain space or #, so we don't have to bother for that
765 # Do this by escaping \0 to \1\0, and replacing matches with \0\0$count\0\0
766 # \0 is used because its unliklely to occur in the text, so the cost of
767 # doing this should be very small
768 # Also, \0 won't appear in the value_quote'd bug title, so we don't have
769 # to worry about bogus substitutions from there
770
771 # escape the 2nd escape char we're using
772 my $chr1 = chr(1);
773 $text =~ s/\0/$chr1\0/g;
774
775 # However, note that adding the title (for buglinks) can affect things
776 # In particular, attachment matches go before bug titles, so that titles
777 # with 'attachment 1' don't double match.
778 # Dupe checks go afterwards, because that uses ^ and \Z, which won't occur
779 # if it was subsituted as a bug title (since that always involve leading
780 # and trailing text)
781
782 # Because of entities, its easier (and quicker) to do this before escaping
783
784 my @things;
785 my $count = 0;
786 my $tmp;
787
788 # non-mailto protocols
789 my $protocol_re = qr/(afs|cid|ftp|gopher|http|https|irc|mid|news|nntp|prospero|telnet|view-source|wais)/i;
790
791 $text =~ s~\b(${protocol_re}: # The protocol:
792 [^\s<>\"]+ # Any non-whitespace
793 [\w\/]) # so that we end in \w or /
794 ~($tmp = html_quote($1)) &&
795 ($things[$count++] = "<a href=\"$tmp\">$tmp</a>") &&
796 ("\0\0" . ($count-1) . "\0\0")
797 ~egox;
798
799 # We have to quote now, otherwise our html is itsself escaped
800 # THIS MEANS THAT A LITERAL ", <, >, ' MUST BE ESCAPED FOR A MATCH
801
802 $text = html_quote($text);
803
804 # mailto:
805 # Use |<nothing> so that $1 is defined regardless
806 $text =~ s~\b(mailto:|)?([\w\.\-\+\=]+\@[\w\-]+(?:\.[\w\-]+)+)\b
807 ~<a href=\"mailto:$2\">$1$2</a>~igx;
808
809 # attachment links - handle both cases separately for simplicity
810 $text =~ s~((?:^Created\ an\ |\b)attachment\s*\(id=(\d+)\)(\s\[edit\])?)
811 ~($things[$count++] = GetAttachmentLink($2, $1)) &&
812 ("\0\0" . ($count-1) . "\0\0")
813 ~egmx;
814
815 $text =~ s~\b(attachment\s*\#?\s*(\d+))
816 ~($things[$count++] = GetAttachmentLink($2, $1)) &&
817 ("\0\0" . ($count-1) . "\0\0")
818 ~egmxi;
819
820 # Current bug ID this comment belongs to
821 my $current_bugurl = $curr_bugid ? "show_bug.cgi?id=$curr_bugid" : "";
822
823 # This handles bug a, comment b type stuff. Because we're using /g
824 # we have to do this in one pattern, and so this is semi-messy.
825 # Also, we can't use $bug_re?$comment_re? because that will match the
826 # empty string
827 my $bug_re = qr/bug\s*\#?\s*(\d+)/i;
828 my $comment_re = qr/comment\s*\#?\s*(\d+)/i;
829 $text =~ s~\b($bug_re(?:\s*,?\s*$comment_re)?|$comment_re)
830 ~ # We have several choices. $1 here is the link, and $2-4 are set
831 # depending on which part matched
832 (defined($2) ? GetBugLink($2,$1,$3) :
833 "<a href=\"$current_bugurl#c$4\">$1</a>")
834 ~egox;
835
836 # Duplicate markers
837 $text =~ s~(?<=^\*\*\*\ This\ bug\ has\ been\ marked\ as\ a\ duplicate\ of\ )
838 (\d+)
839 (?=\ \*\*\*\Z)
840 ~GetBugLink($1, $1)
841 ~egmx;
842
843 # Now remove the encoding hacks
844 $text =~ s/\0\0(\d+)\0\0/$things[$1]/eg;
845 $text =~ s/$chr1\0/\0/g;
846
847 return $text;
848}
849
850# GetAttachmentLink creates a link to an attachment,
851# including its title.
852
853sub GetAttachmentLink {
854 my ($attachid, $link_text) = @_;
855 detaint_natural($attachid) ||
856 die "GetAttachmentLink() called with non-integer attachment number";
857
858 # If we've run GetAttachmentLink() for this attachment before,
859 # %::attachlink will contain an anonymous array ref of relevant
860 # values. If not, we need to get the information from the database.
861 if (! defined $::attachlink{$attachid}) {
862 # Make sure any unfetched data from a currently running query
863 # is saved off rather than overwritten
864 PushGlobalSQLState();
865
866 SendSQL("SELECT bug_id, isobsolete, description
867 FROM attachments WHERE attach_id = $attachid");
868
869 if (MoreSQLData()) {
870 my ($bugid, $isobsolete, $desc) = FetchSQLData();
871 my $title = "";
872 my $className = "";
873 if (Bugzilla->user->can_see_bug($bugid)) {
874 $title = $desc;
875 }
876 if ($isobsolete) {
877 $className = "bz_obsolete";
878 }
879 $::attachlink{$attachid} = [value_quote($title), $className];
880 }
881 else {
882 # Even if there's nothing in the database, we want to save a blank
883 # anonymous array in the %::attachlink hash so the query doesn't get
884 # run again next time we're called for this attachment number.
885 $::attachlink{$attachid} = [];
886 }
887 # All done with this sidetrip
888 PopGlobalSQLState();
889 }
890
891 # Now that we know we've got all the information we're gonna get, let's
892 # return the link (which is the whole reason we were called :)
893 my ($title, $className) = @{$::attachlink{$attachid}};
894 # $title will be undefined if the attachment didn't exist in the database.
895 if (defined $title) {
896 $link_text =~ s/ \[edit\]$//;
897 my $linkval = "attachment.cgi?id=$attachid&amp;action=";
898 # Whitespace matters here because these links are in <pre> tags.
899 return qq|<span class="$className">|
900 . qq|<a href="${linkval}view" title="$title">$link_text</a>|
darin@apple.comc92a1292008-11-10 15:42:34 +0000901 . qq| <a href="${linkval}review" title="$title">[review]</a>|
timothy@apple.comf42518d2008-02-06 20:19:16 +0000902 . qq|</span>|;
903 }
904 else {
905 return qq{$link_text};
906 }
907}
908
909# GetBugLink creates a link to a bug, including its title.
910# It takes either two or three parameters:
911# - The bug number
912# - The link text, to place between the <a>..</a>
913# - An optional comment number, for linking to a particular
914# comment in the bug
915
916sub GetBugLink {
917 my ($bug_num, $link_text, $comment_num) = @_;
918 if (! defined $bug_num || $bug_num eq "") {
919 return "&lt;missing bug number&gt;";
920 }
921 my $quote_bug_num = html_quote($bug_num);
922 detaint_natural($bug_num) || return "&lt;invalid bug number: $quote_bug_num&gt;";
923
924 # If we've run GetBugLink() for this bug number before, %::buglink
925 # will contain an anonymous array ref of relevent values, if not
926 # we need to get the information from the database.
927 if (! defined $::buglink{$bug_num}) {
928 # Make sure any unfetched data from a currently running query
929 # is saved off rather than overwritten
930 PushGlobalSQLState();
931
932 SendSQL("SELECT bugs.bug_status, resolution, short_desc " .
933 "FROM bugs WHERE bugs.bug_id = $bug_num");
934
935 # If the bug exists, save its data off for use later in the sub
936 if (MoreSQLData()) {
937 my ($bug_state, $bug_res, $bug_desc) = FetchSQLData();
938 # Initialize these variables to be "" so that we don't get warnings
939 # if we don't change them below (which is highly likely).
940 my ($pre, $title, $post) = ("", "", "");
941
942 $title = $bug_state;
943 if ($bug_state eq 'UNCONFIRMED') {
944 $pre = "<i>";
945 $post = "</i>";
946 }
947 elsif (! IsOpenedState($bug_state)) {
948 $pre = '<span class="bz_closed">';
949 $title .= " $bug_res";
950 $post = '</span>';
951 }
952 if (Bugzilla->user->can_see_bug($bug_num)) {
953 $title .= " - $bug_desc";
954 }
955 $::buglink{$bug_num} = [$pre, value_quote($title), $post];
956 }
957 else {
958 # Even if there's nothing in the database, we want to save a blank
959 # anonymous array in the %::buglink hash so the query doesn't get
960 # run again next time we're called for this bug number.
961 $::buglink{$bug_num} = [];
962 }
963 # All done with this sidetrip
964 PopGlobalSQLState();
965 }
966
967 # Now that we know we've got all the information we're gonna get, let's
968 # return the link (which is the whole reason we were called :)
969 my ($pre, $title, $post) = @{$::buglink{$bug_num}};
970 # $title will be undefined if the bug didn't exist in the database.
971 if (defined $title) {
972 my $linkval = "show_bug.cgi?id=$bug_num";
973 if (defined $comment_num) {
974 $linkval .= "#c$comment_num";
975 }
976 return qq{$pre<a href="$linkval" title="$title">$link_text</a>$post};
977 }
978 else {
979 return qq{$link_text};
980 }
981}
982
983sub GetLongDescriptionAsText {
984 my ($id, $start, $end) = (@_);
985 my $result = "";
986 my $count = 0;
987 my $anyprivate = 0;
988 my $dbh = Bugzilla->dbh;
989 my ($query) = ("SELECT profiles.login_name, " .
990 $dbh->sql_date_format('longdescs.bug_when', '%Y.%m.%d %H:%i') . ", " .
991 " longdescs.thetext, longdescs.isprivate, " .
992 " longdescs.already_wrapped " .
993 "FROM longdescs, profiles " .
994 "WHERE profiles.userid = longdescs.who " .
995 "AND longdescs.bug_id = $id ");
996
997 # $start will be undef for New bugs, and defined for pre-existing bugs.
998 if ($start) {
999 # If $start is not NULL, obtain the count-index
1000 # of this comment for the leading "Comment #xxx" line.)
1001 SendSQL("SELECT count(*) FROM longdescs " .
1002 " WHERE bug_id = $id AND bug_when <= '$start'");
1003 ($count) = (FetchSQLData());
1004
1005 $query .= " AND longdescs.bug_when > '$start'"
1006 . " AND longdescs.bug_when <= '$end' ";
1007 }
1008
1009 $query .= "ORDER BY longdescs.bug_when";
1010 SendSQL($query);
1011 while (MoreSQLData()) {
1012 my ($who, $when, $text, $isprivate, $work_time, $already_wrapped) =
1013 (FetchSQLData());
1014 if ($count) {
1015 $result .= "\n\n------- Comment #$count from $who".Param('emailsuffix')." ".
1016 Bugzilla::Util::format_time($when) . " -------\n";
1017 }
1018 if (($isprivate > 0) && Param("insidergroup")) {
1019 $anyprivate = 1;
1020 }
1021 $result .= ($already_wrapped ? $text : wrap_comment($text));
1022 $count++;
1023 }
1024
1025 return ($result, $anyprivate);
1026}
1027
1028# Returns a list of all the legal values for a field that has a
1029# list of legal values, like rep_platform or resolution.
1030sub get_legal_field_values {
1031 my ($field) = @_;
1032 my $dbh = Bugzilla->dbh;
1033 my $result_ref = $dbh->selectcol_arrayref(
1034 "SELECT value FROM $field
1035 WHERE isactive = ?
1036 ORDER BY sortkey, value", undef, (1));
1037 return @$result_ref;
1038}
1039
1040sub BugInGroupId {
1041 my ($bugid, $groupid) = (@_);
1042 PushGlobalSQLState();
1043 SendSQL("SELECT bug_id != 0 FROM bug_group_map
1044 WHERE bug_id = $bugid
1045 AND group_id = $groupid");
1046 my $bugingroup = FetchOneColumn();
1047 PopGlobalSQLState();
1048 return $bugingroup;
1049}
1050
1051sub GroupExists {
1052 my ($groupname) = (@_);
1053 PushGlobalSQLState();
1054 SendSQL("SELECT id FROM groups WHERE name=" . SqlQuote($groupname));
1055 my $id = FetchOneColumn();
1056 PopGlobalSQLState();
1057 return $id;
1058}
1059
1060sub GroupNameToId {
1061 my ($groupname) = (@_);
1062 PushGlobalSQLState();
1063 SendSQL("SELECT id FROM groups WHERE name=" . SqlQuote($groupname));
1064 my $id = FetchOneColumn();
1065 PopGlobalSQLState();
1066 return $id;
1067}
1068
1069sub GroupIdToName {
1070 my ($groupid) = (@_);
1071 PushGlobalSQLState();
1072 SendSQL("SELECT name FROM groups WHERE id = $groupid");
1073 my $name = FetchOneColumn();
1074 PopGlobalSQLState();
1075 return $name;
1076}
1077
1078
1079# Determines whether or not a group is active by checking
1080# the "isactive" column for the group in the "groups" table.
1081# Note: This function selects groups by id rather than by name.
1082sub GroupIsActive {
1083 my ($groupid) = (@_);
1084 $groupid ||= 0;
1085 PushGlobalSQLState();
1086 SendSQL("SELECT isactive FROM groups WHERE id=$groupid");
1087 my $isactive = FetchOneColumn();
1088 PopGlobalSQLState();
1089 return $isactive;
1090}
1091
1092# Determines if the given bug_status string represents an "Opened" bug. This
1093# routine ought to be parameterizable somehow, as people tend to introduce
1094# new states into Bugzilla.
1095
1096sub IsOpenedState {
1097 my ($state) = (@_);
1098 if (grep($_ eq $state, OpenStates())) {
1099 return 1;
1100 }
1101 return 0;
1102}
1103
1104# This sub will return an array containing any status that
1105# is considered an open bug.
1106
1107sub OpenStates {
1108 return ('NEW', 'REOPENED', 'ASSIGNED', 'UNCONFIRMED');
1109}
1110
1111
1112###############################################################################
1113
1114# Constructs a format object from URL parameters. You most commonly call it
1115# like this:
1116# my $format = GetFormat("foo/bar", scalar($cgi->param('format')),
1117# scalar($cgi->param('ctype')));
1118
1119sub GetFormat {
1120 my ($template, $format, $ctype) = @_;
1121
1122 $ctype ||= "html";
1123 $format ||= "";
1124
1125 # Security - allow letters and a hyphen only
1126 $ctype =~ s/[^a-zA-Z\-]//g;
1127 $format =~ s/[^a-zA-Z\-]//g;
1128 trick_taint($ctype);
1129 trick_taint($format);
1130
1131 $template .= ($format ? "-$format" : "");
1132 $template .= ".$ctype.tmpl";
1133
1134 # Now check that the template actually exists. We only want to check
1135 # if the template exists; any other errors (eg parse errors) will
1136 # end up being detected later.
1137 eval {
1138 Bugzilla->template->context->template($template);
1139 };
1140 # This parsing may seem fragile, but its OK:
1141 # http://lists.template-toolkit.org/pipermail/templates/2003-March/004370.html
1142 # Even if it is wrong, any sort of error is going to cause a failure
1143 # eventually, so the only issue would be an incorrect error message
1144 if ($@ && $@->info =~ /: not found$/) {
1145 ThrowUserError("format_not_found", { 'format' => $format,
1146 'ctype' => $ctype,
1147 });
1148 }
1149
1150 # Else, just return the info
1151 return
1152 {
1153 'template' => $template ,
1154 'extension' => $ctype ,
1155 'ctype' => Bugzilla::Constants::contenttypes->{$ctype} ,
1156 };
1157}
1158
1159############# Live code below here (that is, not subroutine defs) #############
1160
1161use Bugzilla;
1162
1163$::template = Bugzilla->template();
1164
1165$::vars = {};
1166
11671;