buglist.cgi 54 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327
  1. #!/usr/bin/env perl -wT
  2. # -*- Mode: perl; indent-tabs-mode: nil -*-
  3. #
  4. # The contents of this file are subject to the Mozilla Public
  5. # License Version 1.1 (the "License"); you may not use this file
  6. # except in compliance with the License. You may obtain a copy of
  7. # the License at http://www.mozilla.org/MPL/
  8. #
  9. # Software distributed under the License is distributed on an "AS
  10. # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
  11. # implied. See the License for the specific language governing
  12. # rights and limitations under the License.
  13. #
  14. # The Original Code is the Bugzilla Bug Tracking System.
  15. #
  16. # The Initial Developer of the Original Code is Netscape Communications
  17. # Corporation. Portions created by Netscape are
  18. # Copyright (C) 1998 Netscape Communications Corporation. All
  19. # Rights Reserved.
  20. #
  21. # Contributor(s): Terry Weissman <terry@mozilla.org>
  22. # Dan Mosedale <dmose@mozilla.org>
  23. # Stephan Niemz <st.n@gmx.net>
  24. # Andreas Franke <afranke@mathweb.org>
  25. # Myk Melez <myk@mozilla.org>
  26. # Max Kanat-Alexander <mkanat@bugzilla.org>
  27. ################################################################################
  28. # Script Initialization
  29. ################################################################################
  30. # Make it harder for us to do dangerous things in Perl.
  31. use strict;
  32. use lib qw(. lib);
  33. use Bugzilla;
  34. use Bugzilla::Constants;
  35. use Bugzilla::Error;
  36. use Bugzilla::Util;
  37. use Bugzilla::Search;
  38. use Bugzilla::Search::Quicksearch;
  39. use Bugzilla::Search::Saved;
  40. use Bugzilla::User;
  41. use Bugzilla::Bug;
  42. use Bugzilla::Product;
  43. use Bugzilla::Keyword;
  44. use Bugzilla::Field;
  45. use Bugzilla::Status;
  46. use Bugzilla::Token;
  47. use Date::Parse;
  48. my $cgi = Bugzilla->cgi;
  49. my $dbh = Bugzilla->dbh;
  50. my $template = Bugzilla->template;
  51. my $vars = {};
  52. my $buffer = $cgi->query_string();
  53. # We have to check the login here to get the correct footer if an error is
  54. # thrown and to prevent a logged out user to use QuickSearch if 'requirelogin'
  55. # is turned 'on'.
  56. Bugzilla->login();
  57. if (length($buffer) == 0) {
  58. print $cgi->header(-refresh=> '10; URL=query.cgi');
  59. ThrowUserError("buglist_parameters_required");
  60. }
  61. # Determine whether this is a quicksearch query.
  62. my $searchstring = $cgi->param('quicksearch');
  63. if (defined($searchstring)) {
  64. $buffer = quicksearch($searchstring);
  65. # Quicksearch may do a redirect, in which case it does not return.
  66. # If it does return, it has modified $cgi->params so we can use them here
  67. # as if this had been a normal query from the beginning.
  68. }
  69. # If configured to not allow empty words, reject empty searches from the
  70. # Find a Specific Bug search form, including words being a single or
  71. # several consecutive whitespaces only.
  72. if (!Bugzilla->params->{'specific_search_allow_empty_words'}
  73. && defined($cgi->param('content')) && $cgi->param('content') =~ /^\s*$/)
  74. {
  75. ThrowUserError("buglist_parameters_required");
  76. }
  77. ################################################################################
  78. # Data and Security Validation
  79. ################################################################################
  80. # Whether or not the user wants to change multiple bugs.
  81. my $dotweak = $cgi->param('tweak') ? 1 : 0;
  82. # Log the user in
  83. if ($dotweak) {
  84. Bugzilla->login(LOGIN_REQUIRED);
  85. }
  86. # Hack to support legacy applications that think the RDF ctype is at format=rdf.
  87. if (defined $cgi->param('format') && $cgi->param('format') eq "rdf"
  88. && !defined $cgi->param('ctype')) {
  89. $cgi->param('ctype', "rdf");
  90. $cgi->delete('format');
  91. }
  92. # Treat requests for ctype=rss as requests for ctype=atom
  93. if (defined $cgi->param('ctype') && $cgi->param('ctype') eq "rss") {
  94. $cgi->param('ctype', "atom");
  95. }
  96. # The js ctype presents a security risk; a malicious site could use it
  97. # to gather information about secure bugs. So, we only allow public bugs to be
  98. # retrieved with this format.
  99. #
  100. # Note that if and when this call clears cookies or has other persistent
  101. # effects, we'll need to do this another way instead.
  102. if ((defined $cgi->param('ctype')) && ($cgi->param('ctype') eq "js")) {
  103. Bugzilla->logout_request();
  104. }
  105. # An agent is a program that automatically downloads and extracts data
  106. # on its user's behalf. If this request comes from an agent, we turn off
  107. # various aspects of bug list functionality so agent requests succeed
  108. # and coexist nicely with regular user requests. Currently the only agent
  109. # we know about is Firefox's microsummary feature.
  110. my $agent = ($cgi->http('X-Moz') && $cgi->http('X-Moz') =~ /\bmicrosummary\b/);
  111. # Determine the format in which the user would like to receive the output.
  112. # Uses the default format if the user did not specify an output format;
  113. # otherwise validates the user's choice against the list of available formats.
  114. my $format = $template->get_format("list/list", scalar $cgi->param('format'),
  115. scalar $cgi->param('ctype'));
  116. # Use server push to display a "Please wait..." message for the user while
  117. # executing their query if their browser supports it and they are viewing
  118. # the bug list as HTML and they have not disabled it by adding &serverpush=0
  119. # to the URL.
  120. #
  121. # Server push is a Netscape 3+ hack incompatible with MSIE, Lynx, and others.
  122. # Even Communicator 4.51 has bugs with it, especially during page reload.
  123. # http://www.browsercaps.org used as source of compatible browsers.
  124. # Safari (WebKit) does not support it, despite a UA that says otherwise (bug 188712)
  125. # MSIE 5+ supports it on Mac (but not on Windows) (bug 190370)
  126. #
  127. my $serverpush =
  128. $format->{'extension'} eq "html"
  129. && exists $ENV{'HTTP_USER_AGENT'}
  130. && $ENV{'HTTP_USER_AGENT'} =~ /Mozilla.[3-9]/
  131. && (($ENV{'HTTP_USER_AGENT'} !~ /[Cc]ompatible/) || ($ENV{'HTTP_USER_AGENT'} =~ /MSIE 5.*Mac_PowerPC/))
  132. && $ENV{'HTTP_USER_AGENT'} !~ /WebKit/
  133. && !$agent
  134. && !defined($cgi->param('serverpush'))
  135. || $cgi->param('serverpush');
  136. my $order = $cgi->param('order') || "";
  137. my $order_from_cookie = 0; # True if $order set using the LASTORDER cookie
  138. # The params object to use for the actual query itself
  139. my $params;
  140. # If the user is retrieving the last bug list they looked at, hack the buffer
  141. # storing the query string so that it looks like a query retrieving those bugs.
  142. if (defined $cgi->param('regetlastlist')) {
  143. $cgi->cookie('BUGLIST') || ThrowUserError("missing_cookie");
  144. $order = "reuse last sort" unless $order;
  145. my $bug_id = $cgi->cookie('BUGLIST');
  146. $bug_id =~ s/:/,/g;
  147. # set up the params for this new query
  148. $params = new Bugzilla::CGI({
  149. bug_id => $bug_id,
  150. order => $order,
  151. });
  152. }
  153. if ($buffer =~ /&cmd-/) {
  154. my $url = "query.cgi?$buffer#chart";
  155. print $cgi->redirect(-location => $url);
  156. # Generate and return the UI (HTML page) from the appropriate template.
  157. $vars->{'message'} = "buglist_adding_field";
  158. $vars->{'url'} = $url;
  159. $template->process("global/message.html.tmpl", $vars)
  160. || ThrowTemplateError($template->error());
  161. exit;
  162. }
  163. # Figure out whether or not the user is doing a fulltext search. If not,
  164. # we'll remove the relevance column from the lists of columns to display
  165. # and order by, since relevance only exists when doing a fulltext search.
  166. my $fulltext = 0;
  167. if ($cgi->param('content')) { $fulltext = 1 }
  168. my @charts = map(/^field(\d-\d-\d)$/ ? $1 : (), $cgi->param());
  169. foreach my $chart (@charts) {
  170. if ($cgi->param("field$chart") eq 'content' && $cgi->param("value$chart")) {
  171. $fulltext = 1;
  172. last;
  173. }
  174. }
  175. ################################################################################
  176. # Utilities
  177. ################################################################################
  178. local our @weekday= qw( Sun Mon Tue Wed Thu Fri Sat );
  179. sub DiffDate {
  180. my ($datestr) = @_;
  181. my $date = str2time($datestr);
  182. my $age = time() - $date;
  183. my ($s,$m,$h,$d,$mo,$y,$wd)= localtime $date;
  184. if( $age < 18*60*60 ) {
  185. $date = sprintf "%02d:%02d:%02d", $h,$m,$s;
  186. } elsif( $age < 6*24*60*60 ) {
  187. $date = sprintf "%s %02d:%02d", $weekday[$wd],$h,$m;
  188. } else {
  189. $date = sprintf "%04d-%02d-%02d", 1900+$y,$mo+1,$d;
  190. }
  191. return $date;
  192. }
  193. sub LookupNamedQuery {
  194. my ($name, $sharer_id, $query_type, $throw_error) = @_;
  195. my $user = Bugzilla->login(LOGIN_REQUIRED);
  196. my $dbh = Bugzilla->dbh;
  197. my $owner_id;
  198. $throw_error = 1 unless defined $throw_error;
  199. # $name and $sharer_id are safe -- we only use them below in SELECT
  200. # placeholders and then in error messages (which are always HTML-filtered).
  201. $name || ThrowUserError("query_name_missing");
  202. trick_taint($name);
  203. if ($sharer_id) {
  204. $owner_id = $sharer_id;
  205. detaint_natural($owner_id);
  206. $owner_id || ThrowUserError('illegal_user_id', {'userid' => $sharer_id});
  207. }
  208. else {
  209. $owner_id = $user->id;
  210. }
  211. my @args = ($owner_id, $name);
  212. my $extra = '';
  213. # If $query_type is defined, then we restrict our search.
  214. if (defined $query_type) {
  215. $extra = ' AND query_type = ? ';
  216. detaint_natural($query_type);
  217. push(@args, $query_type);
  218. }
  219. my ($id, $result) = $dbh->selectrow_array("SELECT id, query
  220. FROM namedqueries
  221. WHERE userid = ? AND name = ?
  222. $extra",
  223. undef, @args);
  224. # Some DBs (read: Oracle) incorrectly mark this string as UTF-8
  225. # even though it has no UTF-8 characters in it, which prevents
  226. # Bugzilla::CGI from later reading it correctly.
  227. utf8::downgrade($result) if utf8::is_utf8($result);
  228. if (!defined($result)) {
  229. return 0 unless $throw_error;
  230. ThrowUserError("missing_query", {'queryname' => $name,
  231. 'sharer_id' => $sharer_id});
  232. }
  233. if ($sharer_id) {
  234. my $group = $dbh->selectrow_array('SELECT group_id
  235. FROM namedquery_group_map
  236. WHERE namedquery_id = ?',
  237. undef, $id);
  238. if (!grep {$_ == $group} values(%{$user->groups()})) {
  239. ThrowUserError("missing_query", {'queryname' => $name,
  240. 'sharer_id' => $sharer_id});
  241. }
  242. }
  243. $result
  244. || ThrowUserError("buglist_parameters_required", {'queryname' => $name});
  245. return wantarray ? ($result, $id) : $result;
  246. }
  247. # Inserts a Named Query (a "Saved Search") into the database, or
  248. # updates a Named Query that already exists..
  249. # Takes four arguments:
  250. # userid - The userid who the Named Query will belong to.
  251. # query_name - A string that names the new Named Query, or the name
  252. # of an old Named Query to update. If this is blank, we
  253. # will throw a UserError. Leading and trailing whitespace
  254. # will be stripped from this value before it is inserted
  255. # into the DB.
  256. # query - The query part of the buglist.cgi URL, unencoded. Must not be
  257. # empty, or we will throw a UserError.
  258. # link_in_footer (optional) - 1 if the Named Query should be
  259. # displayed in the user's footer, 0 otherwise.
  260. # query_type (optional) - 1 if the Named Query contains a list of
  261. # bug IDs only, 0 otherwise (default).
  262. #
  263. # All parameters are validated before passing them into the database.
  264. #
  265. # Returns: A boolean true value if the query existed in the database
  266. # before, and we updated it. A boolean false value otherwise.
  267. sub InsertNamedQuery {
  268. my ($query_name, $query, $link_in_footer, $query_type) = @_;
  269. my $dbh = Bugzilla->dbh;
  270. $query_name = trim($query_name);
  271. my ($query_obj) = grep {lc($_->name) eq lc($query_name)} @{Bugzilla->user->queries};
  272. if ($query_obj) {
  273. $query_obj->set_name($query_name);
  274. $query_obj->set_url($query);
  275. $query_obj->set_query_type($query_type);
  276. $query_obj->update();
  277. } else {
  278. Bugzilla::Search::Saved->create({
  279. name => $query_name,
  280. query => $query,
  281. query_type => $query_type,
  282. link_in_footer => $link_in_footer
  283. });
  284. }
  285. return $query_obj ? 1 : 0;
  286. }
  287. sub LookupSeries {
  288. my ($series_id) = @_;
  289. detaint_natural($series_id) || ThrowCodeError("invalid_series_id");
  290. my $dbh = Bugzilla->dbh;
  291. my $result = $dbh->selectrow_array("SELECT query FROM series " .
  292. "WHERE series_id = ?"
  293. , undef, ($series_id));
  294. $result
  295. || ThrowCodeError("invalid_series_id", {'series_id' => $series_id});
  296. return $result;
  297. }
  298. sub GetQuip {
  299. my $dbh = Bugzilla->dbh;
  300. # COUNT is quick because it is cached for MySQL. We may want to revisit
  301. # this when we support other databases.
  302. my $count = $dbh->selectrow_array("SELECT COUNT(quip)"
  303. . " FROM quips WHERE approved = 1");
  304. my $random = int(rand($count));
  305. my $quip =
  306. $dbh->selectrow_array("SELECT quip FROM quips WHERE approved = 1 " .
  307. $dbh->sql_limit(1, $random));
  308. return $quip;
  309. }
  310. # Return groups available for at least one product of the buglist.
  311. sub GetGroups {
  312. my $product_names = shift;
  313. my $user = Bugzilla->user;
  314. my %legal_groups;
  315. foreach my $product_name (@$product_names) {
  316. my $product = new Bugzilla::Product({name => $product_name});
  317. foreach my $gid (keys %{$product->group_controls}) {
  318. # The user can only edit groups he belongs to.
  319. next unless $user->in_group_id($gid);
  320. # The user has no control on groups marked as NA or MANDATORY.
  321. my $group = $product->group_controls->{$gid};
  322. next if ($group->{membercontrol} == CONTROLMAPMANDATORY
  323. || $group->{membercontrol} == CONTROLMAPNA);
  324. # It's fine to include inactive groups. Those will be marked
  325. # as "remove only" when editing several bugs at once.
  326. $legal_groups{$gid} ||= $group->{group};
  327. }
  328. }
  329. # Return a list of group objects.
  330. return [values %legal_groups];
  331. }
  332. sub _close_standby_message {
  333. my ($contenttype, $disposition, $serverpush) = @_;
  334. my $cgi = Bugzilla->cgi;
  335. # Close the "please wait" page, then open the buglist page
  336. if ($serverpush) {
  337. print $cgi->multipart_end();
  338. print $cgi->multipart_start(-type => $contenttype,
  339. -content_disposition => $disposition);
  340. }
  341. else {
  342. print $cgi->header(-type => $contenttype,
  343. -content_disposition => $disposition);
  344. }
  345. }
  346. ################################################################################
  347. # Command Execution
  348. ################################################################################
  349. $cgi->param('cmdtype', "") if !defined $cgi->param('cmdtype');
  350. $cgi->param('remaction', "") if !defined $cgi->param('remaction');
  351. # Backwards-compatibility - the old interface had cmdtype="runnamed" to run
  352. # a named command, and we can't break this because it's in bookmarks.
  353. if ($cgi->param('cmdtype') eq "runnamed") {
  354. $cgi->param('cmdtype', "dorem");
  355. $cgi->param('remaction', "run");
  356. }
  357. # Now we're going to be running, so ensure that the params object is set up,
  358. # using ||= so that we only do so if someone hasn't overridden this
  359. # earlier, for example by setting up a named query search.
  360. # This will be modified, so make a copy.
  361. $params ||= new Bugzilla::CGI($cgi);
  362. # Generate a reasonable filename for the user agent to suggest to the user
  363. # when the user saves the bug list. Uses the name of the remembered query
  364. # if available. We have to do this now, even though we return HTTP headers
  365. # at the end, because the fact that there is a remembered query gets
  366. # forgotten in the process of retrieving it.
  367. my @time = localtime(time());
  368. my $date = sprintf "%04d-%02d-%02d", 1900+$time[5],$time[4]+1,$time[3];
  369. my $filename = "bugs-$date.$format->{extension}";
  370. if ($cgi->param('cmdtype') eq "dorem" && $cgi->param('remaction') =~ /^run/) {
  371. $filename = $cgi->param('namedcmd') . "-$date.$format->{extension}";
  372. # Remove white-space from the filename so the user cannot tamper
  373. # with the HTTP headers.
  374. $filename =~ s/\s/_/g;
  375. }
  376. $filename =~ s/\\/\\\\/g; # escape backslashes
  377. $filename =~ s/"/\\"/g; # escape quotes
  378. # Take appropriate action based on user's request.
  379. if ($cgi->param('cmdtype') eq "dorem") {
  380. if ($cgi->param('remaction') eq "run") {
  381. my $query_id;
  382. ($buffer, $query_id) = LookupNamedQuery(scalar $cgi->param("namedcmd"),
  383. scalar $cgi->param('sharer_id'));
  384. # If this is the user's own query, remember information about it
  385. # so that it can be modified easily.
  386. $vars->{'searchname'} = $cgi->param('namedcmd');
  387. if (!$cgi->param('sharer_id') ||
  388. $cgi->param('sharer_id') == Bugzilla->user->id) {
  389. $vars->{'searchtype'} = "saved";
  390. $vars->{'search_id'} = $query_id;
  391. }
  392. $params = new Bugzilla::CGI($buffer);
  393. $order = $params->param('order') || $order;
  394. }
  395. elsif ($cgi->param('remaction') eq "runseries") {
  396. $buffer = LookupSeries(scalar $cgi->param("series_id"));
  397. $vars->{'searchname'} = $cgi->param('namedcmd');
  398. $vars->{'searchtype'} = "series";
  399. $params = new Bugzilla::CGI($buffer);
  400. $order = $params->param('order') || $order;
  401. }
  402. elsif ($cgi->param('remaction') eq "forget") {
  403. my $user = Bugzilla->login(LOGIN_REQUIRED);
  404. # Copy the name into a variable, so that we can trick_taint it for
  405. # the DB. We know it's safe, because we're using placeholders in
  406. # the SQL, and the SQL is only a DELETE.
  407. my $qname = $cgi->param('namedcmd');
  408. trick_taint($qname);
  409. # Do not forget the saved search if it is being used in a whine
  410. my $whines_in_use =
  411. $dbh->selectcol_arrayref('SELECT DISTINCT whine_events.subject
  412. FROM whine_events
  413. INNER JOIN whine_queries
  414. ON whine_queries.eventid
  415. = whine_events.id
  416. WHERE whine_events.owner_userid
  417. = ?
  418. AND whine_queries.query_name
  419. = ?
  420. ', undef, $user->id, $qname);
  421. if (scalar(@$whines_in_use)) {
  422. ThrowUserError('saved_search_used_by_whines',
  423. { subjects => join(',', @$whines_in_use),
  424. search_name => $qname }
  425. );
  426. }
  427. # If we are here, then we can safely remove the saved search
  428. my ($query_id) = $dbh->selectrow_array('SELECT id FROM namedqueries
  429. WHERE userid = ?
  430. AND name = ?',
  431. undef, ($user->id, $qname));
  432. if (!$query_id) {
  433. # The user has no query of this name. Play along.
  434. }
  435. else {
  436. # Make sure the user really wants to delete his saved search.
  437. my $token = $cgi->param('token');
  438. check_hash_token($token, [$query_id, $qname]);
  439. $dbh->do('DELETE FROM namedqueries
  440. WHERE id = ?',
  441. undef, $query_id);
  442. $dbh->do('DELETE FROM namedqueries_link_in_footer
  443. WHERE namedquery_id = ?',
  444. undef, $query_id);
  445. $dbh->do('DELETE FROM namedquery_group_map
  446. WHERE namedquery_id = ?',
  447. undef, $query_id);
  448. }
  449. # Now reset the cached queries
  450. $user->flush_queries_cache();
  451. print $cgi->header();
  452. # Generate and return the UI (HTML page) from the appropriate template.
  453. $vars->{'message'} = "buglist_query_gone";
  454. $vars->{'namedcmd'} = $qname;
  455. $vars->{'url'} = "query.cgi";
  456. $template->process("global/message.html.tmpl", $vars)
  457. || ThrowTemplateError($template->error());
  458. exit;
  459. }
  460. }
  461. elsif (($cgi->param('cmdtype') eq "doit") && defined $cgi->param('remtype')) {
  462. if ($cgi->param('remtype') eq "asdefault") {
  463. my $user = Bugzilla->login(LOGIN_REQUIRED);
  464. InsertNamedQuery(DEFAULT_QUERY_NAME, $buffer);
  465. $vars->{'message'} = "buglist_new_default_query";
  466. }
  467. elsif ($cgi->param('remtype') eq "asnamed") {
  468. my $user = Bugzilla->login(LOGIN_REQUIRED);
  469. my $query_name = $cgi->param('newqueryname');
  470. my $new_query = $cgi->param('newquery');
  471. my $query_type = QUERY_LIST;
  472. # If list_of_bugs is true, we are adding/removing individual bugs
  473. # to a saved search. We get the existing list of bug IDs (if any)
  474. # and add/remove the passed ones.
  475. if ($cgi->param('list_of_bugs')) {
  476. # We add or remove bugs based on the action choosen.
  477. my $action = trim($cgi->param('action') || '');
  478. $action =~ /^(add|remove)$/
  479. || ThrowCodeError('unknown_action', {'action' => $action});
  480. # If we are removing bugs, then we must have an existing
  481. # saved search selected.
  482. if ($action eq 'remove') {
  483. $query_name && ThrowUserError('no_bugs_to_remove');
  484. }
  485. my %bug_ids;
  486. my $is_new_name = 0;
  487. if ($query_name) {
  488. my ($query, $query_id) =
  489. LookupNamedQuery($query_name, undef, QUERY_LIST, !THROW_ERROR);
  490. # Make sure this name is not already in use by a normal saved search.
  491. if ($query) {
  492. ThrowUserError('query_name_exists', {name => $query_name,
  493. query_id => $query_id});
  494. }
  495. $is_new_name = 1;
  496. }
  497. # If no new tag name has been given, use the selected one.
  498. $query_name ||= $cgi->param('oldqueryname');
  499. # Don't throw an error if it's a new tag name: if the tag already
  500. # exists, add/remove bugs to it, else create it. But if we are
  501. # considering an existing tag, then it has to exist and we throw
  502. # an error if it doesn't (hence the usage of !$is_new_name).
  503. if (my $old_query = LookupNamedQuery($query_name, undef, LIST_OF_BUGS, !$is_new_name)) {
  504. # We get the encoded query. We need to decode it.
  505. my $old_cgi = new Bugzilla::CGI($old_query);
  506. foreach my $bug_id (split /[\s,]+/, scalar $old_cgi->param('bug_id')) {
  507. $bug_ids{$bug_id} = 1 if detaint_natural($bug_id);
  508. }
  509. }
  510. my $keep_bug = ($action eq 'add') ? 1 : 0;
  511. my $changes = 0;
  512. foreach my $bug_id (split(/[\s,]+/, $cgi->param('bug_ids'))) {
  513. next unless $bug_id;
  514. ValidateBugID($bug_id);
  515. $bug_ids{$bug_id} = $keep_bug;
  516. $changes = 1;
  517. }
  518. ThrowUserError('no_bug_ids',
  519. {'action' => $action,
  520. 'tag' => $query_name})
  521. unless $changes;
  522. # Only keep bug IDs we want to add/keep. Disregard deleted ones.
  523. my @bug_ids = grep { $bug_ids{$_} == 1 } keys %bug_ids;
  524. # If the list is now empty, we could as well delete it completely.
  525. ThrowUserError('no_bugs_in_list', {'tag' => $query_name})
  526. unless scalar(@bug_ids);
  527. $new_query = "bug_id=" . join(',', sort {$a <=> $b} @bug_ids);
  528. $query_type = LIST_OF_BUGS;
  529. }
  530. my $tofooter = 1;
  531. my $existed_before = InsertNamedQuery($query_name, $new_query,
  532. $tofooter, $query_type);
  533. if ($existed_before) {
  534. $vars->{'message'} = "buglist_updated_named_query";
  535. }
  536. else {
  537. $vars->{'message'} = "buglist_new_named_query";
  538. }
  539. # Make sure to invalidate any cached query data, so that the footer is
  540. # correctly displayed
  541. $user->flush_queries_cache();
  542. $vars->{'queryname'} = $query_name;
  543. print $cgi->header();
  544. $template->process("global/message.html.tmpl", $vars)
  545. || ThrowTemplateError($template->error());
  546. exit;
  547. }
  548. }
  549. # backward compatibility hack: if the saved query doesn't say which
  550. # form was used to create it, assume it was on the advanced query
  551. # form - see bug 252295
  552. if (!$params->param('query_format')) {
  553. $params->param('query_format', 'advanced');
  554. $buffer = $params->query_string;
  555. }
  556. ################################################################################
  557. # Column Definition
  558. ################################################################################
  559. # Define the columns that can be selected in a query and/or displayed in a bug
  560. # list. Column records include the following fields:
  561. #
  562. # 1. ID: a unique identifier by which the column is referred in code;
  563. #
  564. # 2. Name: The name of the column in the database (may also be an expression
  565. # that returns the value of the column);
  566. #
  567. # 3. Title: The title of the column as displayed to users.
  568. #
  569. # Note: There are a few hacks in the code that deviate from these definitions.
  570. # In particular, when the list is sorted by the "votes" field the word
  571. # "DESC" is added to the end of the field to sort in descending order,
  572. # and the redundant short_desc column is removed when the client
  573. # requests "all" columns.
  574. # Note: For column names using aliasing (SQL "<field> AS <alias>"), the column
  575. # ID needs to be identical to the field ID for list ordering to work.
  576. local our $columns = {};
  577. sub DefineColumn {
  578. my ($id, $name, $title) = @_;
  579. $columns->{$id} = { 'name' => $name , 'title' => $title };
  580. }
  581. # Column: ID Name Title
  582. DefineColumn("bug_id" , "bugs.bug_id" , "ID" );
  583. DefineColumn("alias" , "bugs.alias" , "Alias" );
  584. DefineColumn("opendate" , "bugs.creation_ts" , "Opened" );
  585. DefineColumn("changeddate" , "bugs.delta_ts" , "Changed" );
  586. DefineColumn("bug_severity" , "bugs.bug_severity" , "Severity" );
  587. DefineColumn("priority" , "bugs.priority" , "Priority" );
  588. DefineColumn("rep_platform" , "bugs.rep_platform" , "Hardware" );
  589. DefineColumn("assigned_to" , "map_assigned_to.login_name" , "Assignee" );
  590. DefineColumn("reporter" , "map_reporter.login_name" , "Reporter" );
  591. DefineColumn("qa_contact" , "map_qa_contact.login_name" , "QA Contact" );
  592. if ($format->{'extension'} eq 'html') {
  593. DefineColumn("assigned_to_realname", "CASE WHEN map_assigned_to.realname = '' THEN map_assigned_to.login_name ELSE map_assigned_to.realname END AS assigned_to_realname", "Assignee" );
  594. DefineColumn("reporter_realname" , "CASE WHEN map_reporter.realname = '' THEN map_reporter.login_name ELSE map_reporter.realname END AS reporter_realname" , "Reporter" );
  595. DefineColumn("qa_contact_realname" , "CASE WHEN map_qa_contact.realname = '' THEN map_qa_contact.login_name ELSE map_qa_contact.realname END AS qa_contact_realname" , "QA Contact");
  596. } else {
  597. DefineColumn("assigned_to_realname", "map_assigned_to.realname AS assigned_to_realname", "Assignee" );
  598. DefineColumn("reporter_realname" , "map_reporter.realname AS reporter_realname" , "Reporter" );
  599. DefineColumn("qa_contact_realname" , "map_qa_contact.realname AS qa_contact_realname" , "QA Contact");
  600. }
  601. DefineColumn("bug_status" , "bugs.bug_status" , "Status" );
  602. DefineColumn("resolution" , "bugs.resolution" , "Resolution" );
  603. DefineColumn("short_short_desc" , "bugs.short_desc" , "Summary" );
  604. DefineColumn("short_desc" , "bugs.short_desc" , "Summary" );
  605. DefineColumn("status_whiteboard" , "bugs.status_whiteboard" , "Whiteboard" );
  606. DefineColumn("component" , "map_components.name" , "Component" );
  607. DefineColumn("product" , "map_products.name" , "Product" );
  608. DefineColumn("classification" , "map_classifications.name" , "Classification" );
  609. DefineColumn("version" , "bugs.version" , "Version" );
  610. DefineColumn("op_sys" , "bugs.op_sys" , "OS" );
  611. DefineColumn("target_milestone" , "bugs.target_milestone" , "Target Milestone" );
  612. DefineColumn("votes" , "bugs.votes" , "Votes" );
  613. DefineColumn("keywords" , "bugs.keywords" , "Keywords" );
  614. DefineColumn("estimated_time" , "bugs.estimated_time" , "Estimated Hours" );
  615. DefineColumn("remaining_time" , "bugs.remaining_time" , "Remaining Hours" );
  616. DefineColumn("actual_time" , "(SUM(ldtime.work_time)*COUNT(DISTINCT ldtime.bug_when)/COUNT(bugs.bug_id)) AS actual_time", "Actual Hours");
  617. DefineColumn("percentage_complete",
  618. "(CASE WHEN (SUM(ldtime.work_time)*COUNT(DISTINCT ldtime.bug_when)/COUNT(bugs.bug_id)) " .
  619. " + bugs.remaining_time = 0.0 " .
  620. "THEN 0.0 " .
  621. "ELSE 100*((SUM(ldtime.work_time)*COUNT(DISTINCT ldtime.bug_when)/COUNT(bugs.bug_id)) " .
  622. " /((SUM(ldtime.work_time)*COUNT(DISTINCT ldtime.bug_when)/COUNT(bugs.bug_id)) + bugs.remaining_time)) " .
  623. "END) AS percentage_complete" , "% Complete");
  624. DefineColumn("relevance" , "relevance" , "Relevance" );
  625. DefineColumn("deadline" , $dbh->sql_date_format('bugs.deadline', '%Y-%m-%d') . " AS deadline", "Deadline");
  626. foreach my $field (Bugzilla->active_custom_fields) {
  627. # Multi-select fields are not (yet) supported in buglists.
  628. next if $field->type == FIELD_TYPE_MULTI_SELECT;
  629. DefineColumn($field->name, 'bugs.' . $field->name, $field->description);
  630. }
  631. Bugzilla::Hook::process("buglist-columns", {'columns' => $columns} );
  632. ################################################################################
  633. # Display Column Determination
  634. ################################################################################
  635. # Determine the columns that will be displayed in the bug list via the
  636. # columnlist CGI parameter, the user's preferences, or the default.
  637. my @displaycolumns = ();
  638. if (defined $params->param('columnlist')) {
  639. if ($params->param('columnlist') eq "all") {
  640. # If the value of the CGI parameter is "all", display all columns,
  641. # but remove the redundant "short_desc" column.
  642. @displaycolumns = grep($_ ne 'short_desc', keys(%$columns));
  643. }
  644. else {
  645. @displaycolumns = split(/[ ,]+/, $params->param('columnlist'));
  646. }
  647. }
  648. elsif (defined $cgi->cookie('COLUMNLIST')) {
  649. # 2002-10-31 Rename column names (see bug 176461)
  650. my $columnlist = $cgi->cookie('COLUMNLIST');
  651. $columnlist =~ s/\bowner\b/assigned_to/;
  652. $columnlist =~ s/\bowner_realname\b/assigned_to_realname/;
  653. $columnlist =~ s/\bplatform\b/rep_platform/;
  654. $columnlist =~ s/\bseverity\b/bug_severity/;
  655. $columnlist =~ s/\bstatus\b/bug_status/;
  656. $columnlist =~ s/\bsummaryfull\b/short_desc/;
  657. $columnlist =~ s/\bsummary\b/short_short_desc/;
  658. # Use the columns listed in the user's preferences.
  659. @displaycolumns = split(/ /, $columnlist);
  660. }
  661. else {
  662. # Use the default list of columns.
  663. @displaycolumns = DEFAULT_COLUMN_LIST;
  664. }
  665. # Weed out columns that don't actually exist to prevent the user
  666. # from hacking their column list cookie to grab data to which they
  667. # should not have access. Detaint the data along the way.
  668. @displaycolumns = grep($columns->{$_} && trick_taint($_), @displaycolumns);
  669. # Remove the "ID" column from the list because bug IDs are always displayed
  670. # and are hard-coded into the display templates.
  671. @displaycolumns = grep($_ ne 'bug_id', @displaycolumns);
  672. # Add the votes column to the list of columns to be displayed
  673. # in the bug list if the user is searching for bugs with a certain
  674. # number of votes and the votes column is not already on the list.
  675. # Some versions of perl will taint 'votes' if this is done as a single
  676. # statement, because the votes param is tainted at this point
  677. my $votes = $params->param('votes');
  678. $votes ||= "";
  679. if (trim($votes) && !grep($_ eq 'votes', @displaycolumns)) {
  680. push(@displaycolumns, 'votes');
  681. }
  682. # Remove the timetracking columns if they are not a part of the group
  683. # (happens if a user had access to time tracking and it was revoked/disabled)
  684. if (!Bugzilla->user->in_group(Bugzilla->params->{"timetrackinggroup"})) {
  685. @displaycolumns = grep($_ ne 'estimated_time', @displaycolumns);
  686. @displaycolumns = grep($_ ne 'remaining_time', @displaycolumns);
  687. @displaycolumns = grep($_ ne 'actual_time', @displaycolumns);
  688. @displaycolumns = grep($_ ne 'percentage_complete', @displaycolumns);
  689. @displaycolumns = grep($_ ne 'deadline', @displaycolumns);
  690. }
  691. # Remove the relevance column if the user is not doing a fulltext search.
  692. if (grep('relevance', @displaycolumns) && !$fulltext) {
  693. @displaycolumns = grep($_ ne 'relevance', @displaycolumns);
  694. }
  695. ################################################################################
  696. # Select Column Determination
  697. ################################################################################
  698. # Generate the list of columns that will be selected in the SQL query.
  699. # The bug ID is always selected because bug IDs are always displayed.
  700. # Severity, priority, resolution and status are required for buglist
  701. # CSS classes.
  702. my @selectcolumns = ("bug_id", "bug_severity", "priority", "bug_status",
  703. "resolution");
  704. # if using classification, we also need to look in product.classification_id
  705. if (Bugzilla->params->{"useclassification"}) {
  706. push (@selectcolumns,"product");
  707. }
  708. # remaining and actual_time are required for percentage_complete calculation:
  709. if (lsearch(\@displaycolumns, "percentage_complete") >= 0) {
  710. push (@selectcolumns, "remaining_time");
  711. push (@selectcolumns, "actual_time");
  712. }
  713. # Display columns are selected because otherwise we could not display them.
  714. push (@selectcolumns, @displaycolumns);
  715. # If the user is editing multiple bugs, we also make sure to select the product
  716. # and status because the values of those fields determine what options the user
  717. # has for modifying the bugs.
  718. if ($dotweak) {
  719. push(@selectcolumns, "product") if !grep($_ eq 'product', @selectcolumns);
  720. push(@selectcolumns, "bug_status") if !grep($_ eq 'bug_status', @selectcolumns);
  721. }
  722. if ($format->{'extension'} eq 'ics') {
  723. push(@selectcolumns, "opendate") if !grep($_ eq 'opendate', @selectcolumns);
  724. }
  725. if ($format->{'extension'} eq 'atom') {
  726. # The title of the Atom feed will be the same one as for the bug list.
  727. $vars->{'title'} = $cgi->param('title');
  728. # This is the list of fields that are needed by the Atom filter.
  729. my @required_atom_columns = (
  730. 'short_desc',
  731. 'opendate',
  732. 'changeddate',
  733. 'reporter_realname',
  734. 'priority',
  735. 'bug_severity',
  736. 'assigned_to_realname',
  737. 'bug_status',
  738. 'product',
  739. 'component',
  740. 'resolution'
  741. );
  742. push(@required_atom_columns, 'target_milestone') if Bugzilla->params->{'usetargetmilestone'};
  743. foreach my $required (@required_atom_columns) {
  744. push(@selectcolumns, $required) if !grep($_ eq $required,@selectcolumns);
  745. }
  746. }
  747. ################################################################################
  748. # Query Generation
  749. ################################################################################
  750. # Convert the list of columns being selected into a list of column names.
  751. my @selectnames = map($columns->{$_}->{'name'}, @selectcolumns);
  752. # Remove columns with no names, such as percentage_complete
  753. # (or a removed *_time column due to permissions)
  754. @selectnames = grep($_ ne '', @selectnames);
  755. ################################################################################
  756. # Sort Order Determination
  757. ################################################################################
  758. # Add to the query some instructions for sorting the bug list.
  759. # First check if we'll want to reuse the last sorting order; that happens if
  760. # the order is not defined or its value is "reuse last sort"
  761. if (!$order || $order =~ /^reuse/i) {
  762. if ($cgi->cookie('LASTORDER')) {
  763. $order = $cgi->cookie('LASTORDER');
  764. # Cookies from early versions of Specific Search included this text,
  765. # which is now invalid.
  766. $order =~ s/ LIMIT 200//;
  767. $order_from_cookie = 1;
  768. }
  769. else {
  770. $order = ''; # Remove possible "reuse" identifier as unnecessary
  771. }
  772. }
  773. my $db_order = ""; # Modified version of $order for use with SQL query
  774. if ($order) {
  775. # Convert the value of the "order" form field into a list of columns
  776. # by which to sort the results.
  777. ORDER: for ($order) {
  778. /^Bug Number$/ && do {
  779. $order = "bugs.bug_id";
  780. last ORDER;
  781. };
  782. /^Importance$/ && do {
  783. $order = "bugs.priority, bugs.bug_severity";
  784. last ORDER;
  785. };
  786. /^Assignee$/ && do {
  787. $order = "map_assigned_to.login_name, bugs.bug_status, bugs.priority, bugs.bug_id";
  788. last ORDER;
  789. };
  790. /^Last Changed$/ && do {
  791. $order = "bugs.delta_ts, bugs.bug_status, bugs.priority, map_assigned_to.login_name, bugs.bug_id";
  792. last ORDER;
  793. };
  794. do {
  795. my @order;
  796. my @columnnames = map($columns->{lc($_)}->{'name'}, keys(%$columns));
  797. # A custom list of columns. Make sure each column is valid.
  798. foreach my $fragment (split(/,/, $order)) {
  799. $fragment = trim($fragment);
  800. next unless $fragment;
  801. # Accept an order fragment matching a column name, with
  802. # asc|desc optionally following (to specify the direction)
  803. if (grep($fragment =~ /^\Q$_\E(\s+(asc|desc))?$/, @columnnames, keys(%$columns))) {
  804. next if $fragment =~ /\brelevance\b/ && !$fulltext;
  805. push(@order, $fragment);
  806. }
  807. else {
  808. my $vars = { fragment => $fragment };
  809. if ($order_from_cookie) {
  810. $cgi->remove_cookie('LASTORDER');
  811. ThrowCodeError("invalid_column_name_cookie", $vars);
  812. }
  813. else {
  814. ThrowCodeError("invalid_column_name_form", $vars);
  815. }
  816. }
  817. }
  818. $order = join(",", @order);
  819. # Now that we have checked that all columns in the order are valid,
  820. # detaint the order string.
  821. trick_taint($order) if $order;
  822. };
  823. }
  824. }
  825. if (!$order) {
  826. # DEFAULT
  827. $order = "bugs.bug_status, bugs.priority, map_assigned_to.login_name, bugs.bug_id";
  828. }
  829. # Make sure ORDER BY columns are included in the field list.
  830. foreach my $fragment (split(/,/, $order)) {
  831. $fragment = trim($fragment);
  832. if (!grep($fragment =~ /^\Q$_\E(\s+(asc|desc))?$/, @selectnames)) {
  833. # Add order columns to selectnames
  834. # The fragment has already been validated
  835. $fragment =~ s/\s+(asc|desc)$//;
  836. # While newer fragments contain IDs for aliased columns, older
  837. # LASTORDER cookies (or bookmarks) may contain full names.
  838. # Convert them to an ID here.
  839. if ($fragment =~ / AS (\w+)/) {
  840. $fragment = $1;
  841. }
  842. $fragment =~ tr/a-zA-Z\.0-9\-_//cd;
  843. # If the order fragment is an ID, we need its corresponding name
  844. # to be in the field list.
  845. if (exists($columns->{$fragment})) {
  846. $fragment = $columns->{$fragment}->{'name'};
  847. }
  848. push @selectnames, $fragment;
  849. }
  850. }
  851. $db_order = $order; # Copy $order into $db_order for use with SQL query
  852. # If we are sorting by votes, sort in descending order if no explicit
  853. # sort order was given
  854. $db_order =~ s/bugs.votes\s*(,|$)/bugs.votes desc$1/i;
  855. # the 'actual_time' field is defined as an aggregate function, but
  856. # for order we just need the column name 'actual_time'
  857. my $aggregate_search = quotemeta($columns->{'actual_time'}->{'name'});
  858. $db_order =~ s/$aggregate_search/actual_time/g;
  859. # the 'percentage_complete' field is defined as an aggregate too
  860. $aggregate_search = quotemeta($columns->{'percentage_complete'}->{'name'});
  861. $db_order =~ s/$aggregate_search/percentage_complete/g;
  862. # Now put $db_order into a format that Bugzilla::Search can use.
  863. # (We create $db_order as a string first because that's the way
  864. # we did it before Bugzilla::Search took an "order" argument.)
  865. my @orderstrings = split(/,\s*/, $db_order);
  866. # Generate the basic SQL query that will be used to generate the bug list.
  867. my $search = new Bugzilla::Search('fields' => \@selectnames,
  868. 'params' => $params,
  869. 'order' => \@orderstrings);
  870. my $query = $search->getSQL();
  871. if (defined $cgi->param('limit')) {
  872. my $limit = $cgi->param('limit');
  873. if (detaint_natural($limit)) {
  874. $query .= " " . $dbh->sql_limit($limit);
  875. }
  876. }
  877. elsif ($fulltext) {
  878. $query .= " " . $dbh->sql_limit(FULLTEXT_BUGLIST_LIMIT);
  879. $vars->{'message'} = 'buglist_sorted_by_relevance' if ($cgi->param('order') =~ /^relevance/);
  880. }
  881. ################################################################################
  882. # Query Execution
  883. ################################################################################
  884. if ($cgi->param('debug')
  885. && Bugzilla->params->{debug_group}
  886. && Bugzilla->user->in_group(Bugzilla->params->{debug_group})
  887. ) {
  888. $vars->{'debug'} = 1;
  889. $vars->{'query'} = $query;
  890. $vars->{'debugdata'} = $search->getDebugData();
  891. }
  892. # Time to use server push to display an interim message to the user until
  893. # the query completes and we can display the bug list.
  894. if ($serverpush) {
  895. print $cgi->multipart_init();
  896. print $cgi->multipart_start(-type => 'text/html');
  897. # Generate and return the UI (HTML page) from the appropriate template.
  898. $template->process("list/server-push.html.tmpl", $vars)
  899. || ThrowTemplateError($template->error());
  900. # Under mod_perl, flush stdout so that the page actually shows up.
  901. if ($ENV{MOD_PERL}) {
  902. require Apache2::RequestUtil;
  903. Apache2::RequestUtil->request->rflush();
  904. }
  905. # Don't do multipart_end() until we're ready to display the replacement
  906. # page, otherwise any errors that happen before then (like SQL errors)
  907. # will result in a blank page being shown to the user instead of the error.
  908. }
  909. # Connect to the shadow database if this installation is using one to improve
  910. # query performance.
  911. $dbh = Bugzilla->switch_to_shadow_db();
  912. # Normally, we ignore SIGTERM and SIGPIPE, but we need to
  913. # respond to them here to prevent someone DOSing us by reloading a query
  914. # a large number of times.
  915. $::SIG{TERM} = 'DEFAULT';
  916. $::SIG{PIPE} = 'DEFAULT';
  917. # Execute the query.
  918. my $buglist_sth = $dbh->prepare($query);
  919. $buglist_sth->execute();
  920. ################################################################################
  921. # Results Retrieval
  922. ################################################################################
  923. # Retrieve the query results one row at a time and write the data into a list
  924. # of Perl records.
  925. my $bugowners = {};
  926. my $bugproducts = {};
  927. my $bugstatuses = {};
  928. my @bugidlist;
  929. my @bugs; # the list of records
  930. while (my @row = $buglist_sth->fetchrow_array()) {
  931. my $bug = {}; # a record
  932. # Slurp the row of data into the record.
  933. # The second from last column in the record is the number of groups
  934. # to which the bug is restricted.
  935. foreach my $column (@selectcolumns) {
  936. $bug->{$column} = shift @row;
  937. }
  938. # Process certain values further (i.e. date format conversion).
  939. if ($bug->{'changeddate'}) {
  940. $bug->{'changeddate'} =~
  941. s/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/$1-$2-$3 $4:$5:$6/;
  942. # Put in the change date as a time, so that the template date plugin
  943. # can format the date in any way needed by the template. ICS and Atom
  944. # have specific, and different, date and time formatting.
  945. $bug->{'changedtime'} = str2time($bug->{'changeddate'}, Bugzilla->params->{'timezone'});
  946. $bug->{'changeddate'} = DiffDate($bug->{'changeddate'});
  947. }
  948. if ($bug->{'opendate'}) {
  949. # Put in the open date as a time for the template date plugin.
  950. $bug->{'opentime'} = str2time($bug->{'opendate'}, Bugzilla->params->{'timezone'});
  951. $bug->{'opendate'} = DiffDate($bug->{'opendate'});
  952. }
  953. # Record the assignee, product, and status in the big hashes of those things.
  954. $bugowners->{$bug->{'assigned_to'}} = 1 if $bug->{'assigned_to'};
  955. $bugproducts->{$bug->{'product'}} = 1 if $bug->{'product'};
  956. $bugstatuses->{$bug->{'bug_status'}} = 1 if $bug->{'bug_status'};
  957. $bug->{'secure_mode'} = undef;
  958. # Add the record to the list.
  959. push(@bugs, $bug);
  960. # Add id to list for checking for bug privacy later
  961. push(@bugidlist, $bug->{'bug_id'});
  962. }
  963. # Check for bug privacy and set $bug->{'secure_mode'} to 'implied' or 'manual'
  964. # based on whether the privacy is simply product implied (by mandatory groups)
  965. # or because of human choice
  966. my %min_membercontrol;
  967. if (@bugidlist) {
  968. my $sth = $dbh->prepare(
  969. "SELECT DISTINCT bugs.bug_id, MIN(group_control_map.membercontrol) " .
  970. "FROM bugs " .
  971. "INNER JOIN bug_group_map " .
  972. "ON bugs.bug_id = bug_group_map.bug_id " .
  973. "LEFT JOIN group_control_map " .
  974. "ON group_control_map.product_id = bugs.product_id " .
  975. "AND group_control_map.group_id = bug_group_map.group_id " .
  976. "WHERE " . $dbh->sql_in('bugs.bug_id', \@bugidlist) .
  977. $dbh->sql_group_by('bugs.bug_id'));
  978. $sth->execute();
  979. while (my ($bug_id, $min_membercontrol) = $sth->fetchrow_array()) {
  980. $min_membercontrol{$bug_id} = $min_membercontrol || CONTROLMAPNA;
  981. }
  982. foreach my $bug (@bugs) {
  983. next unless defined($min_membercontrol{$bug->{'bug_id'}});
  984. if ($min_membercontrol{$bug->{'bug_id'}} == CONTROLMAPMANDATORY) {
  985. $bug->{'secure_mode'} = 'implied';
  986. }
  987. else {
  988. $bug->{'secure_mode'} = 'manual';
  989. }
  990. }
  991. }
  992. ################################################################################
  993. # Template Variable Definition
  994. ################################################################################
  995. # Define the variables and functions that will be passed to the UI template.
  996. $vars->{'bugs'} = \@bugs;
  997. $vars->{'buglist'} = \@bugidlist;
  998. $vars->{'buglist_joined'} = join(',', @bugidlist);
  999. $vars->{'columns'} = $columns;
  1000. $vars->{'displaycolumns'} = \@displaycolumns;
  1001. $vars->{'openstates'} = [BUG_STATE_OPEN];
  1002. $vars->{'closedstates'} = [map {$_->name} closed_bug_statuses()];
  1003. # The list of query fields in URL query string format, used when creating
  1004. # URLs to the same query results page with different parameters (such as
  1005. # a different sort order or when taking some action on the set of query
  1006. # results). To get this string, we call the Bugzilla::CGI::canoncalise_query
  1007. # function with a list of elements to be removed from the URL.
  1008. $vars->{'urlquerypart'} = $params->canonicalise_query('order',
  1009. 'cmdtype',
  1010. 'query_based_on');
  1011. $vars->{'order'} = $order;
  1012. $vars->{'caneditbugs'} = 1;
  1013. if (!Bugzilla->user->in_group('editbugs')) {
  1014. foreach my $product (keys %$bugproducts) {
  1015. my $prod = new Bugzilla::Product({name => $product});
  1016. if (!Bugzilla->user->in_group('editbugs', $prod->id)) {
  1017. $vars->{'caneditbugs'} = 0;
  1018. last;
  1019. }
  1020. }
  1021. }
  1022. my @bugowners = keys %$bugowners;
  1023. if (scalar(@bugowners) > 1 && Bugzilla->user->in_group('editbugs')) {
  1024. my $suffix = Bugzilla->params->{'emailsuffix'};
  1025. map(s/$/$suffix/, @bugowners) if $suffix;
  1026. my $bugowners = join(",", @bugowners);
  1027. $vars->{'bugowners'} = $bugowners;
  1028. }
  1029. # Whether or not to split the column titles across two rows to make
  1030. # the list more compact.
  1031. $vars->{'splitheader'} = $cgi->cookie('SPLITHEADER') ? 1 : 0;
  1032. $vars->{'quip'} = GetQuip();
  1033. $vars->{'currenttime'} = time();
  1034. # The following variables are used when the user is making changes to multiple bugs.
  1035. if ($dotweak && scalar @bugs) {
  1036. if (!$vars->{'caneditbugs'}) {
  1037. _close_standby_message('text/html', 'inline', $serverpush);
  1038. ThrowUserError('auth_failure', {group => 'editbugs',
  1039. action => 'modify',
  1040. object => 'multiple_bugs'});
  1041. }
  1042. $vars->{'dotweak'} = 1;
  1043. $vars->{'use_keywords'} = 1 if Bugzilla::Keyword::keyword_count();
  1044. # issue_session_token needs to write to the master DB.
  1045. Bugzilla->switch_to_main_db();
  1046. $vars->{'token'} = issue_session_token('buglist_mass_change');
  1047. Bugzilla->switch_to_shadow_db();
  1048. $vars->{'products'} = Bugzilla->user->get_enterable_products;
  1049. $vars->{'platforms'} = get_legal_field_values('rep_platform');
  1050. $vars->{'op_sys'} = get_legal_field_values('op_sys');
  1051. $vars->{'priorities'} = get_legal_field_values('priority');
  1052. $vars->{'severities'} = get_legal_field_values('bug_severity');
  1053. $vars->{'resolutions'} = get_legal_field_values('resolution');
  1054. $vars->{'unconfirmedstate'} = 'UNCONFIRMED';
  1055. # Convert bug statuses to their ID.
  1056. my @bug_statuses = map {$dbh->quote($_)} keys %$bugstatuses;
  1057. my $bug_status_ids =
  1058. $dbh->selectcol_arrayref('SELECT id FROM bug_status
  1059. WHERE ' . $dbh->sql_in('value', \@bug_statuses));
  1060. # This query collects new statuses which are common to all current bug statuses.
  1061. # It also accepts transitions where the bug status doesn't change.
  1062. $bug_status_ids =
  1063. $dbh->selectcol_arrayref(
  1064. 'SELECT DISTINCT sw1.new_status
  1065. FROM status_workflow sw1
  1066. INNER JOIN bug_status
  1067. ON bug_status.id = sw1.new_status
  1068. WHERE bug_status.isactive = 1
  1069. AND NOT EXISTS
  1070. (SELECT * FROM status_workflow sw2
  1071. WHERE sw2.old_status != sw1.new_status
  1072. AND '
  1073. . $dbh->sql_in('sw2.old_status', $bug_status_ids)
  1074. . ' AND NOT EXISTS
  1075. (SELECT * FROM status_workflow sw3
  1076. WHERE sw3.new_status = sw1.new_status
  1077. AND sw3.old_status = sw2.old_status))');
  1078. $vars->{'current_bug_statuses'} = [keys %$bugstatuses];
  1079. $vars->{'new_bug_statuses'} = Bugzilla::Status->new_from_list($bug_status_ids);
  1080. # The groups the user belongs to and which are editable for the given buglist.
  1081. my @products = keys %$bugproducts;
  1082. $vars->{'groups'} = GetGroups(\@products);
  1083. # If all bugs being changed are in the same product, the user can change
  1084. # their version and component, so generate a list of products, a list of
  1085. # versions for the product (if there is only one product on the list of
  1086. # products), and a list of components for the product.
  1087. if (scalar(@products) == 1) {
  1088. my $product = new Bugzilla::Product({name => $products[0]});
  1089. $vars->{'versions'} = [map($_->name ,@{$product->versions})];
  1090. $vars->{'components'} = [map($_->name, @{$product->components})];
  1091. $vars->{'targetmilestones'} = [map($_->name, @{$product->milestones})]
  1092. if Bugzilla->params->{'usetargetmilestone'};
  1093. }
  1094. }
  1095. # If we're editing a stored query, use the existing query name as default for
  1096. # the "Remember search as" field.
  1097. $vars->{'defaultsavename'} = $cgi->param('query_based_on');
  1098. ################################################################################
  1099. # HTTP Header Generation
  1100. ################################################################################
  1101. # Generate HTTP headers
  1102. my $contenttype;
  1103. my $disposition = "inline";
  1104. if ($format->{'extension'} eq "html" && !$agent) {
  1105. if ($order) {
  1106. $cgi->send_cookie(-name => 'LASTORDER',
  1107. -value => $order,
  1108. -expires => 'Fri, 01-Jan-2038 00:00:00 GMT');
  1109. }
  1110. my $bugids = join(":", @bugidlist);
  1111. # See also Bug 111999
  1112. if (length($bugids) == 0) {
  1113. $cgi->remove_cookie('BUGLIST');
  1114. }
  1115. elsif (length($bugids) < 4000) {
  1116. $cgi->send_cookie(-name => 'BUGLIST',
  1117. -value => $bugids,
  1118. -expires => 'Fri, 01-Jan-2038 00:00:00 GMT');
  1119. }
  1120. else {
  1121. $cgi->remove_cookie('BUGLIST');
  1122. $vars->{'toolong'} = 1;
  1123. }
  1124. $contenttype = "text/html";
  1125. }
  1126. else {
  1127. $contenttype = $format->{'ctype'};
  1128. }
  1129. if ($format->{'extension'} eq "csv") {
  1130. # We set CSV files to be downloaded, as they are designed for importing
  1131. # into other programs.
  1132. $disposition = "attachment";
  1133. }
  1134. # Suggest a name for the bug list if the user wants to save it as a file.
  1135. $disposition .= "; filename=\"$filename\"";
  1136. _close_standby_message($contenttype, $disposition, $serverpush);
  1137. ################################################################################
  1138. # Content Generation
  1139. ################################################################################
  1140. # Generate and return the UI (HTML page) from the appropriate template.
  1141. $template->process($format->{'template'}, $vars)
  1142. || ThrowTemplateError($template->error());
  1143. ################################################################################
  1144. # Script Conclusion
  1145. ################################################################################
  1146. print $cgi->multipart_final() if $serverpush;