Search.pm 73 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069
  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): Gervase Markham <gerv@gerv.net>
  21. # 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. # Michael Schindler <michael@compressconsult.com>
  27. # Max Kanat-Alexander <mkanat@bugzilla.org>
  28. # Joel Peshkin <bugreport@peshkin.net>
  29. # Lance Larsh <lance.larsh@oracle.com>
  30. # Jesse Clark <jjclark1982@gmail.com>
  31. use strict;
  32. package Bugzilla::Search;
  33. use base qw(Exporter);
  34. @Bugzilla::Search::EXPORT = qw(IsValidQueryType);
  35. use Bugzilla::Error;
  36. use Bugzilla::Util;
  37. use Bugzilla::Constants;
  38. use Bugzilla::Group;
  39. use Bugzilla::User;
  40. use Bugzilla::Field;
  41. use Bugzilla::Status;
  42. use Bugzilla::Keyword;
  43. use Date::Format;
  44. use Date::Parse;
  45. # Some fields are not sorted on themselves, but on other fields.
  46. # We need to have a list of these fields and what they map to.
  47. # Each field points to an array that contains the fields mapped
  48. # to, in order.
  49. use constant SPECIAL_ORDER => {
  50. 'bugs.target_milestone' => [ 'ms_order.sortkey','ms_order.value' ],
  51. 'bugs.bug_status' => [ 'bug_status.sortkey','bug_status.value' ],
  52. 'bugs.rep_platform' => [ 'rep_platform.sortkey','rep_platform.value' ],
  53. 'bugs.priority' => [ 'priority.sortkey','priority.value' ],
  54. 'bugs.op_sys' => [ 'op_sys.sortkey','op_sys.value' ],
  55. 'bugs.resolution' => [ 'resolution.sortkey', 'resolution.value' ],
  56. 'bugs.bug_severity' => [ 'bug_severity.sortkey','bug_severity.value' ]
  57. };
  58. # When we add certain fields to the ORDER BY, we need to then add a
  59. # table join to the FROM statement. This hash maps input fields to
  60. # the join statements that need to be added.
  61. use constant SPECIAL_ORDER_JOIN => {
  62. 'bugs.target_milestone' => 'LEFT JOIN milestones AS ms_order ON ms_order.value = bugs.target_milestone AND ms_order.product_id = bugs.product_id',
  63. 'bugs.bug_status' => 'LEFT JOIN bug_status ON bug_status.value = bugs.bug_status',
  64. 'bugs.rep_platform' => 'LEFT JOIN rep_platform ON rep_platform.value = bugs.rep_platform',
  65. 'bugs.priority' => 'LEFT JOIN priority ON priority.value = bugs.priority',
  66. 'bugs.op_sys' => 'LEFT JOIN op_sys ON op_sys.value = bugs.op_sys',
  67. 'bugs.resolution' => 'LEFT JOIN resolution ON resolution.value = bugs.resolution',
  68. 'bugs.bug_severity' => 'LEFT JOIN bug_severity ON bug_severity.value = bugs.bug_severity'
  69. };
  70. # Create a new Search
  71. # Note that the param argument may be modified by Bugzilla::Search
  72. sub new {
  73. my $invocant = shift;
  74. my $class = ref($invocant) || $invocant;
  75. my $self = { @_ };
  76. bless($self, $class);
  77. $self->init();
  78. return $self;
  79. }
  80. sub init {
  81. my $self = shift;
  82. my $fieldsref = $self->{'fields'};
  83. my $params = $self->{'params'};
  84. $self->{'user'} ||= Bugzilla->user;
  85. my $user = $self->{'user'};
  86. my $orderref = $self->{'order'} || 0;
  87. my @inputorder;
  88. @inputorder = @$orderref if $orderref;
  89. my @orderby;
  90. my $debug = 0;
  91. my @debugdata;
  92. if ($params->param('debug')) { $debug = 1; }
  93. my @fields;
  94. my @supptables;
  95. my @wherepart;
  96. my @having;
  97. my @groupby;
  98. @fields = @$fieldsref if $fieldsref;
  99. my @specialchart;
  100. my @andlist;
  101. my %chartfields;
  102. my %special_order = %{SPECIAL_ORDER()};
  103. my %special_order_join = %{SPECIAL_ORDER_JOIN()};
  104. my @select_fields = Bugzilla->get_fields({ type => FIELD_TYPE_SINGLE_SELECT,
  105. obsolete => 0 });
  106. my @multi_select_fields = Bugzilla->get_fields({ type => FIELD_TYPE_MULTI_SELECT,
  107. obsolete => 0 });
  108. foreach my $field (@select_fields) {
  109. my $name = $field->name;
  110. $special_order{"bugs.$name"} = [ "$name.sortkey", "$name.value" ],
  111. $special_order_join{"bugs.$name"} =
  112. "LEFT JOIN $name ON $name.value = bugs.$name";
  113. }
  114. my $dbh = Bugzilla->dbh;
  115. # First, deal with all the old hard-coded non-chart-based poop.
  116. if (grep(/map_assigned_to/, @$fieldsref)) {
  117. push @supptables, "INNER JOIN profiles AS map_assigned_to " .
  118. "ON bugs.assigned_to = map_assigned_to.userid";
  119. }
  120. if (grep(/map_reporter/, @$fieldsref)) {
  121. push @supptables, "INNER JOIN profiles AS map_reporter " .
  122. "ON bugs.reporter = map_reporter.userid";
  123. }
  124. if (grep(/map_qa_contact/, @$fieldsref)) {
  125. push @supptables, "LEFT JOIN profiles AS map_qa_contact " .
  126. "ON bugs.qa_contact = map_qa_contact.userid";
  127. }
  128. if (lsearch($fieldsref, 'map_products.name') >= 0) {
  129. push @supptables, "INNER JOIN products AS map_products " .
  130. "ON bugs.product_id = map_products.id";
  131. }
  132. if (lsearch($fieldsref, 'map_classifications.name') >= 0) {
  133. push @supptables, "INNER JOIN products AS map_products " .
  134. "ON bugs.product_id = map_products.id";
  135. push @supptables,
  136. "INNER JOIN classifications AS map_classifications " .
  137. "ON map_products.classification_id = map_classifications.id";
  138. }
  139. if (lsearch($fieldsref, 'map_components.name') >= 0) {
  140. push @supptables, "INNER JOIN components AS map_components " .
  141. "ON bugs.component_id = map_components.id";
  142. }
  143. if (grep($_ =~/AS (actual_time|percentage_complete)$/, @$fieldsref)) {
  144. push(@supptables, "LEFT JOIN longdescs AS ldtime " .
  145. "ON ldtime.bug_id = bugs.bug_id");
  146. }
  147. my $minvotes;
  148. if (defined $params->param('votes')) {
  149. my $c = trim($params->param('votes'));
  150. if ($c ne "") {
  151. if ($c !~ /^[0-9]*$/) {
  152. ThrowUserError("illegal_at_least_x_votes",
  153. { value => $c });
  154. }
  155. push(@specialchart, ["votes", "greaterthan", $c - 1]);
  156. }
  157. }
  158. if ($params->param('bug_id')) {
  159. my $type = "anyexact";
  160. if ($params->param('bugidtype') && $params->param('bugidtype') eq 'exclude') {
  161. $type = "nowords";
  162. }
  163. push(@specialchart, ["bug_id", $type, join(',', $params->param('bug_id'))]);
  164. }
  165. # If the user has selected all of either status or resolution, change to
  166. # selecting none. This is functionally equivalent, but quite a lot faster.
  167. # Also, if the status is __open__ or __closed__, translate those
  168. # into their equivalent lists of open and closed statuses.
  169. if ($params->param('bug_status')) {
  170. my @bug_statuses = $params->param('bug_status');
  171. my @legal_statuses = @{get_legal_field_values('bug_status')};
  172. if (scalar(@bug_statuses) == scalar(@legal_statuses)
  173. || $bug_statuses[0] eq "__all__")
  174. {
  175. $params->delete('bug_status');
  176. }
  177. elsif ($bug_statuses[0] eq '__open__') {
  178. $params->param('bug_status', grep(is_open_state($_),
  179. @legal_statuses));
  180. }
  181. elsif ($bug_statuses[0] eq "__closed__") {
  182. $params->param('bug_status', grep(!is_open_state($_),
  183. @legal_statuses));
  184. }
  185. }
  186. if ($params->param('resolution')) {
  187. my @resolutions = $params->param('resolution');
  188. my $legal_resolutions = get_legal_field_values('resolution');
  189. if (scalar(@resolutions) == scalar(@$legal_resolutions)) {
  190. $params->delete('resolution');
  191. }
  192. }
  193. my @legal_fields = ("product", "version", "rep_platform", "op_sys",
  194. "bug_status", "resolution", "priority", "bug_severity",
  195. "assigned_to", "reporter", "component", "classification",
  196. "target_milestone", "bug_group");
  197. # Include custom select fields.
  198. push(@legal_fields, map { $_->name } @select_fields);
  199. push(@legal_fields, map { $_->name } @multi_select_fields);
  200. foreach my $field ($params->param()) {
  201. if (lsearch(\@legal_fields, $field) != -1) {
  202. push(@specialchart, [$field, "anyexact",
  203. join(',', $params->param($field))]);
  204. }
  205. }
  206. if ($params->param('keywords')) {
  207. my $t = $params->param('keywords_type');
  208. if (!$t || $t eq "or") {
  209. $t = "anywords";
  210. }
  211. push(@specialchart, ["keywords", $t, $params->param('keywords')]);
  212. }
  213. foreach my $id ("1", "2") {
  214. if (!defined ($params->param("email$id"))) {
  215. next;
  216. }
  217. my $email = trim($params->param("email$id"));
  218. if ($email eq "") {
  219. next;
  220. }
  221. my $type = $params->param("emailtype$id");
  222. if ($type eq "exact") {
  223. $type = "anyexact";
  224. foreach my $name (split(',', $email)) {
  225. $name = trim($name);
  226. if ($name) {
  227. login_to_id($name, THROW_ERROR);
  228. }
  229. }
  230. }
  231. my @clist;
  232. foreach my $field ("assigned_to", "reporter", "cc", "qa_contact") {
  233. if ($params->param("email$field$id")) {
  234. push(@clist, $field, $type, $email);
  235. }
  236. }
  237. if ($params->param("emaillongdesc$id")) {
  238. push(@clist, "commenter", $type, $email);
  239. }
  240. if (@clist) {
  241. push(@specialchart, \@clist);
  242. } else {
  243. ThrowUserError("missing_email_type",
  244. { email => $email });
  245. }
  246. }
  247. my $chfieldfrom = trim(lc($params->param('chfieldfrom'))) || '';
  248. my $chfieldto = trim(lc($params->param('chfieldto'))) || '';
  249. $chfieldfrom = '' if ($chfieldfrom eq 'now');
  250. $chfieldto = '' if ($chfieldto eq 'now');
  251. my @chfield = $params->param('chfield');
  252. my $chvalue = trim($params->param('chfieldvalue')) || '';
  253. # 2003-05-20: The 'changedin' field is no longer in the UI, but we continue
  254. # to process it because it will appear in stored queries and bookmarks.
  255. my $changedin = trim($params->param('changedin')) || '';
  256. if ($changedin) {
  257. if ($changedin !~ /^[0-9]*$/) {
  258. ThrowUserError("illegal_changed_in_last_x_days",
  259. { value => $changedin });
  260. }
  261. if (!$chfieldfrom
  262. && !$chfieldto
  263. && scalar(@chfield) == 1
  264. && $chfield[0] eq "[Bug creation]")
  265. {
  266. # Deal with the special case where the query is using changedin
  267. # to get bugs created in the last n days by converting the value
  268. # into its equivalent for the chfieldfrom parameter.
  269. $chfieldfrom = "-" . ($changedin - 1) . "d";
  270. }
  271. else {
  272. # Oh boy, the general case. Who knows why the user included
  273. # the changedin parameter, but do our best to comply.
  274. push(@specialchart, ["changedin", "lessthan", $changedin + 1]);
  275. }
  276. }
  277. if ($chfieldfrom ne '' || $chfieldto ne '') {
  278. my $sql_chfrom = $chfieldfrom ? $dbh->quote(SqlifyDate($chfieldfrom)):'';
  279. my $sql_chto = $chfieldto ? $dbh->quote(SqlifyDate($chfieldto)) :'';
  280. my $sql_chvalue = $chvalue ne '' ? $dbh->quote($chvalue) : '';
  281. trick_taint($sql_chvalue);
  282. if(!@chfield) {
  283. push(@wherepart, "bugs.delta_ts >= $sql_chfrom") if ($sql_chfrom);
  284. push(@wherepart, "bugs.delta_ts <= $sql_chto") if ($sql_chto);
  285. } else {
  286. my $bug_creation_clause;
  287. my @list;
  288. my @actlist;
  289. foreach my $f (@chfield) {
  290. if ($f eq "[Bug creation]") {
  291. # Treat [Bug creation] differently because we need to look
  292. # at bugs.creation_ts rather than the bugs_activity table.
  293. my @l;
  294. push(@l, "bugs.creation_ts >= $sql_chfrom") if($sql_chfrom);
  295. push(@l, "bugs.creation_ts <= $sql_chto") if($sql_chto);
  296. $bug_creation_clause = "(" . join(' AND ', @l) . ")";
  297. } else {
  298. push(@actlist, get_field_id($f));
  299. }
  300. }
  301. # @actlist won't have any elements if the only field being searched
  302. # is [Bug creation] (in which case we don't need bugs_activity).
  303. if(@actlist) {
  304. my $extra = " actcheck.bug_id = bugs.bug_id";
  305. push(@list, "(actcheck.bug_when IS NOT NULL)");
  306. if($sql_chfrom) {
  307. $extra .= " AND actcheck.bug_when >= $sql_chfrom";
  308. }
  309. if($sql_chto) {
  310. $extra .= " AND actcheck.bug_when <= $sql_chto";
  311. }
  312. if($sql_chvalue) {
  313. $extra .= " AND actcheck.added = $sql_chvalue";
  314. }
  315. push(@supptables, "LEFT JOIN bugs_activity AS actcheck " .
  316. "ON $extra AND "
  317. . $dbh->sql_in('actcheck.fieldid', \@actlist));
  318. }
  319. # Now that we're done using @list to determine if there are any
  320. # regular fields to search (and thus we need bugs_activity),
  321. # add the [Bug creation] criterion to the list so we can OR it
  322. # together with the others.
  323. push(@list, $bug_creation_clause) if $bug_creation_clause;
  324. push(@wherepart, "(" . join(" OR ", @list) . ")");
  325. }
  326. }
  327. my $sql_deadlinefrom;
  328. my $sql_deadlineto;
  329. if ($user->in_group(Bugzilla->params->{'timetrackinggroup'})) {
  330. my $deadlinefrom;
  331. my $deadlineto;
  332. if ($params->param('deadlinefrom')){
  333. $deadlinefrom = $params->param('deadlinefrom');
  334. validate_date($deadlinefrom)
  335. || ThrowUserError('illegal_date', {date => $deadlinefrom,
  336. format => 'YYYY-MM-DD'});
  337. $sql_deadlinefrom = $dbh->quote($deadlinefrom);
  338. trick_taint($sql_deadlinefrom);
  339. push(@wherepart, "bugs.deadline >= $sql_deadlinefrom");
  340. }
  341. if ($params->param('deadlineto')){
  342. $deadlineto = $params->param('deadlineto');
  343. validate_date($deadlineto)
  344. || ThrowUserError('illegal_date', {date => $deadlineto,
  345. format => 'YYYY-MM-DD'});
  346. $sql_deadlineto = $dbh->quote($deadlineto);
  347. trick_taint($sql_deadlineto);
  348. push(@wherepart, "bugs.deadline <= $sql_deadlineto");
  349. }
  350. }
  351. foreach my $f ("short_desc", "long_desc", "bug_file_loc",
  352. "status_whiteboard") {
  353. if (defined $params->param($f)) {
  354. my $s = trim($params->param($f));
  355. if ($s ne "") {
  356. my $n = $f;
  357. my $q = $dbh->quote($s);
  358. trick_taint($q);
  359. my $type = $params->param($f . "_type");
  360. push(@specialchart, [$f, $type, $s]);
  361. }
  362. }
  363. }
  364. if (defined $params->param('content')) {
  365. push(@specialchart, ['content', 'matches', $params->param('content')]);
  366. }
  367. my $multi_fields = join('|', map($_->name, @multi_select_fields));
  368. my $chartid;
  369. my $sequence = 0;
  370. # $type_id is used by the code that queries for attachment flags.
  371. my $type_id = 0;
  372. my $f;
  373. my $ff;
  374. my $t;
  375. my $q;
  376. my $v;
  377. my $term;
  378. my %funcsbykey;
  379. my %func_args = (
  380. 'chartid' => \$chartid,
  381. 'sequence' => \$sequence,
  382. 'f' => \$f,
  383. 'ff' => \$ff,
  384. 't' => \$t,
  385. 'v' => \$v,
  386. 'q' => \$q,
  387. 'term' => \$term,
  388. 'funcsbykey' => \%funcsbykey,
  389. 'supptables' => \@supptables,
  390. 'wherepart' => \@wherepart,
  391. 'having' => \@having,
  392. 'groupby' => \@groupby,
  393. 'chartfields' => \%chartfields,
  394. 'fields' => \@fields,
  395. );
  396. my @funcdefs = (
  397. "^(?:assigned_to|reporter|qa_contact),(?:notequals|equals|anyexact),%group\\.([^%]+)%" => \&_contact_exact_group,
  398. "^(?:assigned_to|reporter|qa_contact),(?:equals|anyexact),(%\\w+%)" => \&_contact_exact,
  399. "^(?:assigned_to|reporter|qa_contact),(?:notequals),(%\\w+%)" => \&_contact_notequals,
  400. "^(assigned_to|reporter),(?!changed)" => \&_assigned_to_reporter_nonchanged,
  401. "^qa_contact,(?!changed)" => \&_qa_contact_nonchanged,
  402. "^(?:cc),(?:notequals|equals|anyexact),%group\\.([^%]+)%" => \&_cc_exact_group,
  403. "^cc,(?:equals|anyexact),(%\\w+%)" => \&_cc_exact,
  404. "^cc,(?:notequals),(%\\w+%)" => \&_cc_notequals,
  405. "^cc,(?!changed)" => \&_cc_nonchanged,
  406. "^long_?desc,changedby" => \&_long_desc_changedby,
  407. "^long_?desc,changedbefore" => \&_long_desc_changedbefore_after,
  408. "^long_?desc,changedafter" => \&_long_desc_changedbefore_after,
  409. "^content,matches" => \&_content_matches,
  410. "^content," => sub { ThrowUserError("search_content_without_matches"); },
  411. "^(?:deadline|creation_ts|delta_ts),(?:lessthan|greaterthan|equals|notequals),(?:-|\\+)?(?:\\d+)(?:[dDwWmMyY])\$" => \&_timestamp_compare,
  412. "^commenter,(?:equals|anyexact),(%\\w+%)" => \&_commenter_exact,
  413. "^commenter," => \&_commenter,
  414. "^long_?desc," => \&_long_desc,
  415. "^longdescs\.isprivate," => \&_longdescs_isprivate,
  416. "^work_time,changedby" => \&_work_time_changedby,
  417. "^work_time,changedbefore" => \&_work_time_changedbefore_after,
  418. "^work_time,changedafter" => \&_work_time_changedbefore_after,
  419. "^work_time," => \&_work_time,
  420. "^percentage_complete," => \&_percentage_complete,
  421. "^bug_group,(?!changed)" => \&_bug_group_nonchanged,
  422. "^attach_data\.thedata,changed" => \&_attach_data_thedata_changed,
  423. "^attach_data\.thedata," => \&_attach_data_thedata,
  424. "^attachments\.submitter," => \&_attachments_submitter,
  425. "^attachments\..*," => \&_attachments,
  426. "^flagtypes.name," => \&_flagtypes_name,
  427. "^requestees.login_name," => \&_requestees_login_name,
  428. "^setters.login_name," => \&_setters_login_name,
  429. "^(changedin|days_elapsed)," => \&_changedin_days_elapsed,
  430. "^component,(?!changed)" => \&_component_nonchanged,
  431. "^product,(?!changed)" => \&_product_nonchanged,
  432. "^classification,(?!changed)" => \&_classification_nonchanged,
  433. "^keywords,(?!changed)" => \&_keywords_nonchanged,
  434. "^dependson,(?!changed)" => \&_dependson_nonchanged,
  435. "^blocked,(?!changed)" => \&_blocked_nonchanged,
  436. "^alias,(?!changed)" => \&_alias_nonchanged,
  437. "^owner_idle_time,(greaterthan|lessthan)" => \&_owner_idle_time_greater_less,
  438. "^($multi_fields),(?:notequals|notregexp|notsubstring|nowords|nowordssubstr)" => \&_multiselect_negative,
  439. "^($multi_fields),(?:allwords|allwordssubstr|anyexact)" => \&_multiselect_multiple,
  440. "^($multi_fields),(?!changed)" => \&_multiselect_nonchanged,
  441. ",equals" => \&_equals,
  442. ",notequals" => \&_notequals,
  443. ",casesubstring" => \&_casesubstring,
  444. ",substring" => \&_substring,
  445. ",substr" => \&_substring,
  446. ",notsubstring" => \&_notsubstring,
  447. ",regexp" => \&_regexp,
  448. ",notregexp" => \&_notregexp,
  449. ",lessthan" => \&_lessthan,
  450. ",matches" => sub { ThrowUserError("search_content_without_matches"); },
  451. ",greaterthan" => \&_greaterthan,
  452. ",anyexact" => \&_anyexact,
  453. ",anywordssubstr" => \&_anywordsubstr,
  454. ",allwordssubstr" => \&_allwordssubstr,
  455. ",nowordssubstr" => \&_nowordssubstr,
  456. ",anywords" => \&_anywords,
  457. ",allwords" => \&_allwords,
  458. ",nowords" => \&_nowords,
  459. ",(changedbefore|changedafter)" => \&_changedbefore_changedafter,
  460. ",(changedfrom|changedto)" => \&_changedfrom_changedto,
  461. ",changedby" => \&_changedby,
  462. );
  463. my @funcnames;
  464. while (@funcdefs) {
  465. my $key = shift(@funcdefs);
  466. my $value = shift(@funcdefs);
  467. if ($key =~ /^[^,]*$/) {
  468. die "All defs in %funcs must have a comma in their name: $key";
  469. }
  470. if (exists $funcsbykey{$key}) {
  471. die "Duplicate key in %funcs: $key";
  472. }
  473. $funcsbykey{$key} = $value;
  474. push(@funcnames, $key);
  475. }
  476. # first we delete any sign of "Chart #-1" from the HTML form hash
  477. # since we want to guarantee the user didn't hide something here
  478. my @badcharts = grep /^(field|type|value)-1-/, $params->param();
  479. foreach my $field (@badcharts) {
  480. $params->delete($field);
  481. }
  482. # now we take our special chart and stuff it into the form hash
  483. my $chart = -1;
  484. my $row = 0;
  485. foreach my $ref (@specialchart) {
  486. my $col = 0;
  487. while (@$ref) {
  488. $params->param("field$chart-$row-$col", shift(@$ref));
  489. $params->param("type$chart-$row-$col", shift(@$ref));
  490. $params->param("value$chart-$row-$col", shift(@$ref));
  491. if ($debug) {
  492. push(@debugdata, "$row-$col = " .
  493. $params->param("field$chart-$row-$col") . ' | ' .
  494. $params->param("type$chart-$row-$col") . ' | ' .
  495. $params->param("value$chart-$row-$col") . ' *');
  496. }
  497. $col++;
  498. }
  499. $row++;
  500. }
  501. # A boolean chart is a way of representing the terms in a logical
  502. # expression. Bugzilla builds SQL queries depending on how you enter
  503. # terms into the boolean chart. Boolean charts are represented in
  504. # urls as tree-tuples of (chart id, row, column). The query form
  505. # (query.cgi) may contain an arbitrary number of boolean charts where
  506. # each chart represents a clause in a SQL query.
  507. #
  508. # The query form starts out with one boolean chart containing one
  509. # row and one column. Extra rows can be created by pressing the
  510. # AND button at the bottom of the chart. Extra columns are created
  511. # by pressing the OR button at the right end of the chart. Extra
  512. # charts are created by pressing "Add another boolean chart".
  513. #
  514. # Each chart consists of an arbitrary number of rows and columns.
  515. # The terms within a row are ORed together. The expressions represented
  516. # by each row are ANDed together. The expressions represented by each
  517. # chart are ANDed together.
  518. #
  519. # ----------------------
  520. # | col2 | col2 | col3 |
  521. # --------------|------|------|
  522. # | row1 | a1 | a2 | |
  523. # |------|------|------|------| => ((a1 OR a2) AND (b1 OR b2 OR b3) AND (c1))
  524. # | row2 | b1 | b2 | b3 |
  525. # |------|------|------|------|
  526. # | row3 | c1 | | |
  527. # -----------------------------
  528. #
  529. # --------
  530. # | col2 |
  531. # --------------|
  532. # | row1 | d1 | => (d1)
  533. # ---------------
  534. #
  535. # Together, these two charts represent a SQL expression like this
  536. # SELECT blah FROM blah WHERE ( (a1 OR a2)AND(b1 OR b2 OR b3)AND(c1)) AND (d1)
  537. #
  538. # The terms within a single row of a boolean chart are all constraints
  539. # on a single piece of data. If you're looking for a bug that has two
  540. # different people cc'd on it, then you need to use two boolean charts.
  541. # This will find bugs with one CC matching 'foo@blah.org' and and another
  542. # CC matching 'bar@blah.org'.
  543. #
  544. # --------------------------------------------------------------
  545. # CC | equal to
  546. # foo@blah.org
  547. # --------------------------------------------------------------
  548. # CC | equal to
  549. # bar@blah.org
  550. #
  551. # If you try to do this query by pressing the AND button in the
  552. # original boolean chart then what you'll get is an expression that
  553. # looks for a single CC where the login name is both "foo@blah.org",
  554. # and "bar@blah.org". This is impossible.
  555. #
  556. # --------------------------------------------------------------
  557. # CC | equal to
  558. # foo@blah.org
  559. # AND
  560. # CC | equal to
  561. # bar@blah.org
  562. # --------------------------------------------------------------
  563. # $chartid is the number of the current chart whose SQL we're constructing
  564. # $row is the current row of the current chart
  565. # names for table aliases are constructed using $chartid and $row
  566. # SELECT blah FROM $table "$table_$chartid_$row" WHERE ....
  567. # $f = field of table in bug db (e.g. bug_id, reporter, etc)
  568. # $ff = qualified field name (field name prefixed by table)
  569. # e.g. bugs_activity.bug_id
  570. # $t = type of query. e.g. "equal to", "changed after", case sensitive substr"
  571. # $v = value - value the user typed in to the form
  572. # $q = sanitized version of user input trick_taint(($dbh->quote($v)))
  573. # @supptables = Tables and/or table aliases used in query
  574. # %suppseen = A hash used to store all the tables in supptables to weed
  575. # out duplicates.
  576. # @supplist = A list used to accumulate all the JOIN clauses for each
  577. # chart to merge the ON sections of each.
  578. # $suppstring = String which is pasted into query containing all table names
  579. # get a list of field names to verify the user-submitted chart fields against
  580. %chartfields = @{$dbh->selectcol_arrayref(
  581. q{SELECT name, id FROM fielddefs}, { Columns=>[1,2] })};
  582. $row = 0;
  583. for ($chart=-1 ;
  584. $chart < 0 || $params->param("field$chart-0-0") ;
  585. $chart++) {
  586. $chartid = $chart >= 0 ? $chart : "";
  587. my @chartandlist = ();
  588. for ($row = 0 ;
  589. $params->param("field$chart-$row-0") ;
  590. $row++) {
  591. my @orlist;
  592. for (my $col = 0 ;
  593. $params->param("field$chart-$row-$col") ;
  594. $col++) {
  595. $f = $params->param("field$chart-$row-$col") || "noop";
  596. $t = $params->param("type$chart-$row-$col") || "noop";
  597. $v = $params->param("value$chart-$row-$col");
  598. $v = "" if !defined $v;
  599. $v = trim($v);
  600. if ($f eq "noop" || $t eq "noop" || $v eq "") {
  601. next;
  602. }
  603. # chart -1 is generated by other code above, not from the user-
  604. # submitted form, so we'll blindly accept any values in chart -1
  605. if ((!$chartfields{$f}) && ($chart != -1)) {
  606. ThrowCodeError("invalid_field_name", {field => $f});
  607. }
  608. # This is either from the internal chart (in which case we
  609. # already know about it), or it was in %chartfields, so it is
  610. # a valid field name, which means that it's ok.
  611. trick_taint($f);
  612. $q = $dbh->quote($v);
  613. trick_taint($q);
  614. my $rhs = $v;
  615. $rhs =~ tr/,//;
  616. my $func;
  617. $term = undef;
  618. foreach my $key (@funcnames) {
  619. if ("$f,$t,$rhs" =~ m/$key/) {
  620. my $ref = $funcsbykey{$key};
  621. if ($debug) {
  622. push(@debugdata, "$key ($f / $t / $rhs) =>");
  623. }
  624. $ff = $f;
  625. if ($f !~ /\./) {
  626. $ff = "bugs.$f";
  627. }
  628. $self->$ref(%func_args);
  629. if ($debug) {
  630. push(@debugdata, "$f / $t / $v / " .
  631. ($term || "undef") . " *");
  632. }
  633. if ($term) {
  634. last;
  635. }
  636. }
  637. }
  638. if ($term) {
  639. push(@orlist, $term);
  640. }
  641. else {
  642. # This field and this type don't work together.
  643. ThrowCodeError("field_type_mismatch",
  644. { field => $params->param("field$chart-$row-$col"),
  645. type => $params->param("type$chart-$row-$col"),
  646. });
  647. }
  648. }
  649. if (@orlist) {
  650. @orlist = map("($_)", @orlist) if (scalar(@orlist) > 1);
  651. push(@chartandlist, "(" . join(" OR ", @orlist) . ")");
  652. }
  653. }
  654. if (@chartandlist) {
  655. if ($params->param("negate$chart")) {
  656. push(@andlist, "NOT(" . join(" AND ", @chartandlist) . ")");
  657. } else {
  658. push(@andlist, "(" . join(" AND ", @chartandlist) . ")");
  659. }
  660. }
  661. }
  662. # The ORDER BY clause goes last, but can require modifications
  663. # to other parts of the query, so we want to create it before we
  664. # write the FROM clause.
  665. foreach my $orderitem (@inputorder) {
  666. # Some fields have 'AS' aliases. The aliases go in the ORDER BY,
  667. # not the whole fields.
  668. # XXX - Ideally, we would get just the aliases in @inputorder,
  669. # and we'd never have to deal with this.
  670. if ($orderitem =~ /\s+AS\s+(.+)$/i) {
  671. $orderitem = $1;
  672. }
  673. BuildOrderBy(\%special_order, $orderitem, \@orderby);
  674. }
  675. # Now JOIN the correct tables in the FROM clause.
  676. # This is done separately from the above because it's
  677. # cleaner to do it this way.
  678. foreach my $orderitem (@inputorder) {
  679. # Grab the part without ASC or DESC.
  680. my @splitfield = split(/\s+/, $orderitem);
  681. if ($special_order_join{$splitfield[0]}) {
  682. push(@supptables, $special_order_join{$splitfield[0]});
  683. }
  684. }
  685. my %suppseen = ("bugs" => 1);
  686. my $suppstring = "bugs";
  687. my @supplist = (" ");
  688. foreach my $str (@supptables) {
  689. if ($str =~ /^(LEFT|INNER|RIGHT)\s+JOIN/i) {
  690. $str =~ /^(.*?)\s+ON\s+(.*)$/i;
  691. my ($leftside, $rightside) = ($1, $2);
  692. if (defined $suppseen{$leftside}) {
  693. $supplist[$suppseen{$leftside}] .= " AND ($rightside)";
  694. } else {
  695. $suppseen{$leftside} = scalar @supplist;
  696. push @supplist, " $leftside ON ($rightside)";
  697. }
  698. } else {
  699. # Do not accept implicit joins using comma operator
  700. # as they are not DB agnostic
  701. ThrowCodeError("comma_operator_deprecated");
  702. }
  703. }
  704. $suppstring .= join('', @supplist);
  705. # Make sure we create a legal SQL query.
  706. @andlist = ("1 = 1") if !@andlist;
  707. my $query = "SELECT " . join(', ', @fields) .
  708. " FROM $suppstring" .
  709. " LEFT JOIN bug_group_map " .
  710. " ON bug_group_map.bug_id = bugs.bug_id ";
  711. if ($user->id) {
  712. if (%{$user->groups}) {
  713. $query .= " AND bug_group_map.group_id NOT IN (" . join(',', values(%{$user->groups})) . ") ";
  714. }
  715. $query .= " LEFT JOIN cc ON cc.bug_id = bugs.bug_id AND cc.who = " . $user->id;
  716. }
  717. $query .= " WHERE " . join(' AND ', (@wherepart, @andlist)) .
  718. " AND bugs.creation_ts IS NOT NULL AND ((bug_group_map.group_id IS NULL)";
  719. if ($user->id) {
  720. my $userid = $user->id;
  721. $query .= " OR (bugs.reporter_accessible = 1 AND bugs.reporter = $userid) " .
  722. " OR (bugs.cclist_accessible = 1 AND cc.who IS NOT NULL) " .
  723. " OR (bugs.assigned_to = $userid) ";
  724. if (Bugzilla->params->{'useqacontact'}) {
  725. $query .= "OR (bugs.qa_contact = $userid) ";
  726. }
  727. }
  728. foreach my $field (@fields, @orderby) {
  729. next if ($field =~ /(AVG|SUM|COUNT|MAX|MIN|VARIANCE)\s*\(/i ||
  730. $field =~ /^\d+$/ || $field eq "bugs.bug_id" ||
  731. $field =~ /^(relevance|actual_time|percentage_complete)/);
  732. # The structure of fields is of the form:
  733. # [foo AS] {bar | bar.baz} [ASC | DESC]
  734. # Only the mandatory part bar OR bar.baz is of interest.
  735. # But for Oracle, it needs the real name part instead.
  736. my $regexp = $dbh->GROUPBY_REGEXP;
  737. if ($field =~ /$regexp/i) {
  738. push(@groupby, $1) if !grep($_ eq $1, @groupby);
  739. }
  740. }
  741. $query .= ") " . $dbh->sql_group_by("bugs.bug_id", join(', ', @groupby));
  742. if (@having) {
  743. $query .= " HAVING " . join(" AND ", @having);
  744. }
  745. if (@orderby) {
  746. $query .= " ORDER BY " . join(',', @orderby);
  747. }
  748. $self->{'sql'} = $query;
  749. $self->{'debugdata'} = \@debugdata;
  750. }
  751. ###############################################################################
  752. # Helper functions for the init() method.
  753. ###############################################################################
  754. sub SqlifyDate {
  755. my ($str) = @_;
  756. $str = "" if !defined $str;
  757. if ($str eq "") {
  758. my ($sec, $min, $hour, $mday, $month, $year, $wday) = localtime(time());
  759. return sprintf("%4d-%02d-%02d 00:00:00", $year+1900, $month+1, $mday);
  760. }
  761. if ($str =~ /^(-|\+)?(\d+)([hHdDwWmMyY])$/) { # relative date
  762. my ($sign, $amount, $unit, $date) = ($1, $2, lc $3, time);
  763. my ($sec, $min, $hour, $mday, $month, $year, $wday) = localtime($date);
  764. if ($sign && $sign eq '+') { $amount = -$amount; }
  765. if ($unit eq 'w') { # convert weeks to days
  766. $amount = 7*$amount + $wday;
  767. $unit = 'd';
  768. }
  769. if ($unit eq 'd') {
  770. $date -= $sec + 60*$min + 3600*$hour + 24*3600*$amount;
  771. return time2str("%Y-%m-%d %H:%M:%S", $date);
  772. }
  773. elsif ($unit eq 'y') {
  774. return sprintf("%4d-01-01 00:00:00", $year+1900-$amount);
  775. }
  776. elsif ($unit eq 'm') {
  777. $month -= $amount;
  778. while ($month<0) { $year--; $month += 12; }
  779. return sprintf("%4d-%02d-01 00:00:00", $year+1900, $month+1);
  780. }
  781. elsif ($unit eq 'h') {
  782. # Special case 0h for 'beginning of this hour'
  783. if ($amount == 0) {
  784. $date -= $sec + 60*$min;
  785. } else {
  786. $date -= 3600*$amount;
  787. }
  788. return time2str("%Y-%m-%d %H:%M:%S", $date);
  789. }
  790. return undef; # should not happen due to regexp at top
  791. }
  792. my $date = str2time($str);
  793. if (!defined($date)) {
  794. ThrowUserError("illegal_date", { date => $str });
  795. }
  796. return time2str("%Y-%m-%d %H:%M:%S", $date);
  797. }
  798. sub build_subselect {
  799. my ($outer, $inner, $table, $cond) = @_;
  800. my $q = "SELECT $inner FROM $table WHERE $cond";
  801. #return "$outer IN ($q)";
  802. my $dbh = Bugzilla->dbh;
  803. my $list = $dbh->selectcol_arrayref($q);
  804. return "1=2" unless @$list; # Could use boolean type on dbs which support it
  805. return $dbh->sql_in($outer, $list);}
  806. sub GetByWordList {
  807. my ($field, $strs) = (@_);
  808. my @list;
  809. my $dbh = Bugzilla->dbh;
  810. foreach my $w (split(/[\s,]+/, $strs)) {
  811. my $word = $w;
  812. if ($word ne "") {
  813. $word =~ tr/A-Z/a-z/;
  814. $word = $dbh->quote('(^|[^a-z0-9])' . quotemeta($word) . '($|[^a-z0-9])');
  815. trick_taint($word);
  816. push(@list, $dbh->sql_regexp($field, $word));
  817. }
  818. }
  819. return \@list;
  820. }
  821. # Support for "any/all/nowordssubstr" comparison type ("words as substrings")
  822. sub GetByWordListSubstr {
  823. my ($field, $strs) = (@_);
  824. my @list;
  825. my $dbh = Bugzilla->dbh;
  826. my $sql_word;
  827. foreach my $word (split(/[\s,]+/, $strs)) {
  828. if ($word ne "") {
  829. $sql_word = $dbh->quote($word);
  830. trick_taint($sql_word);
  831. push(@list, $dbh->sql_iposition($sql_word, $field) . " > 0");
  832. }
  833. }
  834. return \@list;
  835. }
  836. sub getSQL {
  837. my $self = shift;
  838. return $self->{'sql'};
  839. }
  840. sub getDebugData {
  841. my $self = shift;
  842. return $self->{'debugdata'};
  843. }
  844. sub pronoun {
  845. my ($noun, $user) = (@_);
  846. if ($noun eq "%user%") {
  847. if ($user->id) {
  848. return $user->id;
  849. } else {
  850. ThrowUserError('login_required_for_pronoun');
  851. }
  852. }
  853. if ($noun eq "%reporter%") {
  854. return "bugs.reporter";
  855. }
  856. if ($noun eq "%assignee%") {
  857. return "bugs.assigned_to";
  858. }
  859. if ($noun eq "%qacontact%") {
  860. return "bugs.qa_contact";
  861. }
  862. return 0;
  863. }
  864. # Validate that the query type is one we can deal with
  865. sub IsValidQueryType
  866. {
  867. my ($queryType) = @_;
  868. if (grep { $_ eq $queryType } qw(specific advanced)) {
  869. return 1;
  870. }
  871. return 0;
  872. }
  873. # BuildOrderBy - Private Subroutine
  874. # This function converts the input order to an "output" order,
  875. # suitable for concatenation to form an ORDER BY clause. Basically,
  876. # it just handles fields that have non-standard sort orders from
  877. # %specialorder.
  878. # Arguments:
  879. # $orderitem - A string. The next value to append to the ORDER BY clause,
  880. # in the format of an item in the 'order' parameter to
  881. # Bugzilla::Search.
  882. # $stringlist - A reference to the list of strings that will be join()'ed
  883. # to make ORDER BY. This is what the subroutine modifies.
  884. # $reverseorder - (Optional) A boolean. TRUE if we should reverse the order
  885. # of the field that we are given (from ASC to DESC or vice-versa).
  886. #
  887. # Explanation of $reverseorder
  888. # ----------------------------
  889. # The role of $reverseorder is to handle things like sorting by
  890. # "target_milestone DESC".
  891. # Let's say that we had a field "A" that normally translates to a sort
  892. # order of "B ASC, C DESC". If we sort by "A DESC", what we really then
  893. # mean is "B DESC, C ASC". So $reverseorder is only used if we call
  894. # BuildOrderBy recursively, to let it know that we're "reversing" the
  895. # order. That is, that we wanted "A DESC", not "A".
  896. sub BuildOrderBy {
  897. my ($special_order, $orderitem, $stringlist, $reverseorder) = (@_);
  898. my @twopart = split(/\s+/, $orderitem);
  899. my $orderfield = $twopart[0];
  900. my $orderdirection = $twopart[1] || "";
  901. if ($reverseorder) {
  902. # If orderdirection is empty or ASC...
  903. if (!$orderdirection || $orderdirection =~ m/asc/i) {
  904. $orderdirection = "DESC";
  905. } else {
  906. # This has the minor side-effect of making any reversed invalid
  907. # direction into ASC.
  908. $orderdirection = "ASC";
  909. }
  910. }
  911. # Handle fields that have non-standard sort orders, from $specialorder.
  912. if ($special_order->{$orderfield}) {
  913. foreach my $subitem (@{$special_order->{$orderfield}}) {
  914. # DESC on a field with non-standard sort order means
  915. # "reverse the normal order for each field that we map to."
  916. BuildOrderBy($special_order, $subitem, $stringlist,
  917. $orderdirection =~ m/desc/i);
  918. }
  919. return;
  920. }
  921. push(@$stringlist, trim($orderfield . ' ' . $orderdirection));
  922. }
  923. #####################################################################
  924. # Search Functions
  925. #####################################################################
  926. sub _contact_exact_group {
  927. my $self = shift;
  928. my %func_args = @_;
  929. my ($chartid, $supptables, $f, $t, $v, $term) =
  930. @func_args{qw(chartid supptables f t v term)};
  931. my $user = $self->{'user'};
  932. $$v =~ m/%group\\.([^%]+)%/;
  933. my $group = $1;
  934. my $groupid = Bugzilla::Group::ValidateGroupName( $group, ($user));
  935. $groupid || ThrowUserError('invalid_group_name',{name => $group});
  936. my @childgroups = @{$user->flatten_group_membership($groupid)};
  937. my $table = "user_group_map_$$chartid";
  938. push (@$supptables, "LEFT JOIN user_group_map AS $table " .
  939. "ON $table.user_id = bugs.$$f " .
  940. "AND $table.group_id IN(" .
  941. join(',', @childgroups) . ") " .
  942. "AND $table.isbless = 0 " .
  943. "AND $table.grant_type IN(" .
  944. GRANT_DIRECT . "," . GRANT_REGEXP . ")"
  945. );
  946. if ($$t =~ /^not/) {
  947. $$term = "$table.group_id IS NULL";
  948. } else {
  949. $$term = "$table.group_id IS NOT NULL";
  950. }
  951. }
  952. sub _contact_exact {
  953. my $self = shift;
  954. my %func_args = @_;
  955. my ($term, $f, $v) = @func_args{qw(term f v)};
  956. my $user = $self->{'user'};
  957. $$v =~ m/(%\\w+%)/;
  958. $$term = "bugs.$$f = " . pronoun($1, $user);
  959. }
  960. sub _contact_notequals {
  961. my $self = shift;
  962. my %func_args = @_;
  963. my ($term, $f, $v) = @func_args{qw(term f v)};
  964. my $user = $self->{'user'};
  965. $$v =~ m/(%\\w+%)/;
  966. $$term = "bugs.$$f <> " . pronoun($1, $user);
  967. }
  968. sub _assigned_to_reporter_nonchanged {
  969. my $self = shift;
  970. my %func_args = @_;
  971. my ($f, $ff, $funcsbykey, $t, $term) =
  972. @func_args{qw(f ff funcsbykey t term)};
  973. my $real_f = $$f;
  974. $$f = "login_name";
  975. $$ff = "profiles.login_name";
  976. $$funcsbykey{",$$t"}($self, %func_args);
  977. $$term = "bugs.$real_f IN (SELECT userid FROM profiles WHERE $$term)";
  978. }
  979. sub _qa_contact_nonchanged {
  980. my $self = shift;
  981. my %func_args = @_;
  982. my ($supptables, $f) =
  983. @func_args{qw(supptables f)};
  984. push(@$supptables, "LEFT JOIN profiles AS map_qa_contact " .
  985. "ON bugs.qa_contact = map_qa_contact.userid");
  986. $$f = "COALESCE(map_$$f.login_name,'')";
  987. }
  988. sub _cc_exact_group {
  989. my $self = shift;
  990. my %func_args = @_;
  991. my ($chartid, $sequence, $supptables, $t, $v, $term) =
  992. @func_args{qw(chartid sequence supptables t v term)};
  993. my $user = $self->{'user'};
  994. $$v =~ m/%group\\.([^%]+)%/;
  995. my $group = $1;
  996. my $groupid = Bugzilla::Group::ValidateGroupName( $group, ($user));
  997. $groupid || ThrowUserError('invalid_group_name',{name => $group});
  998. my @childgroups = @{$user->flatten_group_membership($groupid)};
  999. my $chartseq = $$chartid;
  1000. if ($$chartid eq "") {
  1001. $chartseq = "CC$$sequence";
  1002. $$sequence++;
  1003. }
  1004. my $table = "user_group_map_$chartseq";
  1005. push(@$supptables, "LEFT JOIN cc AS cc_$chartseq " .
  1006. "ON bugs.bug_id = cc_$chartseq.bug_id");
  1007. push(@$supptables, "LEFT JOIN user_group_map AS $table " .
  1008. "ON $table.user_id = cc_$chartseq.who " .
  1009. "AND $table.group_id IN(" .
  1010. join(',', @childgroups) . ") " .
  1011. "AND $table.isbless = 0 " .
  1012. "AND $table.grant_type IN(" .
  1013. GRANT_DIRECT . "," . GRANT_REGEXP . ")"
  1014. );
  1015. if ($$t =~ /^not/) {
  1016. $$term = "$table.group_id IS NULL";
  1017. } else {
  1018. $$term = "$table.group_id IS NOT NULL";
  1019. }
  1020. }
  1021. sub _cc_exact {
  1022. my $self = shift;
  1023. my %func_args = @_;
  1024. my ($chartid, $sequence, $supptables, $term, $v) =
  1025. @func_args{qw(chartid sequence supptables term v)};
  1026. my $user = $self->{'user'};
  1027. $$v =~ m/(%\\w+%)/;
  1028. my $match = pronoun($1, $user);
  1029. my $chartseq = $$chartid;
  1030. if ($$chartid eq "") {
  1031. $chartseq = "CC$$sequence";
  1032. $$sequence++;
  1033. }
  1034. push(@$supptables, "LEFT JOIN cc AS cc_$chartseq " .
  1035. "ON bugs.bug_id = cc_$chartseq.bug_id " .
  1036. "AND cc_$chartseq.who = $match");
  1037. $$term = "cc_$chartseq.who IS NOT NULL";
  1038. }
  1039. sub _cc_notequals {
  1040. my $self = shift;
  1041. my %func_args = @_;
  1042. my ($chartid, $sequence, $supptables, $term, $v) =
  1043. @func_args{qw(chartid sequence supptables term v)};
  1044. my $user = $self->{'user'};
  1045. $$v =~ m/(%\\w+%)/;
  1046. my $match = pronoun($1, $user);
  1047. my $chartseq = $$chartid;
  1048. if ($$chartid eq "") {
  1049. $chartseq = "CC$$sequence";
  1050. $$sequence++;
  1051. }
  1052. push(@$supptables, "LEFT JOIN cc AS cc_$chartseq " .
  1053. "ON bugs.bug_id = cc_$chartseq.bug_id " .
  1054. "AND cc_$chartseq.who = $match");
  1055. $$term = "cc_$chartseq.who IS NULL";
  1056. }
  1057. sub _cc_nonchanged {
  1058. my $self = shift;
  1059. my %func_args = @_;
  1060. my ($chartid, $sequence, $f, $ff, $t, $funcsbykey, $supptables, $term, $v) =
  1061. @func_args{qw(chartid sequence f ff t funcsbykey supptables term v)};
  1062. my $chartseq = $$chartid;
  1063. if ($$chartid eq "") {
  1064. $chartseq = "CC$$sequence";
  1065. $$sequence++;
  1066. }
  1067. $$f = "login_name";
  1068. $$ff = "profiles.login_name";
  1069. $$funcsbykey{",$$t"}($self, %func_args);
  1070. push(@$supptables, "LEFT JOIN cc AS cc_$chartseq " .
  1071. "ON bugs.bug_id = cc_$chartseq.bug_id " .
  1072. "AND cc_$chartseq.who IN" .
  1073. "(SELECT userid FROM profiles WHERE $$term)"
  1074. );
  1075. $$term = "cc_$chartseq.who IS NOT NULL";
  1076. }
  1077. sub _long_desc_changedby {
  1078. my $self = shift;
  1079. my %func_args = @_;
  1080. my ($chartid, $supptables, $term, $v) =
  1081. @func_args{qw(chartid supptables term v)};
  1082. my $table = "longdescs_$$chartid";
  1083. push(@$supptables, "LEFT JOIN longdescs AS $table " .
  1084. "ON $table.bug_id = bugs.bug_id");
  1085. my $id = login_to_id($$v, THROW_ERROR);
  1086. $$term = "$table.who = $id";
  1087. }
  1088. sub _long_desc_changedbefore_after {
  1089. my $self = shift;
  1090. my %func_args = @_;
  1091. my ($chartid, $t, $v, $supptables, $term) =
  1092. @func_args{qw(chartid t v supptables term)};
  1093. my $dbh = Bugzilla->dbh;
  1094. my $operator = ($$t =~ /before/) ? '<' : '>';
  1095. my $table = "longdescs_$$chartid";
  1096. push(@$supptables, "LEFT JOIN longdescs AS $table " .
  1097. "ON $table.bug_id = bugs.bug_id " .
  1098. "AND $table.bug_when $operator " .
  1099. $dbh->quote(SqlifyDate($$v)) );
  1100. $$term = "($table.bug_when IS NOT NULL)";
  1101. }
  1102. sub _content_matches {
  1103. my $self = shift;
  1104. my %func_args = @_;
  1105. my ($chartid, $supptables, $term, $groupby, $fields, $v) =
  1106. @func_args{qw(chartid supptables term groupby fields v)};
  1107. my $dbh = Bugzilla->dbh;
  1108. # "content" is an alias for columns containing text for which we
  1109. # can search a full-text index and retrieve results by relevance,
  1110. # currently just bug comments (and summaries to some degree).
  1111. # There's only one way to search a full-text index, so we only
  1112. # accept the "matches" operator, which is specific to full-text
  1113. # index searches.
  1114. # Add the fulltext table to the query so we can search on it.
  1115. my $table = "bugs_fulltext_$$chartid";
  1116. my $comments_col = "comments";
  1117. $comments_col = "comments_noprivate" unless $self->{'user'}->is_insider;
  1118. push(@$supptables, "LEFT JOIN bugs_fulltext AS $table " .
  1119. "ON bugs.bug_id = $table.bug_id");
  1120. # Create search terms to add to the SELECT and WHERE clauses.
  1121. my ($term1, $rterm1) = $dbh->sql_fulltext_search("$table.$comments_col",
  1122. $$v, 1);
  1123. my ($term2, $rterm2) = $dbh->sql_fulltext_search("$table.short_desc",
  1124. $$v, 2);
  1125. $rterm1 = $term1 if !$rterm1;
  1126. $rterm2 = $term2 if !$rterm2;
  1127. # The term to use in the WHERE clause.
  1128. $$term = "$term1 > 0 OR $term2 > 0";
  1129. # In order to sort by relevance (in case the user requests it),
  1130. # we SELECT the relevance value and give it an alias so we can
  1131. # add it to the SORT BY clause when we build it in buglist.cgi.
  1132. my $select_term = "($rterm1 + $rterm2) AS relevance";
  1133. # Users can specify to display the relevance field, in which case
  1134. # it'll show up in the list of fields being selected, and we need
  1135. # to replace that occurrence with our select term. Otherwise
  1136. # we can just add the term to the list of fields being selected.
  1137. if (grep($_ eq "relevance", @$fields)) {
  1138. @$fields = map($_ eq "relevance" ? $select_term : $_ , @$fields);
  1139. }
  1140. else {
  1141. push(@$fields, $select_term);
  1142. }
  1143. }
  1144. sub _timestamp_compare {
  1145. my $self = shift;
  1146. my %func_args = @_;
  1147. my ($v, $q) = @func_args{qw(v q)};
  1148. my $dbh = Bugzilla->dbh;
  1149. $$v = SqlifyDate($$v);
  1150. $$q = $dbh->quote($$v);
  1151. }
  1152. sub _commenter_exact {
  1153. my $self = shift;
  1154. my %func_args = @_;
  1155. my ($chartid, $sequence, $supptables, $term, $v) =
  1156. @func_args{qw(chartid sequence supptables term v)};
  1157. my $user = $self->{'user'};
  1158. $$v =~ m/(%\\w+%)/;
  1159. my $match = pronoun($1, $user);
  1160. my $chartseq = $$chartid;
  1161. if ($$chartid eq "") {
  1162. $chartseq = "LD$$sequence";
  1163. $$sequence++;
  1164. }
  1165. my $table = "longdescs_$chartseq";
  1166. my $extra = $user->is_insider ? "" : "AND $table.isprivate < 1";
  1167. push(@$supptables, "LEFT JOIN longdescs AS $table " .
  1168. "ON $table.bug_id = bugs.bug_id $extra " .
  1169. "AND $table.who IN ($match)");
  1170. $$term = "$table.who IS NOT NULL";
  1171. }
  1172. sub _commenter {
  1173. my $self = shift;
  1174. my %func_args = @_;
  1175. my ($chartid, $sequence, $supptables, $f, $ff, $t, $funcsbykey, $term) =
  1176. @func_args{qw(chartid sequence supptables f ff t funcsbykey term)};
  1177. my $chartseq = $$chartid;
  1178. if ($$chartid eq "") {
  1179. $chartseq = "LD$$sequence";
  1180. $$sequence++;
  1181. }
  1182. my $table = "longdescs_$chartseq";
  1183. my $extra = $self->{'user'}->is_insider ? "" : "AND $table.isprivate < 1";
  1184. $$f = "login_name";
  1185. $$ff = "profiles.login_name";
  1186. $$funcsbykey{",$$t"}($self, %func_args);
  1187. push(@$supptables, "LEFT JOIN longdescs AS $table " .
  1188. "ON $table.bug_id = bugs.bug_id $extra " .
  1189. "AND $table.who IN" .
  1190. "(SELECT userid FROM profiles WHERE $$term)"
  1191. );
  1192. $$term = "$table.who IS NOT NULL";
  1193. }
  1194. sub _long_desc {
  1195. my $self = shift;
  1196. my %func_args = @_;
  1197. my ($chartid, $supptables, $f) =
  1198. @func_args{qw(chartid supptables f)};
  1199. my $table = "longdescs_$$chartid";
  1200. my $extra = $self->{'user'}->is_insider ? "" : "AND $table.isprivate < 1";
  1201. push(@$supptables, "LEFT JOIN longdescs AS $table " .
  1202. "ON $table.bug_id = bugs.bug_id $extra");
  1203. $$f = "$table.thetext";
  1204. }
  1205. sub _longdescs_isprivate {
  1206. my $self = shift;
  1207. my %func_args = @_;
  1208. my ($chartid, $supptables, $f) =
  1209. @func_args{qw(chartid supptables f)};
  1210. my $table = "longdescs_$$chartid";
  1211. my $extra = $self->{'user'}->is_insider ? "" : "AND $table.isprivate < 1";
  1212. push(@$supptables, "LEFT JOIN longdescs AS $table " .
  1213. "ON $table.bug_id = bugs.bug_id $extra");
  1214. $$f = "$table.isprivate";
  1215. }
  1216. sub _work_time_changedby {
  1217. my $self = shift;
  1218. my %func_args = @_;
  1219. my ($chartid, $supptables, $v, $term) =
  1220. @func_args{qw(chartid supptables v term)};
  1221. my $table = "longdescs_$$chartid";
  1222. push(@$supptables, "LEFT JOIN longdescs AS $table " .
  1223. "ON $table.bug_id = bugs.bug_id");
  1224. my $id = login_to_id($$v, THROW_ERROR);
  1225. $$term = "(($table.who = $id";
  1226. $$term .= ") AND ($table.work_time <> 0))";
  1227. }
  1228. sub _work_time_changedbefore_after {
  1229. my $self = shift;
  1230. my %func_args = @_;
  1231. my ($chartid, $t, $v, $supptables, $term) =
  1232. @func_args{qw(chartid t v supptables term)};
  1233. my $dbh = Bugzilla->dbh;
  1234. my $operator = ($$t =~ /before/) ? '<' : '>';
  1235. my $table = "longdescs_$$chartid";
  1236. push(@$supptables, "LEFT JOIN longdescs AS $table " .
  1237. "ON $table.bug_id = bugs.bug_id " .
  1238. "AND $table.work_time <> 0 " .
  1239. "AND $table.bug_when $operator " .
  1240. $dbh->quote(SqlifyDate($$v)) );
  1241. $$term = "($table.bug_when IS NOT NULL)";
  1242. }
  1243. sub _work_time {
  1244. my $self = shift;
  1245. my %func_args = @_;
  1246. my ($chartid, $supptables, $f) =
  1247. @func_args{qw(chartid supptables f)};
  1248. my $table = "longdescs_$$chartid";
  1249. push(@$supptables, "LEFT JOIN longdescs AS $table " .
  1250. "ON $table.bug_id = bugs.bug_id");
  1251. $$f = "$table.work_time";
  1252. }
  1253. sub _percentage_complete {
  1254. my $self = shift;
  1255. my %func_args = @_;
  1256. my ($t, $chartid, $supptables, $fields, $q, $v, $having, $groupby, $term) =
  1257. @func_args{qw(t chartid supptables fields q v having groupby term)};
  1258. my $dbh = Bugzilla->dbh;
  1259. my $oper;
  1260. if ($$t eq "equals") {
  1261. $oper = "=";
  1262. } elsif ($$t eq "greaterthan") {
  1263. $oper = ">";
  1264. } elsif ($$t eq "lessthan") {
  1265. $oper = "<";
  1266. } elsif ($$t eq "notequal") {
  1267. $oper = "<>";
  1268. } elsif ($$t eq "regexp") {
  1269. # This is just a dummy to help catch bugs- $oper won't be used
  1270. # since "regexp" is treated as a special case below. But
  1271. # leaving $oper uninitialized seems risky...
  1272. $oper = "sql_regexp";
  1273. } elsif ($$t eq "notregexp") {
  1274. # This is just a dummy to help catch bugs- $oper won't be used
  1275. # since "notregexp" is treated as a special case below. But
  1276. # leaving $oper uninitialized seems risky...
  1277. $oper = "sql_not_regexp";
  1278. } else {
  1279. $oper = "noop";
  1280. }
  1281. if ($oper ne "noop") {
  1282. my $table = "longdescs_$$chartid";
  1283. if(lsearch($fields, "bugs.remaining_time") == -1) {
  1284. push(@$fields, "bugs.remaining_time");
  1285. }
  1286. push(@$supptables, "LEFT JOIN longdescs AS $table " .
  1287. "ON $table.bug_id = bugs.bug_id");
  1288. my $expression = "(100 * ((SUM($table.work_time) *
  1289. COUNT(DISTINCT $table.bug_when) /
  1290. COUNT(bugs.bug_id)) /
  1291. ((SUM($table.work_time) *
  1292. COUNT(DISTINCT $table.bug_when) /
  1293. COUNT(bugs.bug_id)) +
  1294. bugs.remaining_time)))";
  1295. $$q = $dbh->quote($$v);
  1296. trick_taint($$q);
  1297. if ($$t eq "regexp") {
  1298. push(@$having, $dbh->sql_regexp($expression, $$q));
  1299. } elsif ($$t eq "notregexp") {
  1300. push(@$having, $dbh->sql_not_regexp($expression, $$q));
  1301. } else {
  1302. push(@$having, "$expression $oper " . $$q);
  1303. }
  1304. push(@$groupby, "bugs.remaining_time");
  1305. }
  1306. $$term = "0=0";
  1307. }
  1308. sub _bug_group_nonchanged {
  1309. my $self = shift;
  1310. my %func_args = @_;
  1311. my ($supptables, $chartid, $ff, $f, $t, $funcsbykey, $term) =
  1312. @func_args{qw(supptables chartid ff f t funcsbykey term)};
  1313. push(@$supptables,
  1314. "LEFT JOIN bug_group_map AS bug_group_map_$$chartid " .
  1315. "ON bugs.bug_id = bug_group_map_$$chartid.bug_id");
  1316. $$ff = $$f = "groups_$$chartid.name";
  1317. $$funcsbykey{",$$t"}($self, %func_args);
  1318. push(@$supptables,
  1319. "LEFT JOIN groups AS groups_$$chartid " .
  1320. "ON groups_$$chartid.id = bug_group_map_$$chartid.group_id " .
  1321. "AND $$term");
  1322. $$term = "$$ff IS NOT NULL";
  1323. }
  1324. sub _attach_data_thedata_changed {
  1325. my $self = shift;
  1326. my %func_args = @_;
  1327. my ($f) = @func_args{qw(f)};
  1328. # Searches for attachment data's change must search
  1329. # the creation timestamp of the attachment instead.
  1330. $$f = "attachments.whocares";
  1331. }
  1332. sub _attach_data_thedata {
  1333. my $self = shift;
  1334. my %func_args = @_;
  1335. my ($chartid, $supptables, $f) =
  1336. @func_args{qw(chartid supptables f)};
  1337. my $atable = "attachments_$$chartid";
  1338. my $dtable = "attachdata_$$chartid";
  1339. my $extra = $self->{'user'}->is_insider ? "" : "AND $atable.isprivate = 0";
  1340. push(@$supptables, "INNER JOIN attachments AS $atable " .
  1341. "ON bugs.bug_id = $atable.bug_id $extra");
  1342. push(@$supptables, "INNER JOIN attach_data AS $dtable " .
  1343. "ON $dtable.id = $atable.attach_id");
  1344. $$f = "$dtable.thedata";
  1345. }
  1346. sub _attachments_submitter {
  1347. my $self = shift;
  1348. my %func_args = @_;
  1349. my ($chartid, $supptables, $f) =
  1350. @func_args{qw(chartid supptables f)};
  1351. my $atable = "map_attachment_submitter_$$chartid";
  1352. my $extra = $self->{'user'}->is_insider ? "" : "AND $atable.isprivate = 0";
  1353. push(@$supptables, "INNER JOIN attachments AS $atable " .
  1354. "ON bugs.bug_id = $atable.bug_id $extra");
  1355. push(@$supptables, "LEFT JOIN profiles AS attachers_$$chartid " .
  1356. "ON $atable.submitter_id = attachers_$$chartid.userid");
  1357. $$f = "attachers_$$chartid.login_name";
  1358. }
  1359. sub _attachments {
  1360. my $self = shift;
  1361. my %func_args = @_;
  1362. my ($chartid, $supptables, $f, $t, $v, $q) =
  1363. @func_args{qw(chartid supptables f t v q)};
  1364. my $dbh = Bugzilla->dbh;
  1365. my $table = "attachments_$$chartid";
  1366. my $extra = $self->{'user'}->is_insider ? "" : "AND $table.isprivate = 0";
  1367. push(@$supptables, "INNER JOIN attachments AS $table " .
  1368. "ON bugs.bug_id = $table.bug_id $extra");
  1369. $$f =~ m/^attachments\.(.*)$/;
  1370. my $field = $1;
  1371. if ($$t eq "changedby") {
  1372. $$v = login_to_id($$v, THROW_ERROR);
  1373. $$q = $dbh->quote($$v);
  1374. $field = "submitter_id";
  1375. $$t = "equals";
  1376. } elsif ($$t eq "changedbefore") {
  1377. $$v = SqlifyDate($$v);
  1378. $$q = $dbh->quote($$v);
  1379. $field = "creation_ts";
  1380. $$t = "lessthan";
  1381. } elsif ($$t eq "changedafter") {
  1382. $$v = SqlifyDate($$v);
  1383. $$q = $dbh->quote($$v);
  1384. $field = "creation_ts";
  1385. $$t = "greaterthan";
  1386. }
  1387. if ($field eq "ispatch" && $$v ne "0" && $$v ne "1") {
  1388. ThrowUserError("illegal_attachment_is_patch");
  1389. }
  1390. if ($field eq "isobsolete" && $$v ne "0" && $$v ne "1") {
  1391. ThrowUserError("illegal_is_obsolete");
  1392. }
  1393. $$f = "$table.$field";
  1394. }
  1395. sub _flagtypes_name {
  1396. my $self = shift;
  1397. my %func_args = @_;
  1398. my ($t, $chartid, $supptables, $ff, $funcsbykey, $having, $term) =
  1399. @func_args{qw(t chartid supptables ff funcsbykey having term)};
  1400. my $dbh = Bugzilla->dbh;
  1401. # Matches bugs by flag name/status.
  1402. # Note that--for the purposes of querying--a flag comprises
  1403. # its name plus its status (i.e. a flag named "review"
  1404. # with a status of "+" can be found by searching for "review+").
  1405. # Don't do anything if this condition is about changes to flags,
  1406. # as the generic change condition processors can handle those.
  1407. return if ($$t =~ m/^changed/);
  1408. # Add the flags and flagtypes tables to the query. We do
  1409. # a left join here so bugs without any flags still match
  1410. # negative conditions (f.e. "flag isn't review+").
  1411. my $flags = "flags_$$chartid";
  1412. push(@$supptables, "LEFT JOIN flags AS $flags " .
  1413. "ON bugs.bug_id = $flags.bug_id ");
  1414. my $flagtypes = "flagtypes_$$chartid";
  1415. push(@$supptables, "LEFT JOIN flagtypes AS $flagtypes " .
  1416. "ON $flags.type_id = $flagtypes.id");
  1417. # Generate the condition by running the operator-specific
  1418. # function. Afterwards the condition resides in the global $term
  1419. # variable.
  1420. $$ff = $dbh->sql_string_concat("${flagtypes}.name",
  1421. "$flags.status");
  1422. $$funcsbykey{",$$t"}($self, %func_args);
  1423. # If this is a negative condition (f.e. flag isn't "review+"),
  1424. # we only want bugs where all flags match the condition, not
  1425. # those where any flag matches, which needs special magic.
  1426. # Instead of adding the condition to the WHERE clause, we select
  1427. # the number of flags matching the condition and the total number
  1428. # of flags on each bug, then compare them in a HAVING clause.
  1429. # If the numbers are the same, all flags match the condition,
  1430. # so this bug should be included.
  1431. if ($$t =~ m/not/) {
  1432. push(@$having,
  1433. "SUM(CASE WHEN $$ff IS NOT NULL THEN 1 ELSE 0 END) = " .
  1434. "SUM(CASE WHEN $$term THEN 1 ELSE 0 END)");
  1435. $$term = "0=0";
  1436. }
  1437. }
  1438. sub _requestees_login_name {
  1439. my $self = shift;
  1440. my %func_args = @_;
  1441. my ($f, $chartid, $supptables) = @func_args{qw(f chartid supptables)};
  1442. my $flags = "flags_$$chartid";
  1443. push(@$supptables, "LEFT JOIN flags AS $flags " .
  1444. "ON bugs.bug_id = $flags.bug_id ");
  1445. push(@$supptables, "LEFT JOIN profiles AS requestees_$$chartid " .
  1446. "ON $flags.requestee_id = requestees_$$chartid.userid");
  1447. $$f = "requestees_$$chartid.login_name";
  1448. }
  1449. sub _setters_login_name {
  1450. my $self = shift;
  1451. my %func_args = @_;
  1452. my ($f, $chartid, $supptables) = @func_args{qw(f chartid supptables)};
  1453. my $flags = "flags_$$chartid";
  1454. push(@$supptables, "LEFT JOIN flags AS $flags " .
  1455. "ON bugs.bug_id = $flags.bug_id ");
  1456. push(@$supptables, "LEFT JOIN profiles AS setters_$$chartid " .
  1457. "ON $flags.setter_id = setters_$$chartid.userid");
  1458. $$f = "setters_$$chartid.login_name";
  1459. }
  1460. sub _changedin_days_elapsed {
  1461. my $self = shift;
  1462. my %func_args = @_;
  1463. my ($f) = @func_args{qw(f)};
  1464. my $dbh = Bugzilla->dbh;
  1465. $$f = "(" . $dbh->sql_to_days('NOW()') . " - " .
  1466. $dbh->sql_to_days('bugs.delta_ts') . ")";
  1467. }
  1468. sub _component_nonchanged {
  1469. my $self = shift;
  1470. my %func_args = @_;
  1471. my ($f, $ff, $t, $funcsbykey, $term) =
  1472. @func_args{qw(f ff t funcsbykey term)};
  1473. $$f = $$ff = "components.name";
  1474. $$funcsbykey{",$$t"}($self, %func_args);
  1475. $$term = build_subselect("bugs.component_id",
  1476. "components.id",
  1477. "components",
  1478. $$term);
  1479. }
  1480. sub _product_nonchanged {
  1481. my $self = shift;
  1482. my %func_args = @_;
  1483. my ($f, $ff, $t, $funcsbykey, $term) =
  1484. @func_args{qw(f ff t funcsbykey term)};
  1485. # Generate the restriction condition
  1486. $$f = $$ff = "products.name";
  1487. $$funcsbykey{",$$t"}($self, %func_args);
  1488. $$term = build_subselect("bugs.product_id",
  1489. "products.id",
  1490. "products",
  1491. $$term);
  1492. }
  1493. sub _classification_nonchanged {
  1494. my $self = shift;
  1495. my %func_args = @_;
  1496. my ($chartid, $v, $ff, $f, $funcsbykey, $t, $supptables, $term) =
  1497. @func_args{qw(chartid v ff f funcsbykey t supptables term)};
  1498. # Generate the restriction condition
  1499. push @$supptables, "INNER JOIN products AS map_products " .
  1500. "ON bugs.product_id = map_products.id";
  1501. $$f = $$ff = "classifications.name";
  1502. $$funcsbykey{",$$t"}($self, %func_args);
  1503. $$term = build_subselect("map_products.classification_id",
  1504. "classifications.id",
  1505. "classifications",
  1506. $$term);
  1507. }
  1508. sub _keywords_nonchanged {
  1509. my $self = shift;
  1510. my %func_args = @_;
  1511. my ($chartid, $v, $ff, $f, $t, $term, $supptables) =
  1512. @func_args{qw(chartid v ff f t term supptables)};
  1513. my @list;
  1514. my $table = "keywords_$$chartid";
  1515. foreach my $value (split(/[\s,]+/, $$v)) {
  1516. if ($value eq '') {
  1517. next;
  1518. }
  1519. my $keyword = new Bugzilla::Keyword({name => $value});
  1520. if ($keyword) {
  1521. push(@list, "$table.keywordid = " . $keyword->id);
  1522. }
  1523. else {
  1524. ThrowUserError("unknown_keyword",
  1525. { keyword => $$v });
  1526. }
  1527. }
  1528. my $haveawordterm;
  1529. if (@list) {
  1530. $haveawordterm = "(" . join(' OR ', @list) . ")";
  1531. if ($$t eq "anywords") {
  1532. $$term = $haveawordterm;
  1533. } elsif ($$t eq "allwords") {
  1534. $self->_allwords;
  1535. if ($$term && $haveawordterm) {
  1536. $$term = "(($$term) AND $haveawordterm)";
  1537. }
  1538. }
  1539. }
  1540. if ($$term) {
  1541. push(@$supptables, "LEFT JOIN keywords AS $table " .
  1542. "ON $table.bug_id = bugs.bug_id");
  1543. }
  1544. }
  1545. sub _dependson_nonchanged {
  1546. my $self = shift;
  1547. my %func_args = @_;
  1548. my ($chartid, $ff, $f, $funcsbykey, $t, $term, $supptables) =
  1549. @func_args{qw(chartid ff f funcsbykey t term supptables)};
  1550. my $table = "dependson_" . $$chartid;
  1551. $$ff = "$table.$$f";
  1552. $$funcsbykey{",$$t"}($self, %func_args);
  1553. push(@$supptables, "LEFT JOIN dependencies AS $table " .
  1554. "ON $table.blocked = bugs.bug_id " .
  1555. "AND ($$term)");
  1556. $$term = "$$ff IS NOT NULL";
  1557. }
  1558. sub _blocked_nonchanged {
  1559. my $self = shift;
  1560. my %func_args = @_;
  1561. my ($chartid, $ff, $f, $funcsbykey, $t, $term, $supptables) =
  1562. @func_args{qw(chartid ff f funcsbykey t term supptables)};
  1563. my $table = "blocked_" . $$chartid;
  1564. $$ff = "$table.$$f";
  1565. $$funcsbykey{",$$t"}($self, %func_args);
  1566. push(@$supptables, "LEFT JOIN dependencies AS $table " .
  1567. "ON $table.dependson = bugs.bug_id " .
  1568. "AND ($$term)");
  1569. $$term = "$$ff IS NOT NULL";
  1570. }
  1571. sub _alias_nonchanged {
  1572. my $self = shift;
  1573. my %func_args = @_;
  1574. my ($ff, $funcsbykey, $t, $term) =
  1575. @func_args{qw(ff funcsbykey t term)};
  1576. $$ff = "COALESCE(bugs.alias, '')";
  1577. $$funcsbykey{",$$t"}($self, %func_args);
  1578. }
  1579. sub _owner_idle_time_greater_less {
  1580. my $self = shift;
  1581. my %func_args = @_;
  1582. my ($chartid, $v, $supptables, $t, $wherepart, $term) =
  1583. @func_args{qw(chartid v supptables t wherepart term)};
  1584. my $dbh = Bugzilla->dbh;
  1585. my $table = "idle_" . $$chartid;
  1586. $$v =~ /^(\d+)\s*([hHdDwWmMyY])?$/;
  1587. my $quantity = $1;
  1588. my $unit = lc $2;
  1589. my $unitinterval = 'DAY';
  1590. if ($unit eq 'h') {
  1591. $unitinterval = 'HOUR';
  1592. } elsif ($unit eq 'w') {
  1593. $unitinterval = ' * 7 DAY';
  1594. } elsif ($unit eq 'm') {
  1595. $unitinterval = 'MONTH';
  1596. } elsif ($unit eq 'y') {
  1597. $unitinterval = 'YEAR';
  1598. }
  1599. my $cutoff = "NOW() - " .
  1600. $dbh->sql_interval($quantity, $unitinterval);
  1601. my $assigned_fieldid = get_field_id('assigned_to');
  1602. push(@$supptables, "LEFT JOIN longdescs AS comment_$table " .
  1603. "ON comment_$table.who = bugs.assigned_to " .
  1604. "AND comment_$table.bug_id = bugs.bug_id " .
  1605. "AND comment_$table.bug_when > $cutoff");
  1606. push(@$supptables, "LEFT JOIN bugs_activity AS activity_$table " .
  1607. "ON (activity_$table.who = bugs.assigned_to " .
  1608. "OR activity_$table.fieldid = $assigned_fieldid) " .
  1609. "AND activity_$table.bug_id = bugs.bug_id " .
  1610. "AND activity_$table.bug_when > $cutoff");
  1611. if ($$t =~ /greater/) {
  1612. push(@$wherepart, "(comment_$table.who IS NULL " .
  1613. "AND activity_$table.who IS NULL)");
  1614. } else {
  1615. push(@$wherepart, "(comment_$table.who IS NOT NULL " .
  1616. "OR activity_$table.who IS NOT NULL)");
  1617. }
  1618. $$term = "0=0";
  1619. }
  1620. sub _multiselect_negative {
  1621. my $self = shift;
  1622. my %func_args = @_;
  1623. my ($f, $ff, $t, $funcsbykey, $term) = @func_args{qw(f ff t funcsbykey term)};
  1624. my %map = (
  1625. notequals => 'equals',
  1626. notregexp => 'regexp',
  1627. notsubstring => 'substring',
  1628. nowords => 'anywords',
  1629. nowordssubstr => 'anywordssubstr',
  1630. );
  1631. my $table = "bug_$$f";
  1632. $$ff = "$table.value";
  1633. $$funcsbykey{",".$map{$$t}}($self, %func_args);
  1634. $$term = "bugs.bug_id NOT IN (SELECT bug_id FROM $table WHERE $$term)";
  1635. }
  1636. sub _multiselect_multiple {
  1637. my $self = shift;
  1638. my %func_args = @_;
  1639. my ($f, $ff, $t, $v, $funcsbykey, $term) = @func_args{qw(f ff t v funcsbykey term)};
  1640. my @terms;
  1641. my $table = "bug_$$f";
  1642. $$ff = "$table.value";
  1643. foreach my $word (split(/[\s,]+/, $$v)) {
  1644. $$v = $word;
  1645. $$funcsbykey{",".$$t}($self, %func_args);
  1646. push(@terms, "bugs.bug_id IN
  1647. (SELECT bug_id FROM $table WHERE $$term)");
  1648. }
  1649. if ($$t eq 'anyexact') {
  1650. $$term = "(" . join(" OR ", @terms) . ")";
  1651. }
  1652. else {
  1653. $$term = "(" . join(" AND ", @terms) . ")";
  1654. }
  1655. }
  1656. sub _multiselect_nonchanged {
  1657. my $self = shift;
  1658. my %func_args = @_;
  1659. my ($chartid, $f, $ff, $t, $funcsbykey, $supptables) =
  1660. @func_args{qw(chartid f ff t funcsbykey supptables)};
  1661. my $table = $$f."_".$$chartid;
  1662. $$ff = "$table.value";
  1663. $$funcsbykey{",$$t"}($self, %func_args);
  1664. push(@$supptables, "LEFT JOIN bug_$$f AS $table " .
  1665. "ON $table.bug_id = bugs.bug_id ");
  1666. }
  1667. sub _equals {
  1668. my $self = shift;
  1669. my %func_args = @_;
  1670. my ($ff, $q, $term) = @func_args{qw(ff q term)};
  1671. $$term = "$$ff = $$q";
  1672. }
  1673. sub _notequals {
  1674. my $self = shift;
  1675. my %func_args = @_;
  1676. my ($ff, $q, $term) = @func_args{qw(ff q term)};
  1677. $$term = "$$ff != $$q";
  1678. }
  1679. sub _casesubstring {
  1680. my $self = shift;
  1681. my %func_args = @_;
  1682. my ($ff, $q, $term) = @func_args{qw(ff q term)};
  1683. my $dbh = Bugzilla->dbh;
  1684. $$term = $dbh->sql_position($$q, $$ff) . " > 0";
  1685. }
  1686. sub _substring {
  1687. my $self = shift;
  1688. my %func_args = @_;
  1689. my ($ff, $q, $term) = @func_args{qw(ff q term)};
  1690. my $dbh = Bugzilla->dbh;
  1691. $$term = $dbh->sql_iposition($$q, $$ff) . " > 0";
  1692. }
  1693. sub _notsubstring {
  1694. my $self = shift;
  1695. my %func_args = @_;
  1696. my ($ff, $q, $term) = @func_args{qw(ff q term)};
  1697. my $dbh = Bugzilla->dbh;
  1698. $$term = $dbh->sql_iposition($$q, $$ff) . " = 0";
  1699. }
  1700. sub _regexp {
  1701. my $self = shift;
  1702. my %func_args = @_;
  1703. my ($ff, $q, $term) = @func_args{qw(ff q term)};
  1704. my $dbh = Bugzilla->dbh;
  1705. $$term = $dbh->sql_regexp($$ff, $$q);
  1706. }
  1707. sub _notregexp {
  1708. my $self = shift;
  1709. my %func_args = @_;
  1710. my ($ff, $q, $term) = @func_args{qw(ff q term)};
  1711. my $dbh = Bugzilla->dbh;
  1712. $$term = $dbh->sql_not_regexp($$ff, $$q);
  1713. }
  1714. sub _lessthan {
  1715. my $self = shift;
  1716. my %func_args = @_;
  1717. my ($ff, $q, $term) = @func_args{qw(ff q term)};
  1718. $$term = "$$ff < $$q";
  1719. }
  1720. sub _greaterthan {
  1721. my $self = shift;
  1722. my %func_args = @_;
  1723. my ($ff, $q, $term) = @func_args{qw(ff q term)};
  1724. $$term = "$$ff > $$q";
  1725. }
  1726. sub _anyexact {
  1727. my $self = shift;
  1728. my %func_args = @_;
  1729. my ($f, $ff, $v, $q, $term) = @func_args{qw(f ff v q term)};
  1730. my $dbh = Bugzilla->dbh;
  1731. my @list;
  1732. foreach my $w (split(/,/, $$v)) {
  1733. if ($w eq "---" && $$f =~ /resolution/) {
  1734. $w = "";
  1735. }
  1736. $$q = $dbh->quote($w);
  1737. trick_taint($$q);
  1738. push(@list, $$q);
  1739. }
  1740. if (@list) {
  1741. $$term = $dbh->sql_in($$ff, \@list);
  1742. }
  1743. }
  1744. sub _anywordsubstr {
  1745. my $self = shift;
  1746. my %func_args = @_;
  1747. my ($ff, $v, $term) = @func_args{qw(ff v term)};
  1748. $$term = join(" OR ", @{GetByWordListSubstr($$ff, $$v)});
  1749. }
  1750. sub _allwordssubstr {
  1751. my $self = shift;
  1752. my %func_args = @_;
  1753. my ($ff, $v, $term) = @func_args{qw(ff v term)};
  1754. $$term = join(" AND ", @{GetByWordListSubstr($$ff, $$v)});
  1755. }
  1756. sub _nowordssubstr {
  1757. my $self = shift;
  1758. my %func_args = @_;
  1759. my ($ff, $v, $term) = @func_args{qw(ff v term)};
  1760. my @list = @{GetByWordListSubstr($$ff, $$v)};
  1761. if (@list) {
  1762. $$term = "NOT (" . join(" OR ", @list) . ")";
  1763. }
  1764. }
  1765. sub _anywords {
  1766. my $self = shift;
  1767. my %func_args = @_;
  1768. my ($ff, $v, $term) = @func_args{qw(ff v term)};
  1769. $$term = join(" OR ", @{GetByWordList($$ff, $$v)});
  1770. }
  1771. sub _allwords {
  1772. my $self = shift;
  1773. my %func_args = @_;
  1774. my ($ff, $v, $term) = @func_args{qw(ff v term)};
  1775. $$term = join(" AND ", @{GetByWordList($$ff, $$v)});
  1776. }
  1777. sub _nowords {
  1778. my $self = shift;
  1779. my %func_args = @_;
  1780. my ($ff, $v, $term) = @func_args{qw(ff v term)};
  1781. my @list = @{GetByWordList($$ff, $$v)};
  1782. if (@list) {
  1783. $$term = "NOT (" . join(" OR ", @list) . ")";
  1784. }
  1785. }
  1786. sub _changedbefore_changedafter {
  1787. my $self = shift;
  1788. my %func_args = @_;
  1789. my ($chartid, $f, $ff, $t, $v, $chartfields, $supptables, $term) =
  1790. @func_args{qw(chartid f ff t v chartfields supptables term)};
  1791. my $dbh = Bugzilla->dbh;
  1792. my $operator = ($$t =~ /before/) ? '<' : '>';
  1793. my $table = "act_$$chartid";
  1794. my $fieldid = $$chartfields{$$f};
  1795. if (!$fieldid) {
  1796. ThrowCodeError("invalid_field_name", {field => $$f});
  1797. }
  1798. push(@$supptables, "LEFT JOIN bugs_activity AS $table " .
  1799. "ON $table.bug_id = bugs.bug_id " .
  1800. "AND $table.fieldid = $fieldid " .
  1801. "AND $table.bug_when $operator " .
  1802. $dbh->quote(SqlifyDate($$v)) );
  1803. $$term = "($table.bug_when IS NOT NULL)";
  1804. }
  1805. sub _changedfrom_changedto {
  1806. my $self = shift;
  1807. my %func_args = @_;
  1808. my ($chartid, $chartfields, $f, $t, $v, $q, $supptables, $term) =
  1809. @func_args{qw(chartid chartfields f t v q supptables term)};
  1810. my $operator = ($$t =~ /from/) ? 'removed' : 'added';
  1811. my $table = "act_$$chartid";
  1812. my $fieldid = $$chartfields{$$f};
  1813. if (!$fieldid) {
  1814. ThrowCodeError("invalid_field_name", {field => $$f});
  1815. }
  1816. push(@$supptables, "LEFT JOIN bugs_activity AS $table " .
  1817. "ON $table.bug_id = bugs.bug_id " .
  1818. "AND $table.fieldid = $fieldid " .
  1819. "AND $table.$operator = $$q");
  1820. $$term = "($table.bug_when IS NOT NULL)";
  1821. }
  1822. sub _changedby {
  1823. my $self = shift;
  1824. my %func_args = @_;
  1825. my ($chartid, $chartfields, $f, $v, $supptables, $term) =
  1826. @func_args{qw(chartid chartfields f v supptables term)};
  1827. my $table = "act_$$chartid";
  1828. my $fieldid = $$chartfields{$$f};
  1829. if (!$fieldid) {
  1830. ThrowCodeError("invalid_field_name", {field => $$f});
  1831. }
  1832. my $id = login_to_id($$v, THROW_ERROR);
  1833. push(@$supptables, "LEFT JOIN bugs_activity AS $table " .
  1834. "ON $table.bug_id = bugs.bug_id " .
  1835. "AND $table.fieldid = $fieldid " .
  1836. "AND $table.who = $id");
  1837. $$term = "($table.bug_when IS NOT NULL)";
  1838. }
  1839. 1;