timothy@apple.com | f42518d | 2008-02-06 20:19:16 +0000 | [diff] [blame] | 1 | # -*- 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 | |
| 31 | use strict; |
| 32 | |
| 33 | use Bugzilla::DB qw(:DEFAULT :deprecated); |
| 34 | use Bugzilla::Constants; |
| 35 | use Bugzilla::Util; |
| 36 | # Bring ChmodDataFile in until this is all moved to the module |
| 37 | use Bugzilla::Config qw(:DEFAULT ChmodDataFile $localconfig $datadir); |
| 38 | use Bugzilla::BugMail; |
| 39 | use 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 | |
| 44 | sub 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 |
| 75 | do $localconfig; |
| 76 | |
| 77 | use DBI; |
| 78 | |
| 79 | use Date::Format; # For time2str(). |
| 80 | use Date::Parse; # For str2time(). |
| 81 | |
| 82 | # Use standard Perl libraries for cross-platform file/directory manipulation. |
| 83 | use File::Spec; |
| 84 | |
| 85 | # Some environment variables are not taint safe |
| 86 | delete @::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 | |
| 111 | sub 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 |
| 120 | sub 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 | |
| 318 | sub GetKeywordIdFromName { |
| 319 | my ($name) = (@_); |
| 320 | $name = lc($name); |
| 321 | return $::keywordsbyname{$name}; |
| 322 | } |
| 323 | |
| 324 | |
| 325 | $::VersionTableLoaded = 0; |
| 326 | sub 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 | |
| 349 | sub 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. |
| 360 | sub 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. |
| 384 | sub 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. |
| 403 | sub 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 | |
| 421 | sub 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. |
| 441 | sub 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. |
| 499 | sub 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 | |
| 520 | sub 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. |
| 537 | sub 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. |
| 581 | sub 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. |
| 626 | sub 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 | |
| 638 | sub 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 | |
| 654 | sub 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 | |
| 676 | sub 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 | |
| 686 | sub 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 | |
| 695 | sub 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 | |
| 708 | sub 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 | |
| 717 | sub 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 | |
| 728 | sub 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 | |
| 739 | sub 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 | |
| 757 | sub 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 | |
| 853 | sub 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&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.com | c92a129 | 2008-11-10 15:42:34 +0000 | [diff] [blame] | 901 | . qq| <a href="${linkval}review" title="$title">[review]</a>| |
timothy@apple.com | f42518d | 2008-02-06 20:19:16 +0000 | [diff] [blame] | 902 | . 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 | |
| 916 | sub GetBugLink { |
| 917 | my ($bug_num, $link_text, $comment_num) = @_; |
| 918 | if (! defined $bug_num || $bug_num eq "") { |
| 919 | return "<missing bug number>"; |
| 920 | } |
| 921 | my $quote_bug_num = html_quote($bug_num); |
| 922 | detaint_natural($bug_num) || return "<invalid bug number: $quote_bug_num>"; |
| 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 | |
| 983 | sub 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. |
| 1030 | sub 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 | |
| 1040 | sub 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 | |
| 1051 | sub 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 | |
| 1060 | sub 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 | |
| 1069 | sub 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. |
| 1082 | sub 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 | |
| 1096 | sub 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 | |
| 1107 | sub 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 | |
| 1119 | sub 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 | |
| 1161 | use Bugzilla; |
| 1162 | |
| 1163 | $::template = Bugzilla->template(); |
| 1164 | |
| 1165 | $::vars = {}; |
| 1166 | |
| 1167 | 1; |