diff options
Diffstat (limited to 'Bugzilla/Search.pm')
-rw-r--r-- | Bugzilla/Search.pm | 1887 |
1 files changed, 1155 insertions, 732 deletions
diff --git a/Bugzilla/Search.pm b/Bugzilla/Search.pm index 8fa27143b..07038153f 100644 --- a/Bugzilla/Search.pm +++ b/Bugzilla/Search.pm @@ -96,7 +96,8 @@ sub init { my $self = shift; my $fieldsref = $self->{'fields'}; my $params = $self->{'params'}; - my $user = $self->{'user'} || Bugzilla->user; + $self->{'user'} ||= Bugzilla->user; + my $user = $self->{'user'}; my $orderref = $self->{'order'} || 0; my @inputorder; @@ -422,734 +423,87 @@ sub init { my $v; my $term; my %funcsbykey; - my @funcdefs = - ( - "^(?:assigned_to|reporter|qa_contact),(?:notequals|equals|anyexact),%group\\.([^%]+)%" => sub { - my $group = $1; - my $groupid = Bugzilla::Group::ValidateGroupName( $group, ($user)); - $groupid || ThrowUserError('invalid_group_name',{name => $group}); - my @childgroups = @{$user->flatten_group_membership($groupid)}; - my $table = "user_group_map_$chartid"; - push (@supptables, "LEFT JOIN user_group_map AS $table " . - "ON $table.user_id = bugs.$f " . - "AND $table.group_id IN(" . - join(',', @childgroups) . ") " . - "AND $table.isbless = 0 " . - "AND $table.grant_type IN(" . - GRANT_DIRECT . "," . GRANT_REGEXP . ")" - ); - if ($t =~ /^not/) { - $term = "$table.group_id IS NULL"; - } else { - $term = "$table.group_id IS NOT NULL"; - } - }, - "^(?:assigned_to|reporter|qa_contact),(?:equals|anyexact),(%\\w+%)" => sub { - $term = "bugs.$f = " . pronoun($1, $user); - }, - "^(?:assigned_to|reporter|qa_contact),(?:notequals),(%\\w+%)" => sub { - $term = "bugs.$f <> " . pronoun($1, $user); - }, - "^(assigned_to|reporter),(?!changed)" => sub { - my $real_f = $f; - $f = "login_name"; - $ff = "profiles.login_name"; - $funcsbykey{",$t"}->(); - $term = "bugs.$real_f IN (SELECT userid FROM profiles WHERE $term)"; - }, - "^qa_contact,(?!changed)" => sub { - push(@supptables, "LEFT JOIN profiles AS map_qa_contact " . - "ON bugs.qa_contact = map_qa_contact.userid"); - $f = "COALESCE(map_$f.login_name,'')"; - }, - - "^(?:cc),(?:notequals|equals|anyexact),%group\\.([^%]+)%" => sub { - my $group = $1; - my $groupid = Bugzilla::Group::ValidateGroupName( $group, ($user)); - $groupid || ThrowUserError('invalid_group_name',{name => $group}); - my @childgroups = @{$user->flatten_group_membership($groupid)}; - my $chartseq = $chartid; - if ($chartid eq "") { - $chartseq = "CC$sequence"; - $sequence++; - } - my $table = "user_group_map_$chartseq"; - push(@supptables, "LEFT JOIN cc AS cc_$chartseq " . - "ON bugs.bug_id = cc_$chartseq.bug_id"); - push(@supptables, "LEFT JOIN user_group_map AS $table " . - "ON $table.user_id = cc_$chartseq.who " . - "AND $table.group_id IN(" . - join(',', @childgroups) . ") " . - "AND $table.isbless = 0 " . - "AND $table.grant_type IN(" . - GRANT_DIRECT . "," . GRANT_REGEXP . ")" - ); - if ($t =~ /^not/) { - $term = "$table.group_id IS NULL"; - } else { - $term = "$table.group_id IS NOT NULL"; - } - }, - - "^cc,(?:equals|anyexact),(%\\w+%)" => sub { - my $match = pronoun($1, $user); - my $chartseq = $chartid; - if ($chartid eq "") { - $chartseq = "CC$sequence"; - $sequence++; - } - push(@supptables, "LEFT JOIN cc AS cc_$chartseq " . - "ON bugs.bug_id = cc_$chartseq.bug_id " . - "AND cc_$chartseq.who = $match"); - $term = "cc_$chartseq.who IS NOT NULL"; - }, - "^cc,(?:notequals),(%\\w+%)" => sub { - my $match = pronoun($1, $user); - my $chartseq = $chartid; - if ($chartid eq "") { - $chartseq = "CC$sequence"; - $sequence++; - } - push(@supptables, "LEFT JOIN cc AS cc_$chartseq " . - "ON bugs.bug_id = cc_$chartseq.bug_id " . - "AND cc_$chartseq.who = $match"); - $term = "cc_$chartseq.who IS NULL"; - }, - "^cc,(?!changed)" => sub { - my $chartseq = $chartid; - if ($chartid eq "") { - $chartseq = "CC$sequence"; - $sequence++; - } - $f = "login_name"; - $ff = "profiles.login_name"; - $funcsbykey{",$t"}->(); - push(@supptables, "LEFT JOIN cc AS cc_$chartseq " . - "ON bugs.bug_id = cc_$chartseq.bug_id " . - "AND cc_$chartseq.who IN" . - "(SELECT userid FROM profiles WHERE $term)" - ); - $term = "cc_$chartseq.who IS NOT NULL"; - }, - - "^long_?desc,changedby" => sub { - my $table = "longdescs_$chartid"; - push(@supptables, "LEFT JOIN longdescs AS $table " . - "ON $table.bug_id = bugs.bug_id"); - my $id = login_to_id($v, THROW_ERROR); - $term = "$table.who = $id"; - }, - "^long_?desc,changedbefore" => sub { - my $table = "longdescs_$chartid"; - push(@supptables, "LEFT JOIN longdescs AS $table " . - "ON $table.bug_id = bugs.bug_id"); - $term = "$table.bug_when < " . $dbh->quote(SqlifyDate($v)); - }, - "^long_?desc,changedafter" => sub { - my $table = "longdescs_$chartid"; - push(@supptables, "LEFT JOIN longdescs AS $table " . - "ON $table.bug_id = bugs.bug_id"); - $term = "$table.bug_when > " . $dbh->quote(SqlifyDate($v)); - }, - "^content,matches" => sub { - # "content" is an alias for columns containing text for which we - # can search a full-text index and retrieve results by relevance, - # currently just bug comments (and summaries to some degree). - # There's only one way to search a full-text index, so we only - # accept the "matches" operator, which is specific to full-text - # index searches. - - # Add the longdescs table to the query so we can search comments. - my $table = "longdescs_$chartid"; - my $extra = ""; - if (Bugzilla->params->{"insidergroup"} - && !Bugzilla->user->in_group(Bugzilla->params->{"insidergroup"})) - { - $extra = "AND $table.isprivate < 1"; - } - push(@supptables, "LEFT JOIN longdescs AS $table " . - "ON bugs.bug_id = $table.bug_id $extra"); - - # Create search terms to add to the SELECT and WHERE clauses. - # $term1 searches comments. - my $term1 = $dbh->sql_fulltext_search("${table}.thetext", $v); - - # short_desc searching for the WHERE clause - my @words = _split_words_into_like('bugs.short_desc', $v); - my $term2_where = join(' OR ', @words); - - # short_desc relevance - my $factor = SUMMARY_RELEVANCE_FACTOR; - my @s_words = map("CASE WHEN $_ THEN $factor ELSE 0 END", @words); - my $term2_select = join(' + ', @s_words); - - # The term to use in the WHERE clause. - $term = "$term1 > 0 OR ($term2_where)"; - - # In order to sort by relevance (in case the user requests it), - # we SELECT the relevance value and give it an alias so we can - # add it to the SORT BY clause when we build it in buglist.cgi. - # - # Note: We should be calculating the relevance based on all - # comments for a bug, not just matching comments, but that's hard - # (see http://bugzilla.mozilla.org/show_bug.cgi?id=145588#c35). - my $select_term = "(SUM($term1) + $term2_select) AS relevance"; - - # add the column not used in aggregate function explicitly - push(@groupby, 'bugs.short_desc'); - - # Users can specify to display the relevance field, in which case - # it'll show up in the list of fields being selected, and we need - # to replace that occurrence with our select term. Otherwise - # we can just add the term to the list of fields being selected. - if (grep($_ eq "relevance", @fields)) { - @fields = map($_ eq "relevance" ? $select_term : $_ , @fields); - } - else { - push(@fields, $select_term); - } - }, - "^content," => sub { - ThrowUserError("search_content_without_matches"); - }, - "^(?:deadline|creation_ts|delta_ts),(?:lessthan|greaterthan|equals|notequals),(?:-|\\+)?(?:\\d+)(?:[dDwWmMyY])\$" => sub { - $v = SqlifyDate($v); - $q = $dbh->quote($v); - }, - "^commenter,(?:equals|anyexact),(%\\w+%)" => sub { - my $match = pronoun($1, $user); - my $chartseq = $chartid; - if ($chartid eq "") { - $chartseq = "LD$sequence"; - $sequence++; - } - my $table = "longdescs_$chartseq"; - my $extra = ""; - if (Bugzilla->params->{"insidergroup"} - && !Bugzilla->user->in_group(Bugzilla->params->{"insidergroup"})) - { - $extra = "AND $table.isprivate < 1"; - } - push(@supptables, "LEFT JOIN longdescs AS $table " . - "ON $table.bug_id = bugs.bug_id $extra " . - "AND $table.who IN ($match)"); - $term = "$table.who IS NOT NULL"; - }, - "^commenter," => sub { - my $chartseq = $chartid; - if ($chartid eq "") { - $chartseq = "LD$sequence"; - $sequence++; - } - my $table = "longdescs_$chartseq"; - my $extra = ""; - if (Bugzilla->params->{"insidergroup"} - && !Bugzilla->user->in_group(Bugzilla->params->{"insidergroup"})) - { - $extra = "AND $table.isprivate < 1"; - } - $f = "login_name"; - $ff = "profiles.login_name"; - $funcsbykey{",$t"}->(); - push(@supptables, "LEFT JOIN longdescs AS $table " . - "ON $table.bug_id = bugs.bug_id $extra " . - "AND $table.who IN" . - "(SELECT userid FROM profiles WHERE $term)" - ); - $term = "$table.who IS NOT NULL"; - }, - "^long_?desc," => sub { - my $table = "longdescs_$chartid"; - my $extra = ""; - if (Bugzilla->params->{"insidergroup"} - && !Bugzilla->user->in_group(Bugzilla->params->{"insidergroup"})) - { - $extra = "AND $table.isprivate < 1"; - } - push(@supptables, "LEFT JOIN longdescs AS $table " . - "ON $table.bug_id = bugs.bug_id $extra"); - $f = "$table.thetext"; - }, - "^longdescs\.isprivate," => sub { - my $table = "longdescs_$chartid"; - my $extra = ""; - if (Bugzilla->params->{"insidergroup"} - && !Bugzilla->user->in_group(Bugzilla->params->{"insidergroup"})) - { - $extra = "AND $table.isprivate < 1"; - } - push(@supptables, "LEFT JOIN longdescs AS $table " . - "ON $table.bug_id = bugs.bug_id $extra"); - $f = "$table.isprivate"; - }, - "^work_time,changedby" => sub { - my $table = "longdescs_$chartid"; - push(@supptables, "LEFT JOIN longdescs AS $table " . - "ON $table.bug_id = bugs.bug_id"); - my $id = login_to_id($v, THROW_ERROR); - $term = "(($table.who = $id"; - $term .= ") AND ($table.work_time <> 0))"; - }, - "^work_time,changedbefore" => sub { - my $table = "longdescs_$chartid"; - push(@supptables, "LEFT JOIN longdescs AS $table " . - "ON $table.bug_id = bugs.bug_id"); - $term = "(($table.bug_when < " . $dbh->quote(SqlifyDate($v)); - $term .= ") AND ($table.work_time <> 0))"; - }, - "^work_time,changedafter" => sub { - my $table = "longdescs_$chartid"; - push(@supptables, "LEFT JOIN longdescs AS $table " . - "ON $table.bug_id = bugs.bug_id"); - $term = "(($table.bug_when > " . $dbh->quote(SqlifyDate($v)); - $term .= ") AND ($table.work_time <> 0))"; - }, - "^work_time," => sub { - my $table = "longdescs_$chartid"; - push(@supptables, "LEFT JOIN longdescs AS $table " . - "ON $table.bug_id = bugs.bug_id"); - $f = "$table.work_time"; - }, - "^percentage_complete," => sub { - my $oper; - if ($t eq "equals") { - $oper = "="; - } elsif ($t eq "greaterthan") { - $oper = ">"; - } elsif ($t eq "lessthan") { - $oper = "<"; - } elsif ($t eq "notequal") { - $oper = "<>"; - } elsif ($t eq "regexp") { - # This is just a dummy to help catch bugs- $oper won't be used - # since "regexp" is treated as a special case below. But - # leaving $oper uninitialized seems risky... - $oper = "sql_regexp"; - } elsif ($t eq "notregexp") { - # This is just a dummy to help catch bugs- $oper won't be used - # since "notregexp" is treated as a special case below. But - # leaving $oper uninitialized seems risky... - $oper = "sql_not_regexp"; - } else { - $oper = "noop"; - } - if ($oper ne "noop") { - my $table = "longdescs_$chartid"; - if(lsearch(\@fields, "bugs.remaining_time") == -1) { - push(@fields, "bugs.remaining_time"); - } - push(@supptables, "LEFT JOIN longdescs AS $table " . - "ON $table.bug_id = bugs.bug_id"); - my $expression = "(100 * ((SUM($table.work_time) * - COUNT(DISTINCT $table.bug_when) / - COUNT(bugs.bug_id)) / - ((SUM($table.work_time) * - COUNT(DISTINCT $table.bug_when) / - COUNT(bugs.bug_id)) + - bugs.remaining_time)))"; - $q = $dbh->quote($v); - trick_taint($q); - if ($t eq "regexp") { - push(@having, $dbh->sql_regexp($expression, $q)); - } elsif ($t eq "notregexp") { - push(@having, $dbh->sql_not_regexp($expression, $q)); - } else { - push(@having, "$expression $oper " . $q); - } - push(@groupby, "bugs.remaining_time"); - } - $term = "0=0"; - }, - "^bug_group,(?!changed)" => sub { - push(@supptables, - "LEFT JOIN bug_group_map AS bug_group_map_$chartid " . - "ON bugs.bug_id = bug_group_map_$chartid.bug_id"); - $ff = $f = "groups_$chartid.name"; - my $ref = $funcsbykey{",$t"}; - &$ref; - push(@supptables, - "LEFT JOIN groups AS groups_$chartid " . - "ON groups_$chartid.id = bug_group_map_$chartid.group_id " . - "AND $term"); - $term = "$ff IS NOT NULL"; - }, - "^attach_data\.thedata,changed" => sub { - # Searches for attachment data's change must search - # the creation timestamp of the attachment instead. - $f = "attachments.whocares"; - }, - "^attach_data\.thedata," => sub { - my $atable = "attachments_$chartid"; - my $dtable = "attachdata_$chartid"; - my $extra = ""; - if (Bugzilla->params->{"insidergroup"} - && !Bugzilla->user->in_group(Bugzilla->params->{"insidergroup"})) - { - $extra = "AND $atable.isprivate = 0"; - } - push(@supptables, "INNER JOIN attachments AS $atable " . - "ON bugs.bug_id = $atable.bug_id $extra"); - push(@supptables, "INNER JOIN attach_data AS $dtable " . - "ON $dtable.id = $atable.attach_id"); - $f = "$dtable.thedata"; - }, - "^attachments\.submitter," => sub { - my $atable = "map_attachment_submitter_$chartid"; - my $extra = ""; - if (Bugzilla->params->{"insidergroup"} - && !Bugzilla->user->in_group(Bugzilla->params->{"insidergroup"})) - { - $extra = "AND $atable.isprivate = 0"; - } - push(@supptables, "INNER JOIN attachments AS $atable " . - "ON bugs.bug_id = $atable.bug_id $extra"); - push(@supptables, "LEFT JOIN profiles AS attachers_$chartid " . - "ON $atable.submitter_id = attachers_$chartid.userid"); - $f = "attachers_$chartid.login_name"; - }, - "^attachments\..*," => sub { - my $table = "attachments_$chartid"; - my $extra = ""; - if (Bugzilla->params->{"insidergroup"} - && !Bugzilla->user->in_group(Bugzilla->params->{"insidergroup"})) - { - $extra = "AND $table.isprivate = 0"; - } - push(@supptables, "INNER JOIN attachments AS $table " . - "ON bugs.bug_id = $table.bug_id $extra"); - $f =~ m/^attachments\.(.*)$/; - my $field = $1; - if ($t eq "changedby") { - $v = login_to_id($v, THROW_ERROR); - $q = $dbh->quote($v); - $field = "submitter_id"; - $t = "equals"; - } elsif ($t eq "changedbefore") { - $v = SqlifyDate($v); - $q = $dbh->quote($v); - $field = "creation_ts"; - $t = "lessthan"; - } elsif ($t eq "changedafter") { - $v = SqlifyDate($v); - $q = $dbh->quote($v); - $field = "creation_ts"; - $t = "greaterthan"; - } - if ($field eq "ispatch" && $v ne "0" && $v ne "1") { - ThrowUserError("illegal_attachment_is_patch"); - } - if ($field eq "isobsolete" && $v ne "0" && $v ne "1") { - ThrowUserError("illegal_is_obsolete"); - } - $f = "$table.$field"; - }, - "^flagtypes.name," => sub { - # Matches bugs by flag name/status. - # Note that--for the purposes of querying--a flag comprises - # its name plus its status (i.e. a flag named "review" - # with a status of "+" can be found by searching for "review+"). - - # Don't do anything if this condition is about changes to flags, - # as the generic change condition processors can handle those. - return if ($t =~ m/^changed/); - - # Add the flags and flagtypes tables to the query. We do - # a left join here so bugs without any flags still match - # negative conditions (f.e. "flag isn't review+"). - my $flags = "flags_$chartid"; - push(@supptables, "LEFT JOIN flags AS $flags " . - "ON bugs.bug_id = $flags.bug_id "); - my $flagtypes = "flagtypes_$chartid"; - push(@supptables, "LEFT JOIN flagtypes AS $flagtypes " . - "ON $flags.type_id = $flagtypes.id"); - - # Generate the condition by running the operator-specific - # function. Afterwards the condition resides in the global $term - # variable. - $ff = $dbh->sql_string_concat("${flagtypes}.name", - "$flags.status"); - &{$funcsbykey{",$t"}}; - - # If this is a negative condition (f.e. flag isn't "review+"), - # we only want bugs where all flags match the condition, not - # those where any flag matches, which needs special magic. - # Instead of adding the condition to the WHERE clause, we select - # the number of flags matching the condition and the total number - # of flags on each bug, then compare them in a HAVING clause. - # If the numbers are the same, all flags match the condition, - # so this bug should be included. - if ($t =~ m/not/) { - push(@having, - "SUM(CASE WHEN $ff IS NOT NULL THEN 1 ELSE 0 END) = " . - "SUM(CASE WHEN $term THEN 1 ELSE 0 END)"); - $term = "0=0"; - } - }, - "^requestees.login_name," => sub { - my $flags = "flags_$chartid"; - push(@supptables, "LEFT JOIN flags AS $flags " . - "ON bugs.bug_id = $flags.bug_id "); - push(@supptables, "LEFT JOIN profiles AS requestees_$chartid " . - "ON $flags.requestee_id = requestees_$chartid.userid"); - $f = "requestees_$chartid.login_name"; - }, - "^setters.login_name," => sub { - my $flags = "flags_$chartid"; - push(@supptables, "LEFT JOIN flags AS $flags " . - "ON bugs.bug_id = $flags.bug_id "); - push(@supptables, "LEFT JOIN profiles AS setters_$chartid " . - "ON $flags.setter_id = setters_$chartid.userid"); - $f = "setters_$chartid.login_name"; - }, - - "^(changedin|days_elapsed)," => sub { - $f = "(" . $dbh->sql_to_days('NOW()') . " - " . - $dbh->sql_to_days('bugs.delta_ts') . ")"; - }, - - "^component,(?!changed)" => sub { - $f = $ff = "components.name"; - $funcsbykey{",$t"}->(); - $term = build_subselect("bugs.component_id", - "components.id", - "components", - $term); - }, - - "^product,(?!changed)" => sub { - # Generate the restriction condition - $f = $ff = "products.name"; - $funcsbykey{",$t"}->(); - $term = build_subselect("bugs.product_id", - "products.id", - "products", - $term); - }, - - "^classification,(?!changed)" => sub { - # Generate the restriction condition - push @supptables, "INNER JOIN products AS map_products " . - "ON bugs.product_id = map_products.id"; - $f = $ff = "classifications.name"; - $funcsbykey{",$t"}->(); - $term = build_subselect("map_products.classification_id", - "classifications.id", - "classifications", - $term); - }, - - "^keywords,(?!changed)" => sub { - my @list; - my $table = "keywords_$chartid"; - foreach my $value (split(/[\s,]+/, $v)) { - if ($value eq '') { - next; - } - my $keyword = new Bugzilla::Keyword({name => $value}); - if ($keyword) { - push(@list, "$table.keywordid = " . $keyword->id); - } - else { - ThrowUserError("unknown_keyword", - { keyword => $v }); - } - } - my $haveawordterm; - if (@list) { - $haveawordterm = "(" . join(' OR ', @list) . ")"; - if ($t eq "anywords") { - $term = $haveawordterm; - } elsif ($t eq "allwords") { - my $ref = $funcsbykey{",$t"}; - &$ref; - if ($term && $haveawordterm) { - $term = "(($term) AND $haveawordterm)"; - } - } - } - if ($term) { - push(@supptables, "LEFT JOIN keywords AS $table " . - "ON $table.bug_id = bugs.bug_id"); - } - }, - - "^dependson,(?!changed)" => sub { - my $table = "dependson_" . $chartid; - $ff = "$table.$f"; - my $ref = $funcsbykey{",$t"}; - &$ref; - push(@supptables, "LEFT JOIN dependencies AS $table " . - "ON $table.blocked = bugs.bug_id " . - "AND ($term)"); - $term = "$ff IS NOT NULL"; - }, - - "^blocked,(?!changed)" => sub { - my $table = "blocked_" . $chartid; - $ff = "$table.$f"; - my $ref = $funcsbykey{",$t"}; - &$ref; - push(@supptables, "LEFT JOIN dependencies AS $table " . - "ON $table.dependson = bugs.bug_id " . - "AND ($term)"); - $term = "$ff IS NOT NULL"; - }, - - "^alias,(?!changed)" => sub { - $ff = "COALESCE(bugs.alias, '')"; - my $ref = $funcsbykey{",$t"}; - &$ref; - }, - - "^owner_idle_time,(greaterthan|lessthan)" => sub { - my $table = "idle_" . $chartid; - $v =~ /^(\d+)\s*([hHdDwWmMyY])?$/; - my $quantity = $1; - my $unit = lc $2; - my $unitinterval = 'DAY'; - if ($unit eq 'h') { - $unitinterval = 'HOUR'; - } elsif ($unit eq 'w') { - $unitinterval = ' * 7 DAY'; - } elsif ($unit eq 'm') { - $unitinterval = 'MONTH'; - } elsif ($unit eq 'y') { - $unitinterval = 'YEAR'; - } - my $cutoff = "NOW() - " . - $dbh->sql_interval($quantity, $unitinterval); - my $assigned_fieldid = get_field_id('assigned_to'); - push(@supptables, "LEFT JOIN longdescs AS comment_$table " . - "ON comment_$table.who = bugs.assigned_to " . - "AND comment_$table.bug_id = bugs.bug_id " . - "AND comment_$table.bug_when > $cutoff"); - push(@supptables, "LEFT JOIN bugs_activity AS activity_$table " . - "ON (activity_$table.who = bugs.assigned_to " . - "OR activity_$table.fieldid = $assigned_fieldid) " . - "AND activity_$table.bug_id = bugs.bug_id " . - "AND activity_$table.bug_when > $cutoff"); - if ($t =~ /greater/) { - push(@wherepart, "(comment_$table.who IS NULL " . - "AND activity_$table.who IS NULL)"); - } else { - push(@wherepart, "(comment_$table.who IS NOT NULL " . - "OR activity_$table.who IS NOT NULL)"); - } - $term = "0=0"; - }, - - ",equals" => sub { - $term = "$ff = $q"; - }, - ",notequals" => sub { - $term = "$ff != $q"; - }, - ",casesubstring" => sub { - $term = $dbh->sql_position($q, $ff) . " > 0"; - }, - ",substring" => sub { - $term = $dbh->sql_position(lc($q), "LOWER($ff)") . " > 0"; - }, - ",substr" => sub { - $funcsbykey{",substring"}->(); - }, - ",notsubstring" => sub { - $term = $dbh->sql_position(lc($q), "LOWER($ff)") . " = 0"; - }, - ",regexp" => sub { - $term = $dbh->sql_regexp($ff, $q); - }, - ",notregexp" => sub { - $term = $dbh->sql_not_regexp($ff, $q); - }, - ",lessthan" => sub { - $term = "$ff < $q"; - }, - ",matches" => sub { - ThrowUserError("search_content_without_matches"); - }, - ",greaterthan" => sub { - $term = "$ff > $q"; - }, - ",anyexact" => sub { - my @list; - foreach my $w (split(/,/, $v)) { - if ($w eq "---" && $f =~ /resolution/) { - $w = ""; - } - $q = $dbh->quote($w); - trick_taint($q); - push(@list, $q); - } - if (@list) { - $term = $dbh->sql_in($ff, \@list); - } - }, - ",anywordssubstr" => sub { - $term = join(" OR ", @{GetByWordListSubstr($ff, $v)}); - }, - ",allwordssubstr" => sub { - $term = join(" AND ", @{GetByWordListSubstr($ff, $v)}); - }, - ",nowordssubstr" => sub { - my @list = @{GetByWordListSubstr($ff, $v)}; - if (@list) { - $term = "NOT (" . join(" OR ", @list) . ")"; - } - }, - ",anywords" => sub { - $term = join(" OR ", @{GetByWordList($ff, $v)}); - }, - ",allwords" => sub { - $term = join(" AND ", @{GetByWordList($ff, $v)}); - }, - ",nowords" => sub { - my @list = @{GetByWordList($ff, $v)}; - if (@list) { - $term = "NOT (" . join(" OR ", @list) . ")"; - } - }, - ",(changedbefore|changedafter)" => sub { - my $operator = ($t =~ /before/) ? '<' : '>'; - my $table = "act_$chartid"; - my $fieldid = $chartfields{$f}; - if (!$fieldid) { - ThrowCodeError("invalid_field_name", {field => $f}); - } - push(@supptables, "LEFT JOIN bugs_activity AS $table " . - "ON $table.bug_id = bugs.bug_id " . - "AND $table.fieldid = $fieldid " . - "AND $table.bug_when $operator " . - $dbh->quote(SqlifyDate($v)) ); - $term = "($table.bug_when IS NOT NULL)"; - }, - ",(changedfrom|changedto)" => sub { - my $operator = ($t =~ /from/) ? 'removed' : 'added'; - my $table = "act_$chartid"; - my $fieldid = $chartfields{$f}; - if (!$fieldid) { - ThrowCodeError("invalid_field_name", {field => $f}); - } - push(@supptables, "LEFT JOIN bugs_activity AS $table " . - "ON $table.bug_id = bugs.bug_id " . - "AND $table.fieldid = $fieldid " . - "AND $table.$operator = $q"); - $term = "($table.bug_when IS NOT NULL)"; - }, - ",changedby" => sub { - my $table = "act_$chartid"; - my $fieldid = $chartfields{$f}; - if (!$fieldid) { - ThrowCodeError("invalid_field_name", {field => $f}); - } - my $id = login_to_id($v, THROW_ERROR); - push(@supptables, "LEFT JOIN bugs_activity AS $table " . - "ON $table.bug_id = bugs.bug_id " . - "AND $table.fieldid = $fieldid " . - "AND $table.who = $id"); - $term = "($table.bug_when IS NOT NULL)"; - }, - ); + my %func_args = ( + 'chartid' => \$chartid, + 'sequence' => \$sequence, + 'f' => \$f, + 'ff' => \$ff, + 't' => \$t, + 'v' => \$v, + 'q' => \$q, + 'term' => \$term, + 'funcsbykey' => \%funcsbykey, + 'supptables' => \@supptables, + 'wherepart' => \@wherepart, + 'having' => \@having, + 'groupby' => \@groupby, + 'chartfields' => \%chartfields, + 'fields' => \@fields, + ); + my @funcdefs = ( + "^(?:assigned_to|reporter|qa_contact),(?:notequals|equals|anyexact),%group\\.([^%]+)%" => \&_contact_exact_group, + "^(?:assigned_to|reporter|qa_contact),(?:equals|anyexact),(%\\w+%)" => \&_contact_exact, + "^(?:assigned_to|reporter|qa_contact),(?:notequals),(%\\w+%)" => \&_contact_notequals, + "^(assigned_to|reporter),(?!changed)" => \&_assigned_to_reporter_nonchanged, + "^qa_contact,(?!changed)" => \&_qa_contact_nonchanged, + "^(?:cc),(?:notequals|equals|anyexact),%group\\.([^%]+)%" => \&_cc_exact_group, + "^cc,(?:equals|anyexact),(%\\w+%)" => \&_cc_exact, + "^cc,(?:notequals),(%\\w+%)" => \&_cc_notequals, + "^cc,(?!changed)" => \&_cc_nonchanged, + "^long_?desc,changedby" => \&_long_desc_changedby, + "^long_?desc,changedbefore" => \&_long_desc_changedbefore, + "^long_?desc,changedafter" => \&_long_desc_changedafter, + "^content,matches" => \&_content_matches, + "^content," => sub { ThrowUserError("search_content_without_matches"); }, + "^(?:deadline|creation_ts|delta_ts),(?:lessthan|greaterthan|equals|notequals),(?:-|\\+)?(?:\\d+)(?:[dDwWmMyY])\$" => \&_timestamp_compare, + "^commenter,(?:equals|anyexact),(%\\w+%)" => \&_commenter_exact, + "^commenter," => \&_commenter, + "^long_?desc," => \&_long_desc, + "^longdescs\.isprivate," => \&_longdescs_isprivate, + "^work_time,changedby" => \&_work_time_changedby, + "^work_time,changedbefore" => \&_work_time_changedbefore, + "^work_time,changedafter" => \&_work_time_changedafter, + "^work_time," => \&_work_time, + "^percentage_complete," => \&_percentage_complete, + "^bug_group,(?!changed)" => \&_bug_group_nonchanged, + "^attach_data\.thedata,changed" => \&_attach_data_thedata_changed, + "^attach_data\.thedata," => \&_attach_data_thedata, + "^attachments\.submitter," => \&_attachments_submitter, + "^attachments\..*," => \&_attachments, + "^flagtypes.name," => \&_flagtypes_name, + "^requestees.login_name," => \&_requestees_login_name, + "^setters.login_name," => \&_setters_login_name, + "^(changedin|days_elapsed)," => \&_changedin_days_elapsed, + "^component,(?!changed)" => \&_component_nonchanged, + "^product,(?!changed)" => \&_product_nonchanged, + "^classification,(?!changed)" => \&_classification_nonchanged, + "^keywords,(?!changed)" => \&_keywords_nonchanged, + "^dependson,(?!changed)" => \&_dependson_nonchanged, + "^blocked,(?!changed)" => \&_blocked_nonchanged, + "^alias,(?!changed)" => \&_alias_nonchanged, + "^owner_idle_time,(greaterthan|lessthan)" => \&_owner_idle_time_greater_less, + ",equals" => \&_equals, + ",notequals" => \&_notequals, + ",casesubstring" => \&_casesubstring, + ",substring" => \&_substring, + ",substr" => \&_substring, + ",notsubstring" => \&_notsubstring, + ",regexp" => \&_regexp, + ",notregexp" => \&_notregexp, + ",lessthan" => \&_lessthan, + ",matches" => sub { ThrowUserError("search_content_without_matches"); }, + ",greaterthan" => \&_greaterthan, + ",anyexact" => \&_anyexact, + ",anywordssubstr" => \&_anywordsubstr, + ",allwordssubstr" => \&_allwordssubstr, + ",nowordssubstr" => \&_nowordssubstr, + ",anywords" => \&_anywords, + ",allwords" => \&_allwords, + ",nowords" => \&_nowords, + ",(changedbefore|changedafter)" => \&_changedbefore_changedafter, + ",(changedfrom|changedto)" => \&_changedfrom_changedto, + ",changedby" => \&_changedby, + ); my @funcnames; while (@funcdefs) { my $key = shift(@funcdefs); @@ -1326,7 +680,7 @@ sub init { if ($f !~ /\./) { $ff = "bugs.$f"; } - &$ref; + $self->$ref(%func_args); if ($debug) { push(@debugdata, "$f / $t / $v / " . ($term || "undef") . " *"); @@ -1519,8 +873,7 @@ sub build_subselect { my $dbh = Bugzilla->dbh; my $list = $dbh->selectcol_arrayref($q); return "1=2" unless @$list; # Could use boolean type on dbs which support it - return $dbh->sql_in($outer, $list); -} + return $dbh->sql_in($outer, $list);} sub GetByWordList { my ($field, $strs) = (@_); @@ -1672,4 +1025,1074 @@ sub _split_words_into_like { @words = map($dbh->sql_istrcmp($field, $_, 'LIKE'), @words); return @words; } + +##################################################################### +# Search Functions +##################################################################### + +sub _contact_exact_group { + my $self = shift; + my %func_args = @_; + my ($chartid, $supptables, $f, $t, $v, $term) = + @func_args{qw(chartid supptables f t v term)}; + my $user = $self->{'user'}; + + $$v =~ m/%group\\.([^%]+)%/; + my $group = $1; + my $groupid = Bugzilla::Group::ValidateGroupName( $group, ($user)); + $groupid || ThrowUserError('invalid_group_name',{name => $group}); + my @childgroups = @{$user->flatten_group_membership($groupid)}; + my $table = "user_group_map_$$chartid"; + push (@$supptables, "LEFT JOIN user_group_map AS $table " . + "ON $table.user_id = bugs.$$f " . + "AND $table.group_id IN(" . + join(',', @childgroups) . ") " . + "AND $table.isbless = 0 " . + "AND $table.grant_type IN(" . + GRANT_DIRECT . "," . GRANT_REGEXP . ")" + ); + if ($$t =~ /^not/) { + $$term = "$table.group_id IS NULL"; + } else { + $$term = "$table.group_id IS NOT NULL"; + } +} + +sub _contact_exact { + my $self = shift; + my %func_args = @_; + my ($term, $f, $v) = @func_args{qw(term f v)}; + my $user = $self->{'user'}; + + $$v =~ m/(%\\w+%)/; + $$term = "bugs.$$f = " . pronoun($1, $user); +} + +sub _contact_notequals { + my $self = shift; + my %func_args = @_; + my ($term, $f, $v) = @func_args{qw(term f v)}; + my $user = $self->{'user'}; + + $$v =~ m/(%\\w+%)/; + $$term = "bugs.$$f <> " . pronoun($1, $user); +} + +sub _assigned_to_reporter_nonchanged { + my $self = shift; + my %func_args = @_; + my ($f, $ff, $funcsbykey, $t, $term) = + @func_args{qw(f ff funcsbykey t term)}; + + my $real_f = $$f; + $$f = "login_name"; + $$ff = "profiles.login_name"; + $$funcsbykey{",$$t"}($self, %func_args); + $$term = "bugs.$real_f IN (SELECT userid FROM profiles WHERE $$term)"; +} + +sub _qa_contact_nonchanged { + my $self = shift; + my %func_args = @_; + my ($supptables, $f) = + @func_args{qw(supptables f)}; + + push(@$supptables, "LEFT JOIN profiles AS map_qa_contact " . + "ON bugs.qa_contact = map_qa_contact.userid"); + $$f = "COALESCE(map_$$f.login_name,'')"; +} + +sub _cc_exact_group { + my $self = shift; + my %func_args = @_; + my ($chartid, $sequence, $supptables, $t, $v, $term) = + @func_args{qw(chartid sequence supptables t v term)}; + my $user = $self->{'user'}; + + $$v =~ m/%group\\.([^%]+)%/; + my $group = $1; + my $groupid = Bugzilla::Group::ValidateGroupName( $group, ($user)); + $groupid || ThrowUserError('invalid_group_name',{name => $group}); + my @childgroups = @{$user->flatten_group_membership($groupid)}; + my $chartseq = $$chartid; + if ($$chartid eq "") { + $chartseq = "CC$$sequence"; + $$sequence++; + } + my $table = "user_group_map_$chartseq"; + push(@$supptables, "LEFT JOIN cc AS cc_$chartseq " . + "ON bugs.bug_id = cc_$chartseq.bug_id"); + push(@$supptables, "LEFT JOIN user_group_map AS $table " . + "ON $table.user_id = cc_$chartseq.who " . + "AND $table.group_id IN(" . + join(',', @childgroups) . ") " . + "AND $table.isbless = 0 " . + "AND $table.grant_type IN(" . + GRANT_DIRECT . "," . GRANT_REGEXP . ")" + ); + if ($$t =~ /^not/) { + $$term = "$table.group_id IS NULL"; + } else { + $$term = "$table.group_id IS NOT NULL"; + } +} + +sub _cc_exact { + my $self = shift; + my %func_args = @_; + my ($chartid, $sequence, $supptables, $term, $v) = + @func_args{qw(chartid sequence supptables term v)}; + my $user = $self->{'user'}; + + $$v =~ m/(%\\w+%)/; + my $match = pronoun($1, $user); + my $chartseq = $$chartid; + if ($$chartid eq "") { + $chartseq = "CC$$sequence"; + $$sequence++; + } + push(@$supptables, "LEFT JOIN cc AS cc_$chartseq " . + "ON bugs.bug_id = cc_$chartseq.bug_id " . + "AND cc_$chartseq.who = $match"); + $$term = "cc_$chartseq.who IS NOT NULL"; +} + +sub _cc_notequals { + my $self = shift; + my %func_args = @_; + my ($chartid, $sequence, $supptables, $term, $v) = + @func_args{qw(chartid sequence supptables term v)}; + my $user = $self->{'user'}; + + $$v =~ m/(%\\w+%)/; + my $match = pronoun($1, $user); + my $chartseq = $$chartid; + if ($$chartid eq "") { + $chartseq = "CC$$sequence"; + $$sequence++; + } + push(@$supptables, "LEFT JOIN cc AS cc_$chartseq " . + "ON bugs.bug_id = cc_$chartseq.bug_id " . + "AND cc_$chartseq.who = $match"); + $$term = "cc_$chartseq.who IS NULL"; +} + +sub _cc_nonchanged { + my $self = shift; + my %func_args = @_; + my ($chartid, $sequence, $f, $ff, $t, $funcsbykey, $supptables, $term, $v) = + @func_args{qw(chartid sequence f ff t funcsbykey supptables term v)}; + + my $chartseq = $$chartid; + if ($$chartid eq "") { + $chartseq = "CC$$sequence"; + $$sequence++; + } + $$f = "login_name"; + $$ff = "profiles.login_name"; + $$funcsbykey{",$$t"}($self, %func_args); + push(@$supptables, "LEFT JOIN cc AS cc_$chartseq " . + "ON bugs.bug_id = cc_$chartseq.bug_id " . + "AND cc_$chartseq.who IN" . + "(SELECT userid FROM profiles WHERE $$term)" + ); + $$term = "cc_$chartseq.who IS NOT NULL"; +} + +sub _long_desc_changedby { + my $self = shift; + my %func_args = @_; + my ($chartid, $supptables, $term, $v) = + @func_args{qw(chartid supptables term v)}; + + my $table = "longdescs_$$chartid"; + push(@$supptables, "LEFT JOIN longdescs AS $table " . + "ON $table.bug_id = bugs.bug_id"); + my $id = login_to_id($$v, THROW_ERROR); + $$term = "$table.who = $id"; +} + +sub _long_desc_changedbefore { + my $self = shift; + my %func_args = @_; + my ($chartid, $supptables, $term, $v) = + @func_args{qw(chartid supptables term v)}; + my $dbh = Bugzilla->dbh; + + my $table = "longdescs_$$chartid"; + push(@$supptables, "LEFT JOIN longdescs AS $table " . + "ON $table.bug_id = bugs.bug_id"); + $$term = "$table.bug_when < " . $dbh->quote(SqlifyDate($$v)); +} + +sub _long_desc_changedafter { + my $self = shift; + my %func_args = @_; + my ($chartid, $supptables, $term, $v) = + @func_args{qw(chartid supptables term v)}; + my $dbh = Bugzilla->dbh; + + my $table = "longdescs_$$chartid"; + push(@$supptables, "LEFT JOIN longdescs AS $table " . + "ON $table.bug_id = bugs.bug_id"); + $$term = "$table.bug_when > " . $dbh->quote(SqlifyDate($$v)); +} + +sub _content_matches { + my $self = shift; + my %func_args = @_; + my ($chartid, $supptables, $term, $groupby, $fields, $v) = + @func_args{qw(chartid supptables term groupby fields v)}; + my $dbh = Bugzilla->dbh; + + # "content" is an alias for columns containing text for which we + # can search a full-text index and retrieve results by relevance, + # currently just bug comments (and summaries to some degree). + # There's only one way to search a full-text index, so we only + # accept the "matches" operator, which is specific to full-text + # index searches. + + # Add the longdescs table to the query so we can search comments. + my $table = "longdescs_$$chartid"; + my $extra = ""; + if (Bugzilla->params->{"insidergroup"} + && !Bugzilla->user->in_group(Bugzilla->params->{"insidergroup"})) + { + $extra = "AND $table.isprivate < 1"; + } + push(@$supptables, "LEFT JOIN longdescs AS $table " . + "ON bugs.bug_id = $table.bug_id $extra"); + + # Create search terms to add to the SELECT and WHERE clauses. + # $term1 searches comments. + my $term1 = $dbh->sql_fulltext_search("${table}.thetext", $$v); + + # short_desc searching for the WHERE clause + my @words = _split_words_into_like('bugs.short_desc', $$v); + my $term2_where = join(' OR ', @words); + + # short_desc relevance + my $factor = SUMMARY_RELEVANCE_FACTOR; + my @s_words = map("CASE WHEN $_ THEN $factor ELSE 0 END", @words); + my $term2_select = join(' + ', @s_words); + + # The term to use in the WHERE clause. + $$term = "$term1 > 0 OR ($term2_where)"; + + # In order to sort by relevance (in case the user requests it), + # we SELECT the relevance value and give it an alias so we can + # add it to the SORT BY clause when we build it in buglist.cgi. + # + # Note: We should be calculating the relevance based on all + # comments for a bug, not just matching comments, but that's hard + # (see http://bugzilla.mozilla.org/show_bug.cgi?id=145588#c35). + my $select_term = "(SUM($term1) + $term2_select) AS relevance"; + + # add the column not used in aggregate function explicitly + push(@$groupby, 'bugs.short_desc'); + + # Users can specify to display the relevance field, in which case + # it'll show up in the list of fields being selected, and we need + # to replace that occurrence with our select term. Otherwise + # we can just add the term to the list of fields being selected. + if (grep($_ eq "relevance", @$fields)) { + @$fields = map($_ eq "relevance" ? $select_term : $_ , @$fields); + } + else { + push(@$fields, $select_term); + } +} + +sub _timestamp_compare { + my $self = shift; + my %func_args = @_; + my ($v, $q) = @func_args{qw(v q)}; + my $dbh = Bugzilla->dbh; + + $$v = SqlifyDate($$v); + $$q = $dbh->quote($$v); +} + +sub _commenter_exact { + my $self = shift; + my %func_args = @_; + my ($chartid, $sequence, $supptables, $term, $v) = + @func_args{qw(chartid sequence supptables term v)}; + my $user = $self->{'user'}; + + $$v =~ m/(%\\w+%)/; + my $match = pronoun($1, $user); + my $chartseq = $$chartid; + if ($$chartid eq "") { + $chartseq = "LD$$sequence"; + $$sequence++; + } + my $table = "longdescs_$chartseq"; + my $extra = ""; + if (Bugzilla->params->{"insidergroup"} + && !Bugzilla->user->in_group(Bugzilla->params->{"insidergroup"})) + { + $extra = "AND $table.isprivate < 1"; + } + push(@$supptables, "LEFT JOIN longdescs AS $table " . + "ON $table.bug_id = bugs.bug_id $extra " . + "AND $table.who IN ($match)"); + $$term = "$table.who IS NOT NULL"; +} + +sub _commenter { + my $self = shift; + my %func_args = @_; + my ($chartid, $sequence, $supptables, $f, $ff, $t, $funcsbykey, $term) = + @func_args{qw(chartid sequence supptables f ff t funcsbykey term)}; + + my $chartseq = $$chartid; + if ($$chartid eq "") { + $chartseq = "LD$$sequence"; + $$sequence++; + } + my $table = "longdescs_$chartseq"; + my $extra = ""; + if (Bugzilla->params->{"insidergroup"} + && !Bugzilla->user->in_group(Bugzilla->params->{"insidergroup"})) + { + $extra = "AND $table.isprivate < 1"; + } + $$f = "login_name"; + $$ff = "profiles.login_name"; + $$funcsbykey{",$$t"}($self, %func_args); + push(@$supptables, "LEFT JOIN longdescs AS $table " . + "ON $table.bug_id = bugs.bug_id $extra " . + "AND $table.who IN" . + "(SELECT userid FROM profiles WHERE $$term)" + ); + $$term = "$table.who IS NOT NULL"; +} + +sub _long_desc { + my $self = shift; + my %func_args = @_; + my ($chartid, $supptables, $f) = + @func_args{qw(chartid supptables f)}; + + my $table = "longdescs_$$chartid"; + my $extra = ""; + if (Bugzilla->params->{"insidergroup"} + && !Bugzilla->user->in_group(Bugzilla->params->{"insidergroup"})) + { + $extra = "AND $table.isprivate < 1"; + } + push(@$supptables, "LEFT JOIN longdescs AS $table " . + "ON $table.bug_id = bugs.bug_id $extra"); + $$f = "$table.thetext"; +} + +sub _longdescs_isprivate { + my $self = shift; + my %func_args = @_; + my ($chartid, $supptables, $f) = + @func_args{qw(chartid supptables f)}; + + my $table = "longdescs_$$chartid"; + my $extra = ""; + if (Bugzilla->params->{"insidergroup"} + && !Bugzilla->user->in_group(Bugzilla->params->{"insidergroup"})) + { + $extra = "AND $table.isprivate < 1"; + } + push(@$supptables, "LEFT JOIN longdescs AS $table " . + "ON $table.bug_id = bugs.bug_id $extra"); + $$f = "$table.isprivate"; +} + +sub _work_time_changedby { + my $self = shift; + my %func_args = @_; + my ($chartid, $supptables, $v, $term) = + @func_args{qw(chartid supptables v term)}; + + my $table = "longdescs_$$chartid"; + push(@$supptables, "LEFT JOIN longdescs AS $table " . + "ON $table.bug_id = bugs.bug_id"); + my $id = login_to_id($$v, THROW_ERROR); + $$term = "(($table.who = $id"; + $$term .= ") AND ($table.work_time <> 0))"; +} + +sub _work_time_changedbefore { + my $self = shift; + my %func_args = @_; + my ($chartid, $supptables, $v, $term) = + @func_args{qw(chartid supptables v term)}; + my $dbh = Bugzilla->dbh; + + my $table = "longdescs_$$chartid"; + push(@$supptables, "LEFT JOIN longdescs AS $table " . + "ON $table.bug_id = bugs.bug_id"); + $$term = "(($table.bug_when < " . $dbh->quote(SqlifyDate($$v)); + $$term .= ") AND ($table.work_time <> 0))"; +} + +sub _work_time_changedafter { + my $self = shift; + my %func_args = @_; + my ($chartid, $supptables, $v, $term) = + @func_args{qw(chartid supptables v term)}; + my $dbh = Bugzilla->dbh; + + my $table = "longdescs_$$chartid"; + push(@$supptables, "LEFT JOIN longdescs AS $table " . + "ON $table.bug_id = bugs.bug_id"); + $$term = "(($table.bug_when > " . $dbh->quote(SqlifyDate($$v)); + $$term .= ") AND ($table.work_time <> 0))"; +} + +sub _work_time { + my $self = shift; + my %func_args = @_; + my ($chartid, $supptables, $f) = + @func_args{qw(chartid supptables f)}; + + my $table = "longdescs_$$chartid"; + push(@$supptables, "LEFT JOIN longdescs AS $table " . + "ON $table.bug_id = bugs.bug_id"); + $$f = "$table.work_time"; +} + +sub _percentage_complete { + my $self = shift; + my %func_args = @_; + my ($t, $chartid, $supptables, $fields, $q, $v, $having, $groupby, $term) = + @func_args{qw(t chartid supptables fields q v having groupby term)}; + my $dbh = Bugzilla->dbh; + + my $oper; + if ($$t eq "equals") { + $oper = "="; + } elsif ($$t eq "greaterthan") { + $oper = ">"; + } elsif ($$t eq "lessthan") { + $oper = "<"; + } elsif ($$t eq "notequal") { + $oper = "<>"; + } elsif ($$t eq "regexp") { + # This is just a dummy to help catch bugs- $oper won't be used + # since "regexp" is treated as a special case below. But + # leaving $oper uninitialized seems risky... + $oper = "sql_regexp"; + } elsif ($$t eq "notregexp") { + # This is just a dummy to help catch bugs- $oper won't be used + # since "notregexp" is treated as a special case below. But + # leaving $oper uninitialized seems risky... + $oper = "sql_not_regexp"; + } else { + $oper = "noop"; + } + if ($oper ne "noop") { + my $table = "longdescs_$$chartid"; + if(lsearch(@$fields, "bugs.remaining_time") == -1) { + push(@$fields, "bugs.remaining_time"); + } + push(@$supptables, "LEFT JOIN longdescs AS $table " . + "ON $table.bug_id = bugs.bug_id"); + my $expression = "(100 * ((SUM($table.work_time) * + COUNT(DISTINCT $table.bug_when) / + COUNT(bugs.bug_id)) / + ((SUM($table.work_time) * + COUNT(DISTINCT $table.bug_when) / + COUNT(bugs.bug_id)) + + bugs.remaining_time)))"; + $$q = $dbh->quote($$v); + trick_taint($$q); + if ($$t eq "regexp") { + push(@$having, $dbh->sql_regexp($expression, $$q)); + } elsif ($$t eq "notregexp") { + push(@$having, $dbh->sql_not_regexp($expression, $$q)); + } else { + push(@$having, "$expression $oper " . $$q); + } + push(@$groupby, "bugs.remaining_time"); + } + $$term = "0=0"; +} + +sub _bug_group_nonchanged { + my $self = shift; + my %func_args = @_; + my ($supptables, $chartid, $ff, $f, $t, $funcsbykey, $term) = + @func_args{qw(supptables chartid ff f t funcsbykey term)}; + + push(@$supptables, + "LEFT JOIN bug_group_map AS bug_group_map_$$chartid " . + "ON bugs.bug_id = bug_group_map_$$chartid.bug_id"); + $$ff = $$f = "groups_$$chartid.name"; + $$funcsbykey{",$$t"}($self, %func_args); + push(@$supptables, + "LEFT JOIN groups AS groups_$$chartid " . + "ON groups_$$chartid.id = bug_group_map_$$chartid.group_id " . + "AND $$term"); + $$term = "$$ff IS NOT NULL"; +} + +sub _attach_data_thedata_changed { + my $self = shift; + my %func_args = @_; + my ($f) = @func_args{qw(f)}; + + # Searches for attachment data's change must search + # the creation timestamp of the attachment instead. + $$f = "attachments.whocares"; +} + +sub _attach_data_thedata { + my $self = shift; + my %func_args = @_; + my ($chartid, $supptables, $f) = + @func_args{qw(chartid supptables f)}; + + my $atable = "attachments_$$chartid"; + my $dtable = "attachdata_$$chartid"; + my $extra = ""; + if (Bugzilla->params->{"insidergroup"} + && !Bugzilla->user->in_group(Bugzilla->params->{"insidergroup"})) + { + $extra = "AND $atable.isprivate = 0"; + } + push(@$supptables, "INNER JOIN attachments AS $atable " . + "ON bugs.bug_id = $atable.bug_id $extra"); + push(@$supptables, "INNER JOIN attach_data AS $dtable " . + "ON $dtable.id = $atable.attach_id"); + $$f = "$dtable.thedata"; +} + +sub _attachments_submitter { + my $self = shift; + my %func_args = @_; + my ($chartid, $supptables, $f) = + @func_args{qw(chartid supptables f)}; + + my $atable = "map_attachment_submitter_$$chartid"; + my $extra = ""; + if (Bugzilla->params->{"insidergroup"} + && !Bugzilla->user->in_group(Bugzilla->params->{"insidergroup"})) + { + $extra = "AND $atable.isprivate = 0"; + } + push(@$supptables, "INNER JOIN attachments AS $atable " . + "ON bugs.bug_id = $atable.bug_id $extra"); + push(@$supptables, "LEFT JOIN profiles AS attachers_$$chartid " . + "ON $atable.submitter_id = attachers_$$chartid.userid"); + $$f = "attachers_$$chartid.login_name"; +} + +sub _attachments { + my $self = shift; + my %func_args = @_; + my ($chartid, $supptables, $f, $t, $v, $q) = + @func_args{qw(chartid supptables f t v q)}; + my $dbh = Bugzilla->dbh; + + my $table = "attachments_$$chartid"; + my $extra = ""; + if (Bugzilla->params->{"insidergroup"} + && !Bugzilla->user->in_group(Bugzilla->params->{"insidergroup"})) + { + $extra = "AND $table.isprivate = 0"; + } + push(@$supptables, "INNER JOIN attachments AS $table " . + "ON bugs.bug_id = $table.bug_id $extra"); + $$f =~ m/^attachments\.(.*)$/; + my $field = $1; + if ($$t eq "changedby") { + $$v = login_to_id($$v, THROW_ERROR); + $$q = $dbh->quote($$v); + $field = "submitter_id"; + $$t = "equals"; + } elsif ($$t eq "changedbefore") { + $$v = SqlifyDate($$v); + $$q = $dbh->quote($$v); + $field = "creation_ts"; + $$t = "lessthan"; + } elsif ($$t eq "changedafter") { + $$v = SqlifyDate($$v); + $$q = $dbh->quote($$v); + $field = "creation_ts"; + $$t = "greaterthan"; + } + if ($field eq "ispatch" && $$v ne "0" && $$v ne "1") { + ThrowUserError("illegal_attachment_is_patch"); + } + if ($field eq "isobsolete" && $$v ne "0" && $$v ne "1") { + ThrowUserError("illegal_is_obsolete"); + } + $$f = "$table.$field"; +} + +sub _flagtypes_name { + my $self = shift; + my %func_args = @_; + my ($t, $chartid, $supptables, $ff, $funcsbykey, $having, $term) = + @func_args{qw(t chartid supptables ff funcsbykey having term)}; + my $dbh = Bugzilla->dbh; + + # Matches bugs by flag name/status. + # Note that--for the purposes of querying--a flag comprises + # its name plus its status (i.e. a flag named "review" + # with a status of "+" can be found by searching for "review+"). + + # Don't do anything if this condition is about changes to flags, + # as the generic change condition processors can handle those. + return if ($$t =~ m/^changed/); + + # Add the flags and flagtypes tables to the query. We do + # a left join here so bugs without any flags still match + # negative conditions (f.e. "flag isn't review+"). + my $flags = "flags_$$chartid"; + push(@$supptables, "LEFT JOIN flags AS $flags " . + "ON bugs.bug_id = $flags.bug_id "); + my $flagtypes = "flagtypes_$$chartid"; + push(@$supptables, "LEFT JOIN flagtypes AS $flagtypes " . + "ON $flags.type_id = $flagtypes.id"); + + # Generate the condition by running the operator-specific + # function. Afterwards the condition resides in the global $term + # variable. + $$ff = $dbh->sql_string_concat("${flagtypes}.name", + "$flags.status"); + $$funcsbykey{",$$t"}($self, %func_args); + + # If this is a negative condition (f.e. flag isn't "review+"), + # we only want bugs where all flags match the condition, not + # those where any flag matches, which needs special magic. + # Instead of adding the condition to the WHERE clause, we select + # the number of flags matching the condition and the total number + # of flags on each bug, then compare them in a HAVING clause. + # If the numbers are the same, all flags match the condition, + # so this bug should be included. + if ($$t =~ m/not/) { + push(@$having, + "SUM(CASE WHEN $$ff IS NOT NULL THEN 1 ELSE 0 END) = " . + "SUM(CASE WHEN $$term THEN 1 ELSE 0 END)"); + $$term = "0=0"; + } +} + +sub _requestees_login_name { + my $self = shift; + my %func_args = @_; + my ($f, $chartid, $supptables) = @func_args{qw(f chartid supptables)}; + + my $flags = "flags_$$chartid"; + push(@$supptables, "LEFT JOIN flags AS $flags " . + "ON bugs.bug_id = $flags.bug_id "); + push(@$supptables, "LEFT JOIN profiles AS requestees_$$chartid " . + "ON $flags.requestee_id = requestees_$$chartid.userid"); + $$f = "requestees_$$chartid.login_name"; +} + +sub _setters_login_name { + my $self = shift; + my %func_args = @_; + my ($f, $chartid, $supptables) = @func_args{qw(f chartid supptables)}; + + my $flags = "flags_$$chartid"; + push(@$supptables, "LEFT JOIN flags AS $flags " . + "ON bugs.bug_id = $flags.bug_id "); + push(@$supptables, "LEFT JOIN profiles AS setters_$$chartid " . + "ON $flags.setter_id = setters_$$chartid.userid"); + $$f = "setters_$$chartid.login_name"; +} + +sub _changedin_days_elapsed { + my $self = shift; + my %func_args = @_; + my ($f) = @func_args{qw(f)}; + my $dbh = Bugzilla->dbh; + + $$f = "(" . $dbh->sql_to_days('NOW()') . " - " . + $dbh->sql_to_days('bugs.delta_ts') . ")"; +} + +sub _component_nonchanged { + my $self = shift; + my %func_args = @_; + my ($f, $ff, $t, $funcsbykey, $term) = + @func_args{qw(f ff t funcsbykey term)}; + + $$f = $$ff = "components.name"; + $$funcsbykey{",$$t"}($self, %func_args); + $$term = build_subselect("bugs.component_id", + "components.id", + "components", + $$term); +} +sub _product_nonchanged { + my $self = shift; + my %func_args = @_; + my ($f, $ff, $t, $funcsbykey, $term) = + @func_args{qw(f ff t funcsbykey term)}; + + # Generate the restriction condition + $$f = $$ff = "products.name"; + $$funcsbykey{",$$t"}($self, %func_args); + $$term = build_subselect("bugs.product_id", + "products.id", + "products", + $$term); +} + +sub _classification_nonchanged { + my $self = shift; + my %func_args = @_; + my ($chartid, $v, $ff, $f, $funcsbykey, $t, $supptables, $term) = + @func_args{qw(chartid v ff f funcsbykey t supptables term)}; + + # Generate the restriction condition + push @$supptables, "INNER JOIN products AS map_products " . + "ON bugs.product_id = map_products.id"; + $$f = $$ff = "classifications.name"; + $$funcsbykey{",$$t"}($self, %func_args); + $$term = build_subselect("map_products.classification_id", + "classifications.id", + "classifications", + $$term); +} + +sub _keywords_nonchanged { + my $self = shift; + my %func_args = @_; + my ($chartid, $v, $ff, $f, $t, $term, $supptables) = + @func_args{qw(chartid v ff f t term)}; + + my @list; + my $table = "keywords_$$chartid"; + foreach my $value (split(/[\s,]+/, $$v)) { + if ($value eq '') { + next; + } + my $keyword = new Bugzilla::Keyword({name => $value}); + if ($keyword) { + push(@list, "$table.keywordid = " . $keyword->id); + } + else { + ThrowUserError("unknown_keyword", + { keyword => $$v }); + } + } + my $haveawordterm; + if (@list) { + $haveawordterm = "(" . join(' OR ', @list) . ")"; + if ($$t eq "anywords") { + $$term = $haveawordterm; + } elsif ($$t eq "allwords") { + $self->_allwords; + if ($$term && $haveawordterm) { + $$term = "(($$term) AND $haveawordterm)"; + } + } + } + if ($$term) { + push(@$supptables, "LEFT JOIN keywords AS $table " . + "ON $table.bug_id = bugs.bug_id"); + } +} + +sub _dependson_nonchanged { + my $self = shift; + my %func_args = @_; + my ($chartid, $ff, $f, $funcsbykey, $t, $term, $supptables) = + @func_args{qw(chartid ff f funcsbykey t term supptables)}; + + my $table = "dependson_" . $$chartid; + $$ff = "$table.$$f"; + $$funcsbykey{",$$t"}($self, %func_args); + push(@$supptables, "LEFT JOIN dependencies AS $table " . + "ON $table.blocked = bugs.bug_id " . + "AND ($$term)"); + $$term = "$$ff IS NOT NULL"; +} + +sub _blocked_nonchanged { + my $self = shift; + my %func_args = @_; + my ($chartid, $ff, $f, $funcsbykey, $t, $term, $supptables) = + @func_args{qw(chartid ff f funcsbykey t term supptables)}; + + my $table = "blocked_" . $$chartid; + $$ff = "$table.$$f"; + $$funcsbykey{",$$t"}($self, %func_args); + push(@$supptables, "LEFT JOIN dependencies AS $table " . + "ON $table.dependson = bugs.bug_id " . + "AND ($$term)"); + $$term = "$$ff IS NOT NULL"; +} + +sub _alias_nonchanged { + my $self = shift; + my %func_args = @_; + my ($ff, $funcsbykey, $t, $term) = + @func_args{qw(ff funcsbykey t term)}; + + $$ff = "COALESCE(bugs.alias, '')"; + + $$funcsbykey{",$$t"}($self, %func_args); +} + +sub _owner_idle_time_greater_less { + my $self = shift; + my %func_args = @_; + my ($chartid, $v, $supptables, $t, $wherepart, $term) = + @func_args{qw(chartid v supptables t wherepart term)}; + my $dbh = Bugzilla->dbh; + + my $table = "idle_" . $$chartid; + $$v =~ /^(\d+)\s*([hHdDwWmMyY])?$/; + my $quantity = $1; + my $unit = lc $2; + my $unitinterval = 'DAY'; + if ($unit eq 'h') { + $unitinterval = 'HOUR'; + } elsif ($unit eq 'w') { + $unitinterval = ' * 7 DAY'; + } elsif ($unit eq 'm') { + $unitinterval = 'MONTH'; + } elsif ($unit eq 'y') { + $unitinterval = 'YEAR'; + } + my $cutoff = "NOW() - " . + $dbh->sql_interval($quantity, $unitinterval); + my $assigned_fieldid = get_field_id('assigned_to'); + push(@$supptables, "LEFT JOIN longdescs AS comment_$table " . + "ON comment_$table.who = bugs.assigned_to " . + "AND comment_$table.bug_id = bugs.bug_id " . + "AND comment_$table.bug_when > $cutoff"); + push(@$supptables, "LEFT JOIN bugs_activity AS activity_$table " . + "ON (activity_$table.who = bugs.assigned_to " . + "OR activity_$table.fieldid = $assigned_fieldid) " . + "AND activity_$table.bug_id = bugs.bug_id " . + "AND activity_$table.bug_when > $cutoff"); + if ($$t =~ /greater/) { + push(@$wherepart, "(comment_$table.who IS NULL " . + "AND activity_$table.who IS NULL)"); + } else { + push(@$wherepart, "(comment_$table.who IS NOT NULL " . + "OR activity_$table.who IS NOT NULL)"); + } + $$term = "0=0"; +} + +sub _equals { + my $self = shift; + my %func_args = @_; + my ($ff, $q, $term) = @func_args{qw(ff q term)}; + + $$term = "$$ff = $$q"; +} + +sub _notequals { + my $self = shift; + my %func_args = @_; + my ($ff, $q, $term) = @func_args{qw(ff q term)}; + + $$term = "$$ff != $$q"; +} + +sub _casesubstring { + my $self = shift; + my %func_args = @_; + my ($ff, $q, $term) = @func_args{qw(ff q term)}; + my $dbh = Bugzilla->dbh; + + $$term = $dbh->sql_position($$q, $$ff) . " > 0"; +} + +sub _substring { + my $self = shift; + my %func_args = @_; + my ($ff, $q, $term) = @func_args{qw(ff q term)}; + my $dbh = Bugzilla->dbh; + + $$term = $dbh->sql_position(lc($$q), "LOWER($$ff)") . " > 0"; +} + +sub _notsubstring { + my $self = shift; + my %func_args = @_; + my ($ff, $q, $term) = @func_args{qw(ff q term)}; + my $dbh = Bugzilla->dbh; + + $$term = $dbh->sql_position(lc($$q), "LOWER($$ff)") . " = 0"; +} + +sub _regexp { + my $self = shift; + my %func_args = @_; + my ($ff, $q, $term) = @func_args{qw(ff q term)}; + my $dbh = Bugzilla->dbh; + + $$term = $dbh->sql_regexp($$ff, $$q); +} + +sub _notregexp { + my $self = shift; + my %func_args = @_; + my ($ff, $q, $term) = @func_args{qw(ff q term)}; + my $dbh = Bugzilla->dbh; + + $$term = $dbh->sql_not_regexp($$ff, $$q); +} + +sub _lessthan { + my $self = shift; + my %func_args = @_; + my ($ff, $q, $term) = @func_args{qw(ff q term)}; + + $$term = "$$ff < $$q"; +} + +sub _greaterthan { + my $self = shift; + my %func_args = @_; + my ($ff, $q, $term) = @func_args{qw(ff q term)}; + + $$term = "$$ff > $$q"; +} + +sub _anyexact { + my $self = shift; + my %func_args = @_; + my ($f, $ff, $v, $q, $term) = @func_args{qw(f ff v q term)}; + my $dbh = Bugzilla->dbh; + + use Data::Dumper; + open DEBUG, ">/tmp/debug"; + print DEBUG Dumper($self); + close DEBUG; + my @list; + foreach my $w (split(/,/, $$v)) { + if ($w eq "---" && $$f !~ /resolution/) { + $w = ""; + } + $$q = $dbh->quote($w); + trick_taint($$q); + push(@list, $$q); + } + if (@list) { + $$term = $dbh->sql_in($$ff, \@list); + } +} + +sub _anywordsubstr { + my $self = shift; + my %func_args = @_; + my ($ff, $v, $term) = @func_args{qw(ff v term)}; + + $$term = join(" OR ", @{GetByWordListSubstr($$ff, $$v)}); +} + +sub _allwordssubstr { + my $self = shift; + my %func_args = @_; + my ($ff, $v, $term) = @func_args{qw(ff v term)}; + + $$term = join(" AND ", @{GetByWordListSubstr($$ff, $$v)}); +} + +sub _nowordssubstr { + my $self = shift; + my %func_args = @_; + my ($ff, $v, $term) = @func_args{qw(ff v term)}; + + my @list = @{GetByWordListSubstr($$ff, $$v)}; + if (@list) { + $$term = "NOT (" . join(" OR ", @list) . ")"; + } +} + +sub _anywords { + my $self = shift; + my %func_args = @_; + my ($ff, $v, $term) = @func_args{qw(ff v term)}; + + $$term = join(" OR ", @{GetByWordList($$ff, $$v)}); +} + +sub _allwords { + my $self = shift; + my %func_args = @_; + my ($ff, $v, $term) = @func_args{qw(ff v term)}; + + $$term = join(" AND ", @{GetByWordList($$ff, $$v)}); +} + +sub _nowords { + my $self = shift; + my %func_args = @_; + my ($ff, $v, $term) = @func_args{qw(ff v term)}; + + my @list = @{GetByWordList($$ff, $$v)}; + if (@list) { + $$term = "NOT (" . join(" OR ", @list) . ")"; + } +} + +sub _changedbefore_changedafter { + my $self = shift; + my %func_args = @_; + my ($chartid, $f, $ff, $t, $v, $chartfields, $supptables, $term) = + @func_args{qw(chartid f ff t v chartfields supptables term)}; + my $dbh = Bugzilla->dbh; + + my $operator = ($$t =~ /before/) ? '<' : '>'; + my $table = "act_$$chartid"; + my $fieldid = $$chartfields{$$f}; + if (!$fieldid) { + ThrowCodeError("invalid_field_name", {field => $$f}); + } + push(@$supptables, "LEFT JOIN bugs_activity AS $table " . + "ON $table.bug_id = bugs.bug_id " . + "AND $table.fieldid = $fieldid " . + "AND $table.bug_when $operator " . + $dbh->quote(SqlifyDate($$v)) ); + $$term = "($table.bug_when IS NOT NULL)"; +} + +sub _changedfrom_changedto { + my $self = shift; + my %func_args = @_; + my ($chartid, $chartfields, $f, $t, $v, $q, $supptables, $term) = + @func_args{qw(chartid chartfields f t v q supptables term)}; + + my $operator = ($$t =~ /from/) ? 'removed' : 'added'; + my $table = "act_$$chartid"; + my $fieldid = $$chartfields{$$f}; + if (!$fieldid) { + ThrowCodeError("invalid_field_name", {field => $$f}); + } + push(@$supptables, "LEFT JOIN bugs_activity AS $table " . + "ON $table.bug_id = bugs.bug_id " . + "AND $table.fieldid = $fieldid " . + "AND $table.$operator = $$q"); + $$term = "($table.bug_when IS NOT NULL)"; +} + +sub _changedby { + my $self = shift; + my %func_args = @_; + my ($chartid, $chartfields, $f, $v, $supptables, $term) = + @func_args{qw(chartid chartfields f v supptables term)}; + + my $table = "act_$$chartid"; + my $fieldid = $$chartfields{$$f}; + if (!$fieldid) { + ThrowCodeError("invalid_field_name", {field => $$f}); + } + my $id = login_to_id($$v, THROW_ERROR); + push(@$supptables, "LEFT JOIN bugs_activity AS $table " . + "ON $table.bug_id = bugs.bug_id " . + "AND $table.fieldid = $fieldid " . + "AND $table.who = $id"); + $$term = "($table.bug_when IS NOT NULL)"; +} + 1; |