2588 lines
90 KiB
Perl
2588 lines
90 KiB
Perl
# -*- Mode: perl; indent-tabs-mode: nil -*-
|
||
#
|
||
# The contents of this file are subject to the Mozilla Public
|
||
# License Version 1.1 (the "License"); you may not use this file
|
||
# except in compliance with the License. You may obtain a copy of
|
||
# the License at http://www.mozilla.org/MPL/
|
||
#
|
||
# Software distributed under the License is distributed on an "AS
|
||
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
|
||
# implied. See the License for the specific language governing
|
||
# rights and limitations under the License.
|
||
#
|
||
# The Original Code is the Bugzilla Bug Tracking System.
|
||
#
|
||
# The Initial Developer of the Original Code is Netscape Communications
|
||
# Corporation. Portions created by Netscape are
|
||
# Copyright (C) 1998 Netscape Communications Corporation. All
|
||
# Rights Reserved.
|
||
#
|
||
# Contributor(s): Gervase Markham <gerv@gerv.net>
|
||
# Terry Weissman <terry@mozilla.org>
|
||
# Dan Mosedale <dmose@mozilla.org>
|
||
# Stephan Niemz <st.n@gmx.net>
|
||
# Andreas Franke <afranke@mathweb.org>
|
||
# Myk Melez <myk@mozilla.org>
|
||
# Michael Schindler <michael@compressconsult.com>
|
||
# Max Kanat-Alexander <mkanat@bugzilla.org>
|
||
# Joel Peshkin <bugreport@peshkin.net>
|
||
# Lance Larsh <lance.larsh@oracle.com>
|
||
# Jesse Clark <jjclark1982@gmail.com>
|
||
# Rémi Zara <remi_zara@mac.com>
|
||
|
||
use strict;
|
||
|
||
package Bugzilla::Search;
|
||
use base qw(Exporter);
|
||
@Bugzilla::Search::EXPORT = qw(
|
||
EMPTY_COLUMN
|
||
|
||
split_order_term
|
||
translate_old_column
|
||
);
|
||
|
||
use Bugzilla::Error;
|
||
use Bugzilla::Util;
|
||
use Bugzilla::Constants;
|
||
use Bugzilla::Group;
|
||
use Bugzilla::User;
|
||
use Bugzilla::Field;
|
||
use Bugzilla::Status;
|
||
use Bugzilla::Keyword;
|
||
use Bugzilla::Search::Saved;
|
||
|
||
use Date::Format;
|
||
use Date::Parse;
|
||
|
||
# A SELECTed expression that we use as a placeholder if somebody selects
|
||
# <none> for the X, Y, or Z axis in report.cgi.
|
||
use constant EMPTY_COLUMN => '-1';
|
||
|
||
# Some fields are not sorted on themselves, but on other fields.
|
||
# We need to have a list of these fields and what they map to.
|
||
# Each field points to an array that contains the fields mapped
|
||
# to, in order.
|
||
use constant SPECIAL_ORDER => {
|
||
'target_milestone' => [ 'ms_order.sortkey','ms_order.value' ],
|
||
};
|
||
|
||
# When we add certain fields to the ORDER BY, we need to then add a
|
||
# table join to the FROM statement. This hash maps input fields to
|
||
# the join statements that need to be added.
|
||
use constant SPECIAL_ORDER_JOIN => {
|
||
'target_milestone' => 'LEFT JOIN milestones AS ms_order ON ms_order.value = bugs.target_milestone AND ms_order.product_id = bugs.product_id',
|
||
};
|
||
|
||
# This constant defines the columns that can be selected in a query
|
||
# and/or displayed in a bug list. Column records include the following
|
||
# fields:
|
||
#
|
||
# 1. id: a unique identifier by which the column is referred in code;
|
||
#
|
||
# 2. name: The name of the column in the database (may also be an expression
|
||
# that returns the value of the column);
|
||
#
|
||
# 3. title: The title of the column as displayed to users.
|
||
#
|
||
# Note: There are a few hacks in the code that deviate from these definitions.
|
||
# In particular, when the list is sorted by the "votes" field the word
|
||
# "DESC" is added to the end of the field to sort in descending order,
|
||
# and the redundant short_desc column is removed when the client
|
||
# requests "all" columns.
|
||
#
|
||
# This is really a constant--that is, once it's been called once, the value
|
||
# will always be the same unless somebody adds a new custom field. But
|
||
# we have to do a lot of work inside the subroutine to get the data,
|
||
# and we don't want it to happen at compile time, so we have it as a
|
||
# subroutine.
|
||
sub COLUMNS {
|
||
my $dbh = Bugzilla->dbh;
|
||
my $cache = Bugzilla->request_cache;
|
||
return $cache->{search_columns} if defined $cache->{search_columns};
|
||
|
||
# These are columns that don't exist in fielddefs, but are valid buglist
|
||
# columns. (Also see near the bottom of this function for the definition
|
||
# of short_short_desc.)
|
||
my %columns = (
|
||
relevance => { title => 'Relevance' },
|
||
assigned_to_realname => { title => 'Assignee' },
|
||
reporter_realname => { title => 'Reporter' },
|
||
qa_contact_realname => { title => 'QA Contact' },
|
||
);
|
||
|
||
# Next we define columns that have special SQL instead of just something
|
||
# like "bugs.bug_id".
|
||
|
||
# ---- vfilippov@custis.ru 2010-02-02
|
||
# The previous sql code:
|
||
# "(SUM(ldtime.work_time) * COUNT(DISTINCT ldtime.bug_when)/COUNT(bugs.bug_id))"
|
||
# was probably written by Australopithecus.
|
||
my $actual_time = '(SELECT SUM(ldtime.work_time) FROM longdescs ldtime WHERE ldtime.bug_id=bugs.bug_id)';
|
||
my %special_sql = (
|
||
deadline => $dbh->sql_date_format('bugs.deadline', '%Y-%m-%d'),
|
||
actual_time => $actual_time,
|
||
|
||
percentage_complete =>
|
||
"(CASE WHEN $actual_time + bugs.remaining_time = 0.0"
|
||
. " THEN 0.0"
|
||
. " ELSE 100"
|
||
. " * ($actual_time / ($actual_time + bugs.remaining_time))"
|
||
. " END)",
|
||
|
||
'flagtypes.name' => $dbh->sql_group_concat('DISTINCT '
|
||
. $dbh->sql_string_concat('flagtypes.name', 'flags.status'), "', '"),
|
||
);
|
||
|
||
# Backward-compatibility for old field names. Goes new_name => old_name.
|
||
# These are here and not in translate_old_column because the rest of the
|
||
# code actually still uses the old names, while the fielddefs table uses
|
||
# the new names (which is not the case for the fields handled by
|
||
# translate_old_column).
|
||
my %old_names = (
|
||
creation_ts => 'opendate',
|
||
delta_ts => 'changeddate',
|
||
work_time => 'actual_time',
|
||
);
|
||
|
||
# Fields that are email addresses
|
||
my @email_fields = qw(assigned_to reporter qa_contact);
|
||
# Other fields that are stored in the bugs table as an id, but
|
||
# should be displayed using their name.
|
||
my @id_fields = qw(product component classification);
|
||
|
||
foreach my $col (@email_fields) {
|
||
my $sql = "map_${col}.login_name";
|
||
if (!Bugzilla->user->id) {
|
||
$sql = $dbh->sql_string_until($sql, $dbh->quote('@'));
|
||
}
|
||
$special_sql{$col} = $sql;
|
||
$columns{"${col}_realname"}->{name} = "map_${col}.realname";
|
||
}
|
||
|
||
foreach my $col (@id_fields) {
|
||
$special_sql{$col} = "map_${col}s.name";
|
||
}
|
||
|
||
# Do the actual column-getting from fielddefs, now.
|
||
foreach my $field (Bugzilla->get_fields({ obsolete => 0, buglist => 1 })) {
|
||
my $id = $field->name;
|
||
$id = $old_names{$id} if exists $old_names{$id};
|
||
my $sql = 'bugs.' . $field->name;
|
||
$sql = $special_sql{$id} if exists $special_sql{$id};
|
||
$columns{$id} = { name => $sql, title => $field->description };
|
||
}
|
||
|
||
# The short_short_desc column is identical to short_desc
|
||
$columns{'short_short_desc'} = $columns{'short_desc'};
|
||
|
||
Bugzilla::Hook::process('buglist_columns', { columns => \%columns });
|
||
|
||
$cache->{search_columns} = \%columns;
|
||
return $cache->{search_columns};
|
||
}
|
||
|
||
# Create a new Search
|
||
# Note that the param argument may be modified by Bugzilla::Search
|
||
sub new {
|
||
my $invocant = shift;
|
||
my $class = ref($invocant) || $invocant;
|
||
|
||
my $self = { @_ };
|
||
bless($self, $class);
|
||
|
||
$self->init();
|
||
|
||
return $self;
|
||
}
|
||
|
||
# Fields for Boolean Charts
|
||
sub CHART_FIELDS
|
||
{
|
||
if (!Bugzilla->request_cache->{search_chart_fields})
|
||
{
|
||
my @fielddefs = Bugzilla->get_fields({obsolete => 0});
|
||
my %searchcols = %{ COLUMNS() };
|
||
for (@fielddefs)
|
||
{
|
||
delete $searchcols{$_->name};
|
||
delete $searchcols{$_->name.'_realname'};
|
||
}
|
||
for (keys %searchcols)
|
||
{
|
||
push @fielddefs, { name => $_, description => $searchcols{$_}{title} } if $searchcols{$_}{name};
|
||
}
|
||
my $hash = { map { $_->{name} => $_ } @fielddefs };
|
||
if (!Bugzilla->user->is_timetracker)
|
||
{
|
||
delete $hash->{$_} for TIMETRACKING_FIELDS;
|
||
}
|
||
@fielddefs = sort { lc($a->{description}) cmp lc($b->{description}) } values %$hash;
|
||
Bugzilla->request_cache->{search_chart_fields} = \@fielddefs;
|
||
Bugzilla->request_cache->{search_chart_fields_hash} = $hash;
|
||
}
|
||
return Bugzilla->request_cache->{search_chart_fields};
|
||
}
|
||
|
||
sub CHART_FIELDS_HASH
|
||
{
|
||
CHART_FIELDS();
|
||
return Bugzilla->request_cache->{search_chart_fields_hash};
|
||
}
|
||
|
||
sub CHANGEDFROMTO_FIELDS
|
||
{
|
||
if (!Bugzilla->request_cache->{search_changedfromto_fields})
|
||
{
|
||
my $ids = Bugzilla->dbh->selectcol_arrayref('SELECT DISTINCT fieldid FROM bugs_activity');
|
||
my $tt_fields = Bugzilla->user->is_timetracker ? { map { $_ => 1 } TIMETRACKING_FIELDS } : {};
|
||
my $fields = [
|
||
sort { $a->description cmp $b->description }
|
||
grep { !$tt_fields->{$_->name} }
|
||
map { Bugzilla->get_field($_) } @$ids
|
||
];
|
||
Bugzilla->request_cache->{search_changedfromto_fields} = $fields;
|
||
}
|
||
return Bugzilla->request_cache->{search_changedfromto_fields};
|
||
}
|
||
|
||
sub init {
|
||
my $self = shift;
|
||
my @fields = @{ $self->{'fields'} || [] };
|
||
my $params = $self->{'params'};
|
||
$params->convert_old_params();
|
||
$self->{'user'} ||= Bugzilla->user;
|
||
my $user = $self->{'user'};
|
||
|
||
my @inputorder = @{ $self->{'order'} || [] };
|
||
my @orderby;
|
||
|
||
my @supptables;
|
||
my @wherepart;
|
||
my @having;
|
||
my @groupby;
|
||
my @specialchart;
|
||
my @andlist;
|
||
|
||
my %special_order = %{SPECIAL_ORDER()};
|
||
my %special_order_join = %{SPECIAL_ORDER_JOIN()};
|
||
|
||
my @select_fields =
|
||
Bugzilla->get_fields({ type => FIELD_TYPE_SINGLE_SELECT });
|
||
|
||
my @multi_select_fields = Bugzilla->get_fields({
|
||
type => [FIELD_TYPE_MULTI_SELECT, FIELD_TYPE_BUG_URLS],
|
||
obsolete => 0 });
|
||
foreach my $field (@select_fields) {
|
||
my $name = $field->name;
|
||
next if $name eq 'product'; # products don't have sortkeys.
|
||
$special_order{$name} = [ "$name.sortkey", "$name.value" ],
|
||
$special_order_join{$name} =
|
||
"LEFT JOIN $name ON $name.value = bugs.$name";
|
||
}
|
||
|
||
my $dbh = Bugzilla->dbh;
|
||
|
||
# All items that are in the ORDER BY must be in the SELECT.
|
||
foreach my $orderitem (@inputorder) {
|
||
my $column_name = split_order_term($orderitem);
|
||
if (!grep($_ eq $column_name, @fields)) {
|
||
push(@fields, $column_name);
|
||
}
|
||
}
|
||
|
||
# First, deal with all the old hard-coded non-chart-based poop.
|
||
if (grep(/^assigned_to/, @fields)) {
|
||
push @supptables, "INNER JOIN profiles AS map_assigned_to " .
|
||
"ON bugs.assigned_to = map_assigned_to.userid";
|
||
}
|
||
|
||
if (grep(/^reporter/, @fields)) {
|
||
push @supptables, "INNER JOIN profiles AS map_reporter " .
|
||
"ON bugs.reporter = map_reporter.userid";
|
||
}
|
||
|
||
if (grep(/^qa_contact/, @fields)) {
|
||
push @supptables, "LEFT JOIN profiles AS map_qa_contact " .
|
||
"ON bugs.qa_contact = map_qa_contact.userid";
|
||
}
|
||
|
||
if (grep($_ eq 'product' || $_ eq 'classification', @fields))
|
||
{
|
||
push @supptables, "INNER JOIN products AS map_products " .
|
||
"ON bugs.product_id = map_products.id";
|
||
}
|
||
|
||
if (grep($_ eq 'classification', @fields)) {
|
||
push @supptables,
|
||
"INNER JOIN classifications AS map_classifications " .
|
||
"ON map_products.classification_id = map_classifications.id";
|
||
}
|
||
|
||
if (grep($_ eq 'component', @fields)) {
|
||
push @supptables, "INNER JOIN components AS map_components " .
|
||
"ON bugs.component_id = map_components.id";
|
||
}
|
||
|
||
### Testopia ###
|
||
if (grep($_ eq 'test_cases', @fields)){
|
||
push(@supptables, "LEFT JOIN test_case_bugs AS tcb " .
|
||
"ON bugs.bug_id = tcb.bug_id ");
|
||
}
|
||
### end Testopia ###
|
||
|
||
if (grep($_ eq 'flagtypes.name', @fields)) {
|
||
push(@supptables, "LEFT JOIN flags ON flags.bug_id = bugs.bug_id AND attach_id IS NULL");
|
||
push(@supptables, "LEFT JOIN flagtypes ON flagtypes.id = flags.type_id");
|
||
}
|
||
|
||
my $minvotes;
|
||
if (defined $params->param('votes')) {
|
||
my $c = trim($params->param('votes'));
|
||
if ($c ne "") {
|
||
if ($c !~ /^[0-9]*$/) {
|
||
ThrowUserError("illegal_at_least_x_votes",
|
||
{ value => $c });
|
||
}
|
||
push(@specialchart, ["votes", "greaterthan", $c - 1]);
|
||
}
|
||
}
|
||
|
||
# If the user has selected all of either status or resolution, change to
|
||
# selecting none. This is functionally equivalent, but quite a lot faster.
|
||
# Also, if the status is __open__ or __closed__, translate those
|
||
# into their equivalent lists of open and closed statuses.
|
||
if ($params->param('bug_status')) {
|
||
my @bug_statuses = $params->param('bug_status');
|
||
# Also include inactive bug statuses, as you can query them.
|
||
my @legal_statuses =
|
||
map {$_->name} @{Bugzilla->get_field('bug_status')->legal_values};
|
||
|
||
# Filter out any statuses that have been removed completely that are still
|
||
# being used by the client
|
||
my @valid_statuses;
|
||
foreach my $status (@bug_statuses) {
|
||
push(@valid_statuses, $status) if grep($_ eq $status, @legal_statuses);
|
||
}
|
||
|
||
if (scalar(@valid_statuses) == scalar(@legal_statuses)
|
||
|| $bug_statuses[0] eq "__all__")
|
||
{
|
||
$params->delete('bug_status');
|
||
}
|
||
elsif ($bug_statuses[0] eq '__open__') {
|
||
$params->param('bug_status', grep(is_open_state($_),
|
||
@legal_statuses));
|
||
}
|
||
elsif ($bug_statuses[0] eq "__closed__") {
|
||
$params->param('bug_status', grep(!is_open_state($_),
|
||
@legal_statuses));
|
||
}
|
||
else {
|
||
$params->param('bug_status', @valid_statuses);
|
||
}
|
||
}
|
||
|
||
if ($params->param('resolution')) {
|
||
my @resolutions = $params->param('resolution');
|
||
# Also include inactive resolutions, as you can query them.
|
||
my $legal_resolutions = Bugzilla->get_field('resolution')->legal_values;
|
||
if (scalar(@resolutions) == scalar(@$legal_resolutions)) {
|
||
$params->delete('resolution');
|
||
}
|
||
}
|
||
|
||
# All fields that don't have a . in their name should be specifyable
|
||
# in the URL directly.
|
||
my @legal_fields = grep { $_->name !~ /\./ } Bugzilla->get_fields;
|
||
if (!$user->is_timetracker) {
|
||
foreach my $field (TIMETRACKING_FIELDS) {
|
||
@legal_fields = grep { $_->name ne $field } @legal_fields;
|
||
}
|
||
}
|
||
|
||
foreach my $field ($params->param()) {
|
||
# "votes" got special treatment, above.
|
||
next if $field eq 'votes';
|
||
if (grep { $_->name eq $field } @legal_fields) {
|
||
my $type = $params->param("${field}_type");
|
||
if (!$type) {
|
||
if ($field eq 'keywords') {
|
||
$type = 'anywords';
|
||
}
|
||
else {
|
||
$type = 'anyexact';
|
||
}
|
||
}
|
||
$type = 'matches' if $field eq 'content';
|
||
push(@specialchart, [$field, $type,
|
||
join(',', $params->param($field))]);
|
||
}
|
||
}
|
||
|
||
foreach my $id ("1", "2") {
|
||
if (!defined ($params->param("email$id"))) {
|
||
next;
|
||
}
|
||
my $email = trim($params->param("email$id"));
|
||
if ($email eq "") {
|
||
next;
|
||
}
|
||
my $type = $params->param("emailtype$id");
|
||
$type = "anyexact" if ($type eq "exact");
|
||
|
||
my @clist;
|
||
foreach my $field ("assigned_to", "reporter", "cc", "qa_contact") {
|
||
if ($params->param("email$field$id")) {
|
||
push(@clist, $field, $type, $email);
|
||
}
|
||
}
|
||
if ($params->param("emaillongdesc$id")) {
|
||
push(@clist, "commenter", $type, $email);
|
||
}
|
||
if (@clist) {
|
||
push(@specialchart, \@clist);
|
||
}
|
||
else {
|
||
# No field is selected. Nothing to see here.
|
||
next;
|
||
}
|
||
|
||
if ($type eq "anyexact") {
|
||
foreach my $name (split(',', $email)) {
|
||
$name = trim($name);
|
||
login_to_id($name, THROW_ERROR) if $name && lc $name ne '%user%';
|
||
}
|
||
}
|
||
}
|
||
|
||
$Bugzilla::Search::interval_from = undef;
|
||
$Bugzilla::Search::interval_to = undef;
|
||
$Bugzilla::Search::interval_who = undef;
|
||
|
||
my $chfieldfrom = trim(lc($params->param('chfieldfrom') || ''));
|
||
my $chfieldto = trim(lc($params->param('chfieldto') || ''));
|
||
$chfieldfrom = '' if ($chfieldfrom eq 'now');
|
||
$chfieldto = '' if ($chfieldto eq 'now');
|
||
my @chfield = $params->param('chfield');
|
||
my $chvalue = trim($params->param('chfieldvalue')) || '';
|
||
|
||
# 2003-05-20: The 'changedin' field is no longer in the UI, but we continue
|
||
# to process it because it will appear in stored queries and bookmarks.
|
||
my $changedin = trim($params->param('changedin')) || '';
|
||
if ($changedin) {
|
||
if ($changedin !~ /^[0-9]*$/) {
|
||
ThrowUserError("illegal_changed_in_last_x_days",
|
||
{ value => $changedin });
|
||
}
|
||
|
||
if (!$chfieldfrom
|
||
&& !$chfieldto
|
||
&& scalar(@chfield) == 1
|
||
&& $chfield[0] eq "[Bug creation]")
|
||
{
|
||
# Deal with the special case where the query is using changedin
|
||
# to get bugs created in the last n days by converting the value
|
||
# into its equivalent for the chfieldfrom parameter.
|
||
$chfieldfrom = "-" . ($changedin - 1) . "d";
|
||
}
|
||
else {
|
||
# Oh boy, the general case. Who knows why the user included
|
||
# the changedin parameter, but do our best to comply.
|
||
push(@specialchart, ["changedin", "lessthan", $changedin + 1]);
|
||
}
|
||
}
|
||
|
||
if ($chfieldfrom ne '' || $chfieldto ne '')
|
||
{
|
||
my $sql_chfrom = $chfieldfrom ? $dbh->quote(SqlifyDate($chfieldfrom)):'';
|
||
my $sql_chto = $chfieldto ? $dbh->quote(SqlifyDate($chfieldto)) :'';
|
||
my $who_term;
|
||
|
||
# do a match on user login or name
|
||
my $chfieldwho = trim(lc($params->param('chfieldwho') || ''));
|
||
if ($chfieldwho)
|
||
{
|
||
$chfieldwho = $chfieldwho eq '%user%' ? $user : Bugzilla::User::match($chfieldwho, 1)->[0];
|
||
if ($chfieldwho)
|
||
{
|
||
$Bugzilla::Search::interval_who = $chfieldwho;
|
||
$chfieldwho = $chfieldwho->id;
|
||
$who_term = " AND actcheck.who=$chfieldwho";
|
||
}
|
||
}
|
||
|
||
# CustIS Bug 68921 - a dirty hack: "interval worktime" column
|
||
$Bugzilla::Search::interval_from = SqlifyDate($chfieldfrom);
|
||
$Bugzilla::Search::interval_to = SqlifyDate($chfieldto);
|
||
COLUMNS->{interval_time} = {
|
||
name =>
|
||
"(SELECT IFNULL(SUM(ldtime.work_time),0) FROM longdescs ldtime".
|
||
" WHERE ldtime.bug_id=bugs.bug_id".
|
||
($sql_chfrom?" AND ldtime.bug_when>=$sql_chfrom":"").
|
||
($sql_chto ?" AND ldtime.bug_when<=$sql_chto":"").
|
||
($chfieldwho?" AND ldtime.who=$chfieldwho":"").
|
||
")",
|
||
title => "Interval worktime",
|
||
};
|
||
|
||
my $sql_chvalue = $chvalue ne '' ? $dbh->quote($chvalue) : '';
|
||
trick_taint($sql_chvalue);
|
||
my $from_term = " AND actcheck.bug_when >= $sql_chfrom";
|
||
my $to_term = " AND actcheck.bug_when <= $sql_chto";
|
||
my $value_term = " AND actcheck.added = $sql_chvalue";
|
||
# ---- vfilippov@custis.ru 2010-02-01
|
||
# Search using bugs.delta_ts is not correct. It's "LAST changed in", not "Changed in".
|
||
my $bug_creation_clause;
|
||
my @list;
|
||
my @actlist;
|
||
my $seen_longdesc;
|
||
my $need_commenter;
|
||
foreach my $f (@chfield)
|
||
{
|
||
my $term;
|
||
if ($f eq "[Bug creation]")
|
||
{
|
||
# Treat [Bug creation] differently because we need to look
|
||
# at bugs.creation_ts rather than the bugs_activity table.
|
||
my @l;
|
||
if ($sql_chfrom) {
|
||
my $term = "bugs.creation_ts >= $sql_chfrom";
|
||
push(@l, $term);
|
||
$self->search_description({
|
||
field => 'creation_ts', type => 'greaterthaneq',
|
||
value => $chfieldfrom, term => $term,
|
||
});
|
||
}
|
||
if ($sql_chto) {
|
||
my $term = "bugs.creation_ts <= $sql_chto";
|
||
push(@l, $term);
|
||
$self->search_description({
|
||
field => 'creation_ts', type => 'lessthaneq',
|
||
value => $chfieldto, term => $term,
|
||
});
|
||
}
|
||
$bug_creation_clause = "(" . join(' AND ', @l) . ")";
|
||
}
|
||
elsif ($f eq 'longdesc' || $f eq 'longdescs.isprivate' || $f eq 'commenter')
|
||
{
|
||
# Treat comment properties differently because we need to look at longdescs table.
|
||
if ($sql_chvalue)
|
||
{
|
||
if ($f eq 'longdesc' && !$seen_longdesc)
|
||
{
|
||
# User is searching for a comment with specific text,
|
||
# but that has no sense if $chvalue was already used for comment privacy.
|
||
$seen_longdesc = [ $term = "INSTR(actcheck_comment.thetext, $sql_chvalue) > 0" ];
|
||
}
|
||
elsif ($f eq 'commenter')
|
||
{
|
||
# User is searching for a comment with specific author
|
||
$need_commenter = $term = "actcheck_commenter.login_name = $sql_chvalue";
|
||
$value_term = " AND actcheck.who = (SELECT actcheck_profiles.userid FROM profiles actcheck_profiles WHERE actcheck_profiles.login_name = $sql_chvalue)";
|
||
$seen_longdesc = [];
|
||
}
|
||
elsif (!$seen_longdesc)
|
||
{
|
||
# User is searching for a private / non-private comment for specific period,
|
||
# but that has no sense if $chvalue was already used for comment text.
|
||
$seen_longdesc = [ $term = "actcheck_comment.isprivate = ".($chvalue ? 1 : 0) ];
|
||
}
|
||
if ($term)
|
||
{
|
||
$self->search_description({
|
||
field => $f, type => 'changedto',
|
||
value => $chvalue, term => $term,
|
||
});
|
||
}
|
||
}
|
||
else
|
||
{
|
||
$seen_longdesc = [];
|
||
}
|
||
}
|
||
else
|
||
{
|
||
push @actlist, get_field_id($f);
|
||
$term = 1;
|
||
if ($sql_chvalue)
|
||
{
|
||
$self->search_description({
|
||
field => $f, type => 'changedto',
|
||
value => $chvalue, term => $value_term,
|
||
});
|
||
}
|
||
}
|
||
if ($term)
|
||
{
|
||
if ($sql_chfrom)
|
||
{
|
||
$self->search_description({
|
||
field => $f, type => 'changedafter',
|
||
value => $chfieldfrom, term => $from_term,
|
||
});
|
||
}
|
||
if ($sql_chto)
|
||
{
|
||
$self->search_description({
|
||
field => $f, type => 'changedbefore',
|
||
value => $chfieldto, term => $to_term,
|
||
});
|
||
}
|
||
if ($chfieldwho)
|
||
{
|
||
$self->search_description({
|
||
field => $f, type => 'changedby',
|
||
value => user_id_to_login($chfieldwho), term => $who_term,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!@chfield)
|
||
{
|
||
# Add search description for the case when no field was selected
|
||
if ($sql_chfrom)
|
||
{
|
||
$self->search_description({
|
||
field => '', type => 'changedafter',
|
||
value => $chfieldfrom, term => $from_term,
|
||
});
|
||
}
|
||
if ($sql_chto)
|
||
{
|
||
$self->search_description({
|
||
field => '', type => 'changedbefore',
|
||
value => $chfieldto, term => $to_term,
|
||
});
|
||
}
|
||
if ($chfieldwho)
|
||
{
|
||
$self->search_description({
|
||
field => '', type => 'changedby',
|
||
value => user_id_to_login($chfieldwho), term => $who_term,
|
||
});
|
||
}
|
||
}
|
||
|
||
my $extra;
|
||
if (!@chfield || @actlist)
|
||
{
|
||
$extra = '1';
|
||
$extra .= $from_term if $sql_chfrom;
|
||
$extra .= $to_term if $sql_chto;
|
||
$extra .= $value_term if $sql_chvalue;
|
||
$extra .= $who_term if $who_term;
|
||
|
||
if (@actlist)
|
||
{
|
||
# Restrict bug activity to selected fields
|
||
$extra .= " AND " . $dbh->sql_in('actcheck.fieldid', \@actlist);
|
||
}
|
||
|
||
push @list, "SELECT bug_id FROM bugs_activity AS actcheck WHERE $extra";
|
||
}
|
||
|
||
# http://wiki.office.custis.ru/Bugzilla_-_оптимизация_поиска_по_изменениям
|
||
if (!@chfield || $seen_longdesc)
|
||
{
|
||
$extra = "1";
|
||
$extra .= " AND actcheck_comment.bug_when >= $sql_chfrom" if $sql_chfrom;
|
||
$extra .= " AND actcheck_comment.bug_when <= $sql_chto" if $sql_chto;
|
||
$extra .= " AND actcheck_comment.who = $chfieldwho" if $chfieldwho;
|
||
if ($seen_longdesc && @$seen_longdesc)
|
||
{
|
||
$extra .= " AND ".$seen_longdesc->[0];
|
||
}
|
||
push @list,
|
||
"SELECT bug_id FROM longdescs AS actcheck_comment" .
|
||
($need_commenter ?
|
||
" INNER JOIN profiles AS actcheck_commenter" .
|
||
" ON actcheck_commenter.userid=actcheck_comment.who AND $need_commenter" : '') .
|
||
" WHERE $extra";
|
||
}
|
||
|
||
if ($bug_creation_clause)
|
||
{
|
||
push @list, "SELECT bug_id FROM bugs WHERE $bug_creation_clause";
|
||
}
|
||
|
||
if (@list == 1)
|
||
{
|
||
$list[0] =~ s/^SELECT/SELECT DISTINCT/;
|
||
}
|
||
|
||
push @supptables, "INNER JOIN (" . join(' UNION ', @list) . ") actcheck_union ON bugs.bug_id=actcheck_union.bug_id";
|
||
}
|
||
|
||
my $sql_deadlinefrom;
|
||
my $sql_deadlineto;
|
||
if ($user->is_timetracker) {
|
||
if ($params->param('deadlinefrom')) {
|
||
my $deadlinefrom = $params->param('deadlinefrom');
|
||
$sql_deadlinefrom = $dbh->quote(SqlifyDate($deadlinefrom));
|
||
trick_taint($sql_deadlinefrom);
|
||
my $term = "bugs.deadline >= $sql_deadlinefrom";
|
||
push(@wherepart, $term);
|
||
$self->search_description({
|
||
field => 'deadline', type => 'greaterthaneq',
|
||
value => $deadlinefrom, term => $term,
|
||
});
|
||
}
|
||
|
||
if ($params->param('deadlineto')) {
|
||
my $deadlineto = $params->param('deadlineto');
|
||
$sql_deadlineto = $dbh->quote(SqlifyDate($deadlineto));
|
||
trick_taint($sql_deadlineto);
|
||
my $term = "bugs.deadline <= $sql_deadlineto";
|
||
push(@wherepart, $term);
|
||
$self->search_description({
|
||
field => 'deadline', type => 'lessthaneq',
|
||
value => $deadlineto, term => $term,
|
||
});
|
||
}
|
||
}
|
||
|
||
my @textfields = ("short_desc", "longdesc", "bug_file_loc", "status_whiteboard");
|
||
# CustIS Bug 58300 - Add custom fields to search filters
|
||
push @textfields,
|
||
map { $_->name }
|
||
grep { $_->type == FIELD_TYPE_FREETEXT || $_->type == FIELD_TYPE_TEXTAREA }
|
||
Bugzilla->active_custom_fields;
|
||
foreach my $f (@textfields) {
|
||
if (defined $params->param($f)) {
|
||
my $s = trim($params->param($f));
|
||
if ($s ne "") {
|
||
my $n = $f;
|
||
my $q = $dbh->quote($s);
|
||
trick_taint($q);
|
||
my $type = $params->param($f . "_type");
|
||
push(@specialchart, [$f, $type, $s]);
|
||
}
|
||
}
|
||
}
|
||
|
||
my $multi_fields = join('|', map($_->name, @multi_select_fields));
|
||
|
||
my $chartid;
|
||
my $sequence = 0;
|
||
my $f;
|
||
my $ff;
|
||
my $t;
|
||
my $q;
|
||
my $v;
|
||
my $term;
|
||
my %funcsbykey;
|
||
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,
|
||
'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_after,
|
||
"^long_?desc,changedafter" => \&_long_desc_changedbefore_after,
|
||
"^content,(?:not)?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,
|
||
# The _ is allowed for backwards-compatibility with 3.2 and lower.
|
||
"^long_?desc," => \&_long_desc,
|
||
"^longdescs\.isprivate," => \&_longdescs_isprivate,
|
||
"^work_time,changedby" => \&_work_time_changedby,
|
||
"^work_time,changedbefore" => \&_work_time_changedbefore_after,
|
||
"^work_time,changedafter" => \&_work_time_changedbefore_after,
|
||
"^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,(?:equals|anyexact|anyword|allwords)" => \&_keywords_exact,
|
||
"^keywords,(?:notequals|notregexp|notsubstring|nowords|nowordssubstr)" => \&_multiselect_negative,
|
||
"^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,
|
||
"^($multi_fields),(?:notequals|notregexp|notsubstring|nowords|nowordssubstr)" => \&_multiselect_negative,
|
||
"^($multi_fields),(?:allwords|allwordssubstr|anyexact)" => \&_multiselect_multiple,
|
||
"^($multi_fields),(?!changed)" => \&_multiselect_nonchanged,
|
||
",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"); },
|
||
",notmatches" => 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,
|
||
",insearch" => \&_in_search_results,
|
||
",notinsearch" => \&_not_in_search_results,
|
||
);
|
||
my @funcnames;
|
||
while (@funcdefs) {
|
||
my $key = shift(@funcdefs);
|
||
my $value = shift(@funcdefs);
|
||
if ($key =~ /^[^,]*$/) {
|
||
die "All defs in %funcs must have a comma in their name: $key";
|
||
}
|
||
if (exists $funcsbykey{$key}) {
|
||
die "Duplicate key in %funcs: $key";
|
||
}
|
||
$funcsbykey{$key} = $value;
|
||
push(@funcnames, $key);
|
||
}
|
||
|
||
# first we delete any sign of "Chart #-1" from the HTML form hash
|
||
# since we want to guarantee the user didn't hide something here
|
||
my @badcharts = grep /^(field|type|value)-1-/, $params->param();
|
||
foreach my $field (@badcharts) {
|
||
$params->delete($field);
|
||
}
|
||
|
||
# now we take our special chart and stuff it into the form hash
|
||
my $chart = -1;
|
||
my $row = 0;
|
||
foreach my $ref (@specialchart) {
|
||
my $col = 0;
|
||
while (@$ref) {
|
||
$params->param("field$chart-$row-$col", shift(@$ref));
|
||
$params->param("type$chart-$row-$col", shift(@$ref));
|
||
$params->param("value$chart-$row-$col", shift(@$ref));
|
||
$col++;
|
||
|
||
}
|
||
$row++;
|
||
}
|
||
|
||
my $specialchart_terms;
|
||
|
||
# A boolean chart is a way of representing the terms in a logical
|
||
# expression. Bugzilla builds SQL queries depending on how you enter
|
||
# terms into the boolean chart. Boolean charts are represented in
|
||
# urls as tree-tuples of (chart id, row, column). The query form
|
||
# (query.cgi) may contain an arbitrary number of boolean charts where
|
||
# each chart represents a clause in a SQL query.
|
||
#
|
||
# The query form starts out with one boolean chart containing one
|
||
# row and one column. Extra rows can be created by pressing the
|
||
# AND button at the bottom of the chart. Extra columns are created
|
||
# by pressing the OR button at the right end of the chart. Extra
|
||
# charts are created by pressing "Add another boolean chart".
|
||
#
|
||
# Each chart consists of an arbitrary number of rows and columns.
|
||
# The terms within a row are ORed together. The expressions represented
|
||
# by each row are ANDed together. The expressions represented by each
|
||
# chart are ANDed together.
|
||
#
|
||
# ----------------------
|
||
# | col2 | col2 | col3 |
|
||
# --------------|------|------|
|
||
# | row1 | a1 | a2 | |
|
||
# |------|------|------|------| => ((a1 OR a2) AND (b1 OR b2 OR b3) AND (c1))
|
||
# | row2 | b1 | b2 | b3 |
|
||
# |------|------|------|------|
|
||
# | row3 | c1 | | |
|
||
# -----------------------------
|
||
#
|
||
# --------
|
||
# | col2 |
|
||
# --------------|
|
||
# | row1 | d1 | => (d1)
|
||
# ---------------
|
||
#
|
||
# Together, these two charts represent a SQL expression like this
|
||
# SELECT blah FROM blah WHERE ( (a1 OR a2)AND(b1 OR b2 OR b3)AND(c1)) AND (d1)
|
||
#
|
||
# The terms within a single row of a boolean chart are all constraints
|
||
# on a single piece of data. If you're looking for a bug that has two
|
||
# different people cc'd on it, then you need to use two boolean charts.
|
||
# This will find bugs with one CC matching 'foo@blah.org' and and another
|
||
# CC matching 'bar@blah.org'.
|
||
#
|
||
# --------------------------------------------------------------
|
||
# CC | equal to
|
||
# foo@blah.org
|
||
# --------------------------------------------------------------
|
||
# CC | equal to
|
||
# bar@blah.org
|
||
#
|
||
# If you try to do this query by pressing the AND button in the
|
||
# original boolean chart then what you'll get is an expression that
|
||
# looks for a single CC where the login name is both "foo@blah.org",
|
||
# and "bar@blah.org". This is impossible.
|
||
#
|
||
# --------------------------------------------------------------
|
||
# CC | equal to
|
||
# foo@blah.org
|
||
# AND
|
||
# CC | equal to
|
||
# bar@blah.org
|
||
# --------------------------------------------------------------
|
||
|
||
# $chartid is the number of the current chart whose SQL we're constructing
|
||
# $row is the current row of the current chart
|
||
|
||
# names for table aliases are constructed using $chartid and $row
|
||
# SELECT blah FROM $table "$table_$chartid_$row" WHERE ....
|
||
|
||
# $f = field of table in bug db (e.g. bug_id, reporter, etc)
|
||
# $ff = qualified field name (field name prefixed by table)
|
||
# e.g. bugs_activity.bug_id
|
||
# $t = type of query. e.g. "equal to", "changed after", case sensitive substr"
|
||
# $v = value - value the user typed in to the form
|
||
# $q = sanitized version of user input trick_taint(($dbh->quote($v)))
|
||
# @supptables = Tables and/or table aliases used in query
|
||
# %suppseen = A hash used to store all the tables in supptables to weed
|
||
# out duplicates.
|
||
# @supplist = A list used to accumulate all the JOIN clauses for each
|
||
# chart to merge the ON sections of each.
|
||
# $suppstring = String which is pasted into query containing all table names
|
||
|
||
$row = 0;
|
||
for ($chart=-1 ;
|
||
$chart < 0 || $params->param("field$chart-0-0") ;
|
||
$chart++) {
|
||
$chartid = $chart >= 0 ? $chart : "";
|
||
my @chartandlist = ();
|
||
for ($row = 0 ;
|
||
$params->param("field$chart-$row-0") ;
|
||
$row++) {
|
||
my @orlist;
|
||
for (my $col = 0 ;
|
||
$params->param("field$chart-$row-$col") ;
|
||
$col++) {
|
||
$f = $params->param("field$chart-$row-$col") || "noop";
|
||
my $original_f = $f; # Saved for search_description
|
||
$t = $params->param("type$chart-$row-$col") || "noop";
|
||
$v = $params->param("value$chart-$row-$col");
|
||
$v = "" if !defined $v;
|
||
$v = trim($v);
|
||
if ($f eq "noop" || $t eq "noop" || $v eq "" &&
|
||
$t ne "equals" && $t ne "notequals" && $t ne "exact") {
|
||
next;
|
||
}
|
||
# chart -1 is generated by other code above, not from the user-
|
||
# submitted form, so we'll blindly accept any values in chart -1
|
||
if (!CHART_FIELDS_HASH->{$f} && $chart != -1) {
|
||
ThrowCodeError("invalid_field_name", {field => $f});
|
||
}
|
||
# CustIS Bug 53836
|
||
if ($t eq "equals" || $t eq "exact" || $t eq "anyexact") {
|
||
$v =~ s/\%user\%/$user->login/isge;
|
||
}
|
||
|
||
# This is either from the internal chart (in which case we
|
||
# already know about it), or it was in %chartfields, so it is
|
||
# a valid field name, which means that it's ok.
|
||
trick_taint($f);
|
||
$q = $dbh->quote($v);
|
||
trick_taint($q);
|
||
my $rhs = $v;
|
||
$rhs =~ tr/,//;
|
||
my $func;
|
||
$term = undef;
|
||
foreach my $key (@funcnames) {
|
||
if ("$f,$t,$rhs" =~ m/$key/) {
|
||
my $ref = $funcsbykey{$key};
|
||
$ff = $f;
|
||
if (COLUMNS->{$f}->{name}) {
|
||
$ff = COLUMNS->{$f}->{name};
|
||
} elsif ($f !~ /\./) {
|
||
$ff = "bugs.$f";
|
||
}
|
||
$self->$ref(%func_args);
|
||
if ($term) {
|
||
last;
|
||
}
|
||
}
|
||
}
|
||
if ($term) {
|
||
$self->search_description({
|
||
field => $original_f, type => $t, value => $v,
|
||
term => $term,
|
||
});
|
||
push(@orlist, $term);
|
||
}
|
||
else {
|
||
# This field and this type don't work together.
|
||
ThrowCodeError("field_type_mismatch",
|
||
{ field => $params->param("field$chart-$row-$col"),
|
||
type => $params->param("type$chart-$row-$col"),
|
||
});
|
||
}
|
||
}
|
||
if (@orlist) {
|
||
@orlist = map("($_)", @orlist) if (scalar(@orlist) > 1);
|
||
push(@chartandlist, "(" . join(" OR ", @orlist) . ")");
|
||
}
|
||
}
|
||
if (@chartandlist) {
|
||
if ($params->param("negate$chart") ? 1 : 0) {
|
||
push(@andlist, "NOT(" . join(" AND ", @chartandlist) . ")");
|
||
} else {
|
||
push(@andlist, "(" . join(" AND ", @chartandlist) . ")");
|
||
}
|
||
if ($chart < 0) {
|
||
$specialchart_terms = pop @andlist;
|
||
}
|
||
}
|
||
}
|
||
|
||
# The ORDER BY clause goes last, but can require modifications
|
||
# to other parts of the query, so we want to create it before we
|
||
# write the FROM clause.
|
||
foreach my $orderitem (@inputorder) {
|
||
BuildOrderBy(\%special_order, $orderitem, \@orderby);
|
||
}
|
||
# Now JOIN the correct tables in the FROM clause.
|
||
# This is done separately from the above because it's
|
||
# cleaner to do it this way.
|
||
foreach my $orderitem (@inputorder) {
|
||
# Grab the part without ASC or DESC.
|
||
my $column_name = split_order_term($orderitem);
|
||
if ($special_order_join{$column_name}) {
|
||
push(@supptables, $special_order_join{$column_name});
|
||
}
|
||
}
|
||
|
||
my %suppseen = ("bugs" => 1);
|
||
my $suppstring = "bugs";
|
||
my @supplist = (" ");
|
||
foreach my $str (@supptables) {
|
||
|
||
if ($str =~ /^(LEFT|INNER|RIGHT)\s+JOIN/iso) {
|
||
$str =~ /^(.*?)\s+ON\s+(.*)$/iso;
|
||
my ($leftside, $rightside) = ($1, $2);
|
||
if (defined $suppseen{$leftside}) {
|
||
$supplist[$suppseen{$leftside}] .= " AND ($rightside)";
|
||
} else {
|
||
$suppseen{$leftside} = scalar @supplist;
|
||
push @supplist, " $leftside ON ($rightside)";
|
||
}
|
||
} else {
|
||
# Do not accept implicit joins using comma operator
|
||
# as they are not DB agnostic
|
||
ThrowCodeError("comma_operator_deprecated");
|
||
}
|
||
}
|
||
$suppstring .= join('', @supplist);
|
||
|
||
# <vfilippov@custis.ru> AND(AND(OR)) is IMO pointless. Do OR(AND(OR)).
|
||
@andlist = ("(" . join(" OR ", @andlist) . ")") if @andlist;
|
||
unshift @andlist, $specialchart_terms if $specialchart_terms;
|
||
|
||
# Make sure we create a legal SQL query.
|
||
@andlist = ("1 = 1") if !@andlist;
|
||
|
||
my @sql_fields;
|
||
foreach my $field (@fields)
|
||
{
|
||
if (COLUMNS->{$field}->{name})
|
||
{
|
||
my $alias = $field;
|
||
# Aliases cannot contain dots in them. We convert them to underscores.
|
||
$alias =~ s/\./_/g;
|
||
push @sql_fields, $field eq EMPTY_COLUMN
|
||
? EMPTY_COLUMN : COLUMNS->{$field}->{name} . " AS $alias";
|
||
}
|
||
else
|
||
{
|
||
push @sql_fields, $field;
|
||
}
|
||
}
|
||
my $query = "SELECT " . join(', ', @sql_fields) . " FROM $suppstring";
|
||
|
||
if (!$user->is_super_user)
|
||
{
|
||
$query .= " LEFT JOIN bug_group_map ON bug_group_map.bug_id = bugs.bug_id";
|
||
if ($user->id)
|
||
{
|
||
if (scalar @{ $user->groups })
|
||
{
|
||
$query .=
|
||
" AND bug_group_map.group_id NOT IN ("
|
||
. $user->groups_as_string . ") ";
|
||
}
|
||
$query .= " LEFT JOIN cc ON cc.bug_id = bugs.bug_id AND cc.who = " . $user->id;
|
||
}
|
||
}
|
||
|
||
$query .= " WHERE (" . join(' AND ', (@wherepart, @andlist)) .
|
||
" AND bugs.creation_ts IS NOT NULL ";
|
||
|
||
if (!$user->is_super_user)
|
||
{
|
||
$query .= " AND ((bug_group_map.group_id IS NULL)";
|
||
if ($user->id)
|
||
{
|
||
my $userid = $user->id;
|
||
$query .=
|
||
" OR (bugs.reporter_accessible = 1 AND bugs.reporter = $userid) " .
|
||
" OR (bugs.cclist_accessible = 1 AND cc.who IS NOT NULL) " .
|
||
" OR (bugs.assigned_to = $userid) ";
|
||
if (Bugzilla->params->{'useqacontact'})
|
||
{
|
||
$query .= "OR (bugs.qa_contact = $userid) ";
|
||
}
|
||
}
|
||
$query .= ") ";
|
||
}
|
||
$query .= ") ";
|
||
|
||
# For some DBs, every field in the SELECT must be in the GROUP BY.
|
||
foreach my $field (@fields) {
|
||
# These fields never go into the GROUP BY (bug_id goes in
|
||
# explicitly, below).
|
||
next if (grep($_ eq $field, EMPTY_COLUMN,
|
||
qw(bug_id actual_time percentage_complete flagtypes.name)));
|
||
my $col = COLUMNS->{$field}->{name};
|
||
push(@groupby, $col) if !grep($_ eq $col, @groupby);
|
||
}
|
||
# And all items from ORDER BY must be in the GROUP BY. The above loop
|
||
# doesn't catch items that were put into the ORDER BY from SPECIAL_ORDER.
|
||
foreach my $item (@inputorder) {
|
||
my $column_name = split_order_term($item);
|
||
if ($special_order{$column_name}) {
|
||
push(@groupby, @{ $special_order{$column_name} });
|
||
}
|
||
}
|
||
$query .= $dbh->sql_group_by("bugs.bug_id", join(', ', @groupby));
|
||
|
||
|
||
if (@having) {
|
||
$query .= " HAVING " . join(" AND ", @having);
|
||
}
|
||
|
||
if (@orderby) {
|
||
$query .= " ORDER BY " . join(',', @orderby);
|
||
}
|
||
|
||
$self->{'sql'} = $query;
|
||
}
|
||
|
||
###############################################################################
|
||
# Helper functions for the init() method.
|
||
###############################################################################
|
||
sub SqlifyDate {
|
||
my ($str) = @_;
|
||
$str = "" if (!defined $str || lc($str) eq 'now');
|
||
if ($str eq "") {
|
||
my ($sec, $min, $hour, $mday, $month, $year, $wday) = localtime(time());
|
||
return sprintf("%4d-%02d-%02d 00:00:00", $year+1900, $month+1, $mday);
|
||
}
|
||
|
||
|
||
if ($str =~ /^(-|\+)?(\d+)([hHdDwWmMyY])$/) { # relative date
|
||
my ($sign, $amount, $unit, $date) = ($1, $2, lc $3, time);
|
||
my ($sec, $min, $hour, $mday, $month, $year, $wday) = localtime($date);
|
||
if ($sign && $sign eq '+') { $amount = -$amount; }
|
||
if ($unit eq 'w') { # convert weeks to days
|
||
$amount = 7*$amount + $wday;
|
||
$unit = 'd';
|
||
}
|
||
if ($unit eq 'd') {
|
||
$date -= $sec + 60*$min + 3600*$hour + 24*3600*$amount;
|
||
return time2str("%Y-%m-%d %H:%M:%S", $date);
|
||
}
|
||
elsif ($unit eq 'y') {
|
||
return sprintf("%4d-01-01 00:00:00", $year+1900-$amount);
|
||
}
|
||
elsif ($unit eq 'm') {
|
||
$month -= $amount;
|
||
while ($month<0) { $year--; $month += 12; }
|
||
return sprintf("%4d-%02d-01 00:00:00", $year+1900, $month+1);
|
||
}
|
||
elsif ($unit eq 'h') {
|
||
# Special case 0h for 'beginning of this hour'
|
||
if ($amount == 0) {
|
||
$date -= $sec + 60*$min;
|
||
} else {
|
||
$date -= 3600*$amount;
|
||
}
|
||
return time2str("%Y-%m-%d %H:%M:%S", $date);
|
||
}
|
||
return undef; # should not happen due to regexp at top
|
||
}
|
||
my $date = str2time($str);
|
||
if (!defined($date)) {
|
||
ThrowUserError("illegal_date", { date => $str });
|
||
}
|
||
return time2str("%Y-%m-%d %H:%M:%S", $date);
|
||
}
|
||
|
||
sub build_subselect {
|
||
my ($outer, $inner, $table, $cond) = @_;
|
||
my $q = "SELECT $inner FROM $table WHERE $cond";
|
||
return "$outer IN ($q)";
|
||
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);
|
||
}
|
||
|
||
sub GetByWordList {
|
||
my ($field, $strs) = (@_);
|
||
my @list;
|
||
my $dbh = Bugzilla->dbh;
|
||
return [] unless defined $strs;
|
||
|
||
foreach my $w (split(/[\s,]+/, $strs)) {
|
||
my $word = $w;
|
||
if ($word ne "") {
|
||
$word =~ tr/A-Z/a-z/;
|
||
$word = $dbh->quote('(^|[^a-z0-9])' . quotemeta($word) . '($|[^a-z0-9])');
|
||
trick_taint($word);
|
||
push(@list, $dbh->sql_regexp($field, $word));
|
||
}
|
||
}
|
||
|
||
return \@list;
|
||
}
|
||
|
||
# Support for "any/all/nowordssubstr" comparison type ("words as substrings")
|
||
sub GetByWordListSubstr {
|
||
my ($field, $strs) = (@_);
|
||
my @list;
|
||
my $dbh = Bugzilla->dbh;
|
||
my $sql_word;
|
||
|
||
foreach my $word (split(/[\s,]+/, $strs)) {
|
||
if ($word ne "") {
|
||
$sql_word = $dbh->quote($word);
|
||
trick_taint($sql_word);
|
||
push(@list, $dbh->sql_iposition($sql_word, $field) . " > 0");
|
||
}
|
||
}
|
||
|
||
return \@list;
|
||
}
|
||
|
||
sub getSQL {
|
||
my $self = shift;
|
||
return $self->{'sql'};
|
||
}
|
||
|
||
sub search_description {
|
||
my ($self, $params) = @_;
|
||
my $desc = $self->{'search_description'} ||= [];
|
||
if ($params) {
|
||
push(@$desc, $params);
|
||
}
|
||
return $self->{'search_description'};
|
||
}
|
||
|
||
sub pronoun {
|
||
my ($noun, $user) = (@_);
|
||
if ($noun eq "%user%") {
|
||
if ($user->id) {
|
||
return $user->id;
|
||
} else {
|
||
ThrowUserError('login_required_for_pronoun');
|
||
}
|
||
}
|
||
if ($noun eq "%reporter%") {
|
||
return "bugs.reporter";
|
||
}
|
||
if ($noun eq "%assignee%") {
|
||
return "bugs.assigned_to";
|
||
}
|
||
if ($noun eq "%qacontact%") {
|
||
return "bugs.qa_contact";
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
# BuildOrderBy - Private Subroutine
|
||
# This function converts the input order to an "output" order,
|
||
# suitable for concatenation to form an ORDER BY clause. Basically,
|
||
# it just handles fields that have non-standard sort orders from
|
||
# %specialorder.
|
||
# Arguments:
|
||
# $orderitem - A string. The next value to append to the ORDER BY clause,
|
||
# in the format of an item in the 'order' parameter to
|
||
# Bugzilla::Search.
|
||
# $stringlist - A reference to the list of strings that will be join()'ed
|
||
# to make ORDER BY. This is what the subroutine modifies.
|
||
# $reverseorder - (Optional) A boolean. TRUE if we should reverse the order
|
||
# of the field that we are given (from ASC to DESC or vice-versa).
|
||
#
|
||
# Explanation of $reverseorder
|
||
# ----------------------------
|
||
# The role of $reverseorder is to handle things like sorting by
|
||
# "target_milestone DESC".
|
||
# Let's say that we had a field "A" that normally translates to a sort
|
||
# order of "B ASC, C DESC". If we sort by "A DESC", what we really then
|
||
# mean is "B DESC, C ASC". So $reverseorder is only used if we call
|
||
# BuildOrderBy recursively, to let it know that we're "reversing" the
|
||
# order. That is, that we wanted "A DESC", not "A".
|
||
sub BuildOrderBy {
|
||
my ($special_order, $orderitem, $stringlist, $reverseorder) = (@_);
|
||
|
||
my ($orderfield, $orderdirection) = split_order_term($orderitem);
|
||
|
||
if ($reverseorder) {
|
||
# If orderdirection is empty or ASC...
|
||
if (!$orderdirection || $orderdirection =~ m/asc/i) {
|
||
$orderdirection = "DESC";
|
||
} else {
|
||
# This has the minor side-effect of making any reversed invalid
|
||
# direction into ASC.
|
||
$orderdirection = "ASC";
|
||
}
|
||
}
|
||
|
||
# Handle fields that have non-standard sort orders, from $specialorder.
|
||
if ($special_order->{$orderfield}) {
|
||
foreach my $subitem (@{$special_order->{$orderfield}}) {
|
||
# DESC on a field with non-standard sort order means
|
||
# "reverse the normal order for each field that we map to."
|
||
BuildOrderBy($special_order, $subitem, $stringlist,
|
||
$orderdirection =~ m/desc/i);
|
||
}
|
||
return;
|
||
}
|
||
# Aliases cannot contain dots in them. We convert them to underscores.
|
||
$orderfield =~ s/\./_/g if exists COLUMNS->{$orderfield};
|
||
|
||
push(@$stringlist, trim($orderfield . ' ' . $orderdirection));
|
||
}
|
||
|
||
# Splits out "asc|desc" from a sort order item.
|
||
sub split_order_term {
|
||
my $fragment = shift;
|
||
$fragment =~ /^(.+?)(?:\s+(ASC|DESC))?$/i;
|
||
my ($column_name, $direction) = (lc($1), uc($2 || ''));
|
||
return wantarray ? ($column_name, $direction) : $column_name;
|
||
}
|
||
|
||
# Used to translate old SQL fragments from buglist.cgi's "order" argument
|
||
# into our modern field IDs.
|
||
sub translate_old_column {
|
||
my ($column) = @_;
|
||
# All old SQL fragments have a period in them somewhere.
|
||
return $column if $column !~ /\./;
|
||
|
||
if ($column =~ /\bAS\s+(\w+)$/i) {
|
||
return $1;
|
||
}
|
||
# product, component, classification, assigned_to, qa_contact, reporter
|
||
elsif ($column =~ /map_(\w+?)s?\.(login_)?name/i) {
|
||
return $1;
|
||
}
|
||
|
||
# If it doesn't match the regexps above, check to see if the old
|
||
# SQL fragment matches the SQL of an existing column
|
||
foreach my $key (%{ COLUMNS() }) {
|
||
next unless exists COLUMNS->{$key}->{name};
|
||
return $key if COLUMNS->{$key}->{name} eq $column;
|
||
}
|
||
|
||
return $column;
|
||
}
|
||
|
||
#####################################################################
|
||
# 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 && $user->in_group_id($groupid))
|
||
|| ThrowUserError('invalid_group_name',{name => $group});
|
||
my @childgroups = @{Bugzilla::Group->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 && $user->in_group_id($groupid))
|
||
|| ThrowUserError('invalid_group_name',{name => $group});
|
||
my @childgroups = @{Bugzilla::Group->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_after {
|
||
my $self = shift;
|
||
my %func_args = @_;
|
||
my ($chartid, $t, $v, $supptables, $term) =
|
||
@func_args{qw(chartid t v supptables term)};
|
||
my $dbh = Bugzilla->dbh;
|
||
|
||
my $operator = ($$t =~ /before/) ? '<' : '>';
|
||
my $table = "longdescs_$$chartid";
|
||
push(@$supptables, "LEFT JOIN longdescs AS $table " .
|
||
"ON $table.bug_id = bugs.bug_id " .
|
||
"AND $table.bug_when $operator " .
|
||
$dbh->quote(SqlifyDate($$v)) );
|
||
$$term = "($table.bug_when IS NOT NULL)";
|
||
}
|
||
|
||
sub _content_matches
|
||
{
|
||
my $self = shift;
|
||
my %func_args = @_;
|
||
my ($chartid, $supptables, $term, $groupby, $fields, $t, $v) =
|
||
@func_args{qw(chartid supptables term groupby fields t 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 fulltext table to the query so we can search on it.
|
||
my $table = "bugs_fulltext_$$chartid";
|
||
my $comments_col = "comments";
|
||
$comments_col = "comments_noprivate" unless $self->{'user'}->is_insider;
|
||
|
||
# Create search terms to add to the SELECT and WHERE clauses.
|
||
my $text = stem_text($$v);
|
||
my ($term1, $rterm1) = $dbh->sql_fulltext_search("bugs_fulltext.$comments_col",
|
||
$text, 1);
|
||
my ($term2, $rterm2) = $dbh->sql_fulltext_search("bugs_fulltext.short_desc",
|
||
$text, 2);
|
||
$rterm1 = $term1 if !$rterm1;
|
||
$rterm2 = $term2 if !$rterm2;
|
||
|
||
# The term to use in the WHERE clause.
|
||
$$term = "$term1 > 0 OR $term2 > 0";
|
||
|
||
# In order to sort by relevance (in case the user requests it),
|
||
# we SELECT the relevance value so we can add it to the ORDER BY
|
||
# clause. Every time a new fulltext chart isadded, this adds more
|
||
# terms to the relevance sql. (That doesn't make sense in
|
||
# "NOT" charts, but Bugzilla never uses those with fulltext
|
||
# by default.)
|
||
#
|
||
|
||
# Bug 46221 - Russian Stemming in Bugzilla fulltext search
|
||
# Bugzilla's fulltext search mechanism is bad because
|
||
# MATCH(...) OR MATCH(...) is very slow in MySQL - it doesn't do
|
||
# fulltext index merge optimization. So we use INNER JOIN to UNION.
|
||
push @$supptables, "INNER JOIN (SELECT bug_id FROM bugs_fulltext WHERE $term1 > 0
|
||
UNION SELECT bug_id FROM bugs_fulltext WHERE $term2 > 0) AS $table ON bugs.bug_id=$table.bug_id";
|
||
|
||
# We build the relevance SQL by modifying the COLUMNS list directly,
|
||
# which is kind of a hack but works.
|
||
COLUMNS->{relevance}->{name} = "(SELECT $rterm1+$rterm2 FROM bugs_fulltext WHERE bugs_fulltext.bug_id=bugs.bug_id)";
|
||
|
||
# All work done by INNER JOIN
|
||
$$term = "1";
|
||
}
|
||
|
||
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 = $user->is_insider ? "" : "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 = $self->{'user'}->is_insider ? "" : "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 = $self->{'user'}->is_insider ? "" : "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 = $self->{'user'}->is_insider ? "" : "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_after {
|
||
my $self = shift;
|
||
my %func_args = @_;
|
||
my ($chartid, $t, $v, $supptables, $term) =
|
||
@func_args{qw(chartid t v supptables term)};
|
||
my $dbh = Bugzilla->dbh;
|
||
|
||
my $operator = ($$t =~ /before/) ? '<' : '>';
|
||
my $table = "longdescs_$$chartid";
|
||
push(@$supptables, "LEFT JOIN longdescs AS $table " .
|
||
"ON $table.bug_id = bugs.bug_id " .
|
||
"AND $table.work_time <> 0 " .
|
||
"AND $table.bug_when $operator " .
|
||
$dbh->quote(SqlifyDate($$v)) );
|
||
$$term = "($table.bug_when IS NOT NULL)";
|
||
}
|
||
|
||
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 (!grep($_ eq 'remaining_time', @$fields)) {
|
||
push(@$fields, "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 = $self->{'user'}->is_insider ? "" : "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 = $self->{'user'}->is_insider ? "" : "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 = $self->{'user'}->is_insider ? "" : "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_exact {
|
||
my $self = shift;
|
||
my %func_args = @_;
|
||
my ($chartid, $v, $ff, $f, $t, $term, $supptables) =
|
||
@func_args{qw(chartid v ff f t term supptables)};
|
||
|
||
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 _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 supptables)};
|
||
|
||
my $k_table = "keywords_$$chartid";
|
||
my $kd_table = "keyworddefs_$$chartid";
|
||
|
||
# CustIS Bug 65346 - keyword search is broken in 3.6
|
||
|
||
$$f = "(SELECT IFNULL(GROUP_CONCAT($kd_table.name SEPARATOR ' '), '') FROM keywords AS $k_table " .
|
||
" LEFT JOIN keyworddefs AS $kd_table ON $kd_table.id=$k_table.keywordid" .
|
||
" WHERE $k_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 _multiselect_negative {
|
||
my $self = shift;
|
||
my %func_args = @_;
|
||
my ($f, $ff, $t, $funcsbykey, $term) = @func_args{qw(f ff t funcsbykey term)};
|
||
|
||
my %map = (
|
||
notequals => 'equals',
|
||
notregexp => 'regexp',
|
||
notsubstring => 'substring',
|
||
nowords => 'anywords',
|
||
nowordssubstr => 'anywordssubstr',
|
||
);
|
||
|
||
my $table;
|
||
if ($$f eq 'keywords') {
|
||
$table = "keywords LEFT JOIN keyworddefs"
|
||
. " ON keywords.keywordid = keyworddefs.id";
|
||
$$ff = "keyworddefs.name";
|
||
}
|
||
else {
|
||
$table = "bug_$$f";
|
||
$$ff = "$table.value";
|
||
}
|
||
|
||
$$funcsbykey{",".$map{$$t}}($self, %func_args);
|
||
$$term = "bugs.bug_id NOT IN (SELECT bug_id FROM $table WHERE $$term)";
|
||
}
|
||
|
||
sub _multiselect_multiple {
|
||
my $self = shift;
|
||
my %func_args = @_;
|
||
my ($f, $ff, $t, $v, $funcsbykey, $term) = @func_args{qw(f ff t v funcsbykey term)};
|
||
|
||
my @terms;
|
||
my $table = "bug_$$f";
|
||
$$ff = "$table.value";
|
||
|
||
foreach my $word (split(/[\s,]+/, $$v)) {
|
||
$$v = $word;
|
||
$$funcsbykey{",".$$t}($self, %func_args);
|
||
push(@terms, "bugs.bug_id IN
|
||
(SELECT bug_id FROM $table WHERE $$term)");
|
||
}
|
||
|
||
if ($$t eq 'anyexact') {
|
||
$$term = "(" . join(" OR ", @terms) . ")";
|
||
}
|
||
else {
|
||
$$term = "(" . join(" AND ", @terms) . ")";
|
||
}
|
||
}
|
||
|
||
sub _multiselect_nonchanged {
|
||
my $self = shift;
|
||
my %func_args = @_;
|
||
my ($chartid, $f, $ff, $t, $funcsbykey, $supptables) =
|
||
@func_args{qw(chartid f ff t funcsbykey supptables)};
|
||
|
||
my $table = $$f."_".$$chartid;
|
||
$$ff = "$table.value";
|
||
|
||
$$funcsbykey{",$$t"}($self, %func_args);
|
||
push(@$supptables, "LEFT JOIN bug_$$f AS $table " .
|
||
"ON $table.bug_id = bugs.bug_id ");
|
||
}
|
||
|
||
sub _equals
|
||
{
|
||
my $self = shift;
|
||
my %func_args = @_;
|
||
my ($ff, $q, $v, $term) = @func_args{qw(ff q v term)};
|
||
|
||
if ($v eq '')
|
||
{
|
||
$$term = "IFNULL($$ff,'') = $$q";
|
||
}
|
||
else
|
||
{
|
||
$$term = "$$ff = $$q";
|
||
}
|
||
}
|
||
|
||
sub _notequals
|
||
{
|
||
my $self = shift;
|
||
my %func_args = @_;
|
||
my ($ff, $q, $v, $term) = @func_args{qw(ff q v term)};
|
||
|
||
if ($v eq '')
|
||
{
|
||
$$term = "IFNULL($$ff,'') != $$q";
|
||
}
|
||
else
|
||
{
|
||
$$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_iposition($$q, $$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_iposition($$q, $$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;
|
||
|
||
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, $supptables, $term) =
|
||
@func_args{qw(chartid f ff t v supptables term)};
|
||
my $dbh = Bugzilla->dbh;
|
||
|
||
my $operator = ($$t =~ /before/) ? '<' : '>';
|
||
my $table = "act_$$chartid";
|
||
my $fieldid = CHART_FIELDS->{$$f}->{id};
|
||
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, $f, $t, $v, $q, $supptables, $term) =
|
||
@func_args{qw(chartid f t v q supptables term)};
|
||
|
||
my $operator = ($$t =~ /from/) ? 'removed' : 'added';
|
||
my $table = "act_$$chartid";
|
||
my $fieldid = CHART_FIELDS->{$$f}->{id};
|
||
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, $f, $v, $supptables, $term) =
|
||
@func_args{qw(chartid f v supptables term)};
|
||
|
||
my $table = "act_$$chartid";
|
||
my $fieldid = CHART_FIELDS->{$$f}->{id};
|
||
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)";
|
||
}
|
||
|
||
sub _in_search_results
|
||
{
|
||
my $self = shift;
|
||
my %func_args = @_;
|
||
my ($not_in, $f, $v, $term) =
|
||
@func_args{qw(__not_in f v term)};
|
||
my $query = LookupNamedQuery(trim($$v));
|
||
my $queryparams = new Bugzilla::CGI($query);
|
||
my $search = new Bugzilla::Search(
|
||
params => $queryparams,
|
||
fields => ["bugs.bug_id"],
|
||
user => Bugzilla->user,
|
||
);
|
||
my $sqlquery = $search->getSQL();
|
||
unless ($not_in)
|
||
{
|
||
$$term = "($$f IN ($sqlquery))";
|
||
}
|
||
else
|
||
{
|
||
$$term = "($$f NOT IN ($sqlquery))";
|
||
}
|
||
}
|
||
|
||
sub _not_in_search_results
|
||
{
|
||
return _in_search_results(@_, __not_in => 1);
|
||
}
|
||
|
||
sub LookupNamedQuery
|
||
{
|
||
my ($name, $sharer_id, $query_type, $throw_error) = @_;
|
||
$throw_error = THROW_ERROR unless defined $throw_error;
|
||
|
||
Bugzilla->login(LOGIN_REQUIRED);
|
||
|
||
my $constructor = $throw_error ? 'check' : 'new';
|
||
my $query = Bugzilla::Search::Saved->$constructor(
|
||
{ user => $sharer_id, name => $name });
|
||
|
||
if (!$query ||
|
||
defined $query_type && $query->type != $query_type)
|
||
{
|
||
if ($throw_error)
|
||
{
|
||
ThrowUserError("missing_query", { queryname => $name,
|
||
sharer_id => $sharer_id });
|
||
}
|
||
else
|
||
{
|
||
return undef;
|
||
}
|
||
}
|
||
|
||
$query->url
|
||
|| ThrowUserError("buglist_parameters_required", { queryname => $name });
|
||
|
||
return wantarray ? ($query->url, $query->id) : $query->url;
|
||
}
|
||
|
||
1;
|