487 lines
13 KiB
Perl
487 lines
13 KiB
Perl
# 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 Everything Solved.
|
|
# Portions created by Everything Solved are Copyright (C) 2006
|
|
# Everything Solved. All Rights Reserved.
|
|
#
|
|
# Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org>
|
|
|
|
use strict;
|
|
|
|
package Bugzilla::Search::Saved;
|
|
|
|
use base qw(Bugzilla::Object);
|
|
|
|
use Bugzilla::CGI;
|
|
use Bugzilla::Hook;
|
|
use Bugzilla::Constants;
|
|
use Bugzilla::Group;
|
|
use Bugzilla::Error;
|
|
use Bugzilla::User;
|
|
use Bugzilla::Util;
|
|
use Bugzilla::Search;
|
|
use Bugzilla::CheckerUtils;
|
|
use Bugzilla::Views;
|
|
|
|
use Scalar::Util qw(blessed);
|
|
|
|
#############
|
|
# Constants #
|
|
#############
|
|
|
|
use constant DB_TABLE => 'namedqueries';
|
|
|
|
use constant DB_COLUMNS => qw(
|
|
id
|
|
userid
|
|
name
|
|
query
|
|
);
|
|
|
|
use constant REQUIRED_CREATE_FIELDS => qw(name query);
|
|
|
|
use constant VALIDATORS => {
|
|
name => \&_check_name,
|
|
query => \&_check_query,
|
|
link_in_footer => \&_check_link_in_footer,
|
|
};
|
|
|
|
use constant UPDATE_COLUMNS => qw(name query);
|
|
|
|
###############
|
|
# Constructor #
|
|
###############
|
|
|
|
sub new
|
|
{
|
|
my $class = shift;
|
|
my $param = shift;
|
|
my $dbh = Bugzilla->dbh;
|
|
|
|
my $user;
|
|
if (ref $param && !$param->{id})
|
|
{
|
|
$user = $param->{user} || Bugzilla->user;
|
|
my $name = $param->{name};
|
|
if (!defined $name)
|
|
{
|
|
ThrowCodeError('bad_arg', { argument => 'name', function => "${class}::new" });
|
|
}
|
|
my $condition = 'userid = ? AND '.$dbh->sql_istrcmp('name', '?');
|
|
my $user_id = blessed $user ? $user->id : $user;
|
|
detaint_natural($user_id) || ThrowCodeError('param_must_be_numeric',
|
|
{ function => $class . '::_init', param => 'user' });
|
|
my @values = ($user_id, $name);
|
|
$param = { condition => $condition, values => \@values };
|
|
}
|
|
|
|
unshift @_, $param;
|
|
my $self = $class->SUPER::new(@_);
|
|
if ($self)
|
|
{
|
|
$self->{user} = $user if blessed $user;
|
|
# Some DBs (read: Oracle) incorrectly mark the query string as UTF-8
|
|
# when it's coming out of the database, even though it has no UTF-8
|
|
# characters in it, which prevents Bugzilla::CGI from later reading
|
|
# it correctly.
|
|
utf8::downgrade($self->{query}) if utf8::is_utf8($self->{query});
|
|
}
|
|
return $self;
|
|
}
|
|
|
|
sub check
|
|
{
|
|
my $class = shift;
|
|
my $search = $class->SUPER::check(@_);
|
|
my $user = Bugzilla->user;
|
|
return $search if $search->user->id == $user->id;
|
|
if (!Bugzilla->user->in_group('admin') &&
|
|
(!$search->shared_with_group || !$user->in_group($search->shared_with_group)))
|
|
{
|
|
ThrowUserError('missing_query', {
|
|
queryname => $search->name,
|
|
sharer_id => $search->user->id,
|
|
});
|
|
}
|
|
return $search;
|
|
}
|
|
|
|
##############
|
|
# Validators #
|
|
##############
|
|
|
|
sub _check_link_in_footer { return $_[1] ? 1 : 0; }
|
|
|
|
sub _check_name
|
|
{
|
|
my ($invocant, $name) = @_;
|
|
$name = trim($name);
|
|
$name || ThrowUserError("query_name_missing");
|
|
$name !~ /[<>&]/ || ThrowUserError("illegal_query_name");
|
|
if (length($name) > MAX_FIELD_VALUE_SIZE)
|
|
{
|
|
ThrowUserError("query_name_too_long");
|
|
}
|
|
return $name;
|
|
}
|
|
|
|
sub _check_query
|
|
{
|
|
my ($invocant, $query) = @_;
|
|
$query || ThrowUserError("buglist_parameters_required");
|
|
my $params = http_decode_query($query);
|
|
# Don't store the query name as a parameter.
|
|
delete $params->{known_name};
|
|
delete $params->{sharer_id};
|
|
return http_build_query(Bugzilla::Search->clean_search_params($params));
|
|
}
|
|
|
|
#########################
|
|
# Database Manipulation #
|
|
#########################
|
|
|
|
sub create
|
|
{
|
|
my $class = shift;
|
|
Bugzilla->login(LOGIN_REQUIRED);
|
|
my $dbh = Bugzilla->dbh;
|
|
$class->check_required_create_fields(@_);
|
|
$dbh->bz_start_transaction();
|
|
my $params = $class->run_create_validators(@_);
|
|
|
|
# Right now you can only create a Saved Search for the current user.
|
|
$params->{userid} = Bugzilla->user->id;
|
|
|
|
my $lif = delete $params->{link_in_footer};
|
|
my $obj = $class->insert_create_data($params);
|
|
if ($lif)
|
|
{
|
|
$dbh->do(
|
|
'INSERT INTO namedqueries_link_in_footer (user_id, namedquery_id) VALUES (?,?)',
|
|
undef, $params->{userid}, $obj->id
|
|
);
|
|
}
|
|
$dbh->bz_commit_transaction();
|
|
|
|
return $obj;
|
|
}
|
|
|
|
sub preload
|
|
{
|
|
my ($searches) = @_;
|
|
my $dbh = Bugzilla->dbh;
|
|
|
|
return unless scalar @$searches;
|
|
|
|
my @query_ids = map { $_->id } @$searches;
|
|
my $queries_in_footer = $dbh->selectcol_arrayref(
|
|
'SELECT namedquery_id FROM namedqueries_link_in_footer' .
|
|
' WHERE ' . $dbh->sql_in('namedquery_id', \@query_ids) . ' AND user_id = ?',
|
|
undef, Bugzilla->user->id
|
|
);
|
|
|
|
my %links_in_footer = map { $_ => 1 } @$queries_in_footer;
|
|
foreach my $query (@$searches)
|
|
{
|
|
$query->{link_in_footer} = ($links_in_footer{$query->id}) ? 1 : 0;
|
|
}
|
|
}
|
|
|
|
sub update
|
|
{
|
|
my $self = shift;
|
|
my @r;
|
|
if (wantarray)
|
|
{
|
|
@r = $self->SUPER::update(@_);
|
|
}
|
|
else
|
|
{
|
|
@r = scalar $self->SUPER::update(@_);
|
|
}
|
|
Bugzilla::Hook::process('savedsearch-post-update', { search => $self });
|
|
Bugzilla::CheckerUtils::savedsearch_post_update({ search => $self });
|
|
Bugzilla::Views::refresh_some_views([ $self->user->login ]);
|
|
return @r;
|
|
}
|
|
|
|
sub remove_from_db
|
|
{
|
|
my $self = shift;
|
|
|
|
# Do not forget the saved search if it is being used in a whine or a checker
|
|
if (my $whines_in_use = $self->used_in_whine)
|
|
{
|
|
ThrowUserError('saved_search_used_by_whines', {
|
|
subjects => join(', ', @$whines_in_use),
|
|
search_name => $self->name,
|
|
});
|
|
}
|
|
if (my $checkers = $self->used_in_checkers)
|
|
{
|
|
ThrowUserError('saved_search_used_by_checkers', {
|
|
search_name => $self->name,
|
|
});
|
|
}
|
|
|
|
my $dbh = Bugzilla->dbh;
|
|
$dbh->do('DELETE FROM namedqueries WHERE id = ?', undef, $self->id);
|
|
|
|
if (Bugzilla->user->id == $self->userid)
|
|
{
|
|
# Reset the cached queries
|
|
$self->user->flush_queries_cache;
|
|
}
|
|
}
|
|
|
|
#####################
|
|
# Complex Accessors #
|
|
#####################
|
|
|
|
sub used_in_whine
|
|
{
|
|
my ($self) = @_;
|
|
return $self->{used_in_whine} if exists $self->{used_in_whine};
|
|
my $r = Bugzilla->dbh->selectcol_arrayref(
|
|
'SELECT DISTINCT whine_events.subject FROM whine_events'.
|
|
' INNER JOIN whine_queries ON whine_events.id = whine_queries.eventid'.
|
|
' WHERE whine_events.owner_userid = ? AND query_name = ?',
|
|
undef, $self->{userid}, $self->name
|
|
);
|
|
$self->{used_in_whine} = $r && @$r ? $r : undef;
|
|
return $self->{used_in_whine};
|
|
}
|
|
|
|
sub used_in_checkers
|
|
{
|
|
my $self = shift;
|
|
if (!exists $self->{used_in_checkers})
|
|
{
|
|
($self->{used_in_checkers}) = Bugzilla->dbh->selectrow_array('SELECT 1 FROM checkers WHERE query_id=?', undef, $self->id);
|
|
}
|
|
return $self->{used_in_checkers};
|
|
}
|
|
|
|
sub link_in_footer
|
|
{
|
|
my ($self, $user) = @_;
|
|
# We only cache link_in_footer for the current Bugzilla->user.
|
|
return $self->{link_in_footer} if exists $self->{link_in_footer} && !$user;
|
|
my $user_id = $user ? $user->id : Bugzilla->user->id;
|
|
my $link_in_footer = Bugzilla->dbh->selectrow_array(
|
|
'SELECT 1 FROM namedqueries_link_in_footer WHERE namedquery_id = ? AND user_id = ?',
|
|
undef, $self->id, $user_id
|
|
) || 0;
|
|
$self->{link_in_footer} = $link_in_footer if !$user;
|
|
return $link_in_footer;
|
|
}
|
|
|
|
sub shared_with_group
|
|
{
|
|
my ($self) = @_;
|
|
return $self->{shared_with_group} if exists $self->{shared_with_group};
|
|
# Bugzilla only currently supports sharing with one group, even
|
|
# though the database backend allows for an infinite number.
|
|
my ($group_id) = Bugzilla->dbh->selectrow_array(
|
|
'SELECT group_id FROM namedquery_group_map WHERE namedquery_id = ?',
|
|
undef, $self->id
|
|
);
|
|
$self->{shared_with_group} = $group_id ? new Bugzilla::Group($group_id) : undef;
|
|
return $self->{shared_with_group};
|
|
}
|
|
|
|
sub shared_with_users
|
|
{
|
|
my $self = shift;
|
|
my $dbh = Bugzilla->dbh;
|
|
if (!exists $self->{shared_with_users})
|
|
{
|
|
$self->{shared_with_users} = $dbh->selectrow_array(
|
|
'SELECT COUNT(*) FROM namedqueries_link_in_footer'.
|
|
' INNER JOIN namedqueries ON namedquery_id = id'.
|
|
' WHERE namedquery_id = ? AND user_id != userid',
|
|
undef, $self->id
|
|
);
|
|
}
|
|
return $self->{shared_with_users};
|
|
}
|
|
|
|
####################
|
|
# Simple Accessors #
|
|
####################
|
|
|
|
sub query { $_[0]->{query} }
|
|
sub userid { $_[0]->{userid} }
|
|
|
|
sub user
|
|
{
|
|
my ($self) = @_;
|
|
return $self->{user} if defined $self->{user};
|
|
return Bugzilla->user if Bugzilla->user->id == $self->{userid};
|
|
$self->{user} = new Bugzilla::User($self->{userid});
|
|
return $self->{user};
|
|
}
|
|
|
|
############
|
|
# Mutators #
|
|
############
|
|
|
|
sub set_name { $_[0]->set('name', $_[1]); }
|
|
sub set_query { $_[0]->set('query', $_[1]); }
|
|
|
|
sub set_link_in_footer
|
|
{
|
|
my $self = shift;
|
|
my ($link) = @_;
|
|
if ($link && !$self->link_in_footer)
|
|
{
|
|
Bugzilla->dbh->do(
|
|
'INSERT INTO namedqueries_link_in_footer (namedquery_id, user_id) VALUES (?, ?)',
|
|
undef, $self->id, Bugzilla->user->id
|
|
);
|
|
}
|
|
elsif (!$link && $self->link_in_footer)
|
|
{
|
|
Bugzilla->dbh->do(
|
|
'DELETE FROM namedqueries_link_in_footer WHERE namedquery_id=? AND user_id=?',
|
|
undef, $self->id, Bugzilla->user->id
|
|
);
|
|
}
|
|
delete $self->{link_in_footer};
|
|
}
|
|
|
|
sub set_shared_with_group
|
|
{
|
|
my $self = shift;
|
|
my ($group, $force_link_in_footer) = @_;
|
|
my $user = Bugzilla->user;
|
|
# Don't allow the user to share queries with groups he's not allowed to.
|
|
$group = undef if $group && !grep { $_ eq $group->id } @{$user->queryshare_groups};
|
|
# Don't remove namedqueries_link_in_footer entries for users
|
|
# subscribing to the shared query. The idea is that they will
|
|
# probably want to be subscribers again should the sharing
|
|
# user choose to share the query again.
|
|
my $dbh = Bugzilla->dbh;
|
|
$dbh->do('DELETE FROM namedquery_group_map WHERE namedquery_id=?', undef, $self->id);
|
|
if ($group)
|
|
{
|
|
# Share the query to $group
|
|
$dbh->do('INSERT INTO namedquery_group_map (namedquery_id, group_id) VALUES (?, ?)', undef, $self->id, $group->id);
|
|
|
|
# If we're sharing our query with a group we can bless, we
|
|
# have the ability to add link to our search to the footer of
|
|
# direct group members automatically.
|
|
my $force;
|
|
if ($force_link_in_footer && $user->can_bless($group->id) &&
|
|
($force = $group->members_non_inherited) &&
|
|
(@$force = grep { $_->id != $user->id } @$force))
|
|
{
|
|
$dbh->do(
|
|
'DELETE FROM namedqueries_link_in_footer WHERE namedquery_id='.$self->id.
|
|
' AND user_id IN ('.join(',', map { $_->id } @$force).')'
|
|
);
|
|
$dbh->do(
|
|
'INSERT INTO namedqueries_link_in_footer (namedquery_id, user_id) VALUES '.
|
|
join(',', map { '('.$self->id.','.$_->id.')' } @$force)
|
|
);
|
|
}
|
|
}
|
|
$self->{shared_with_group} = $group;
|
|
}
|
|
|
|
1;
|
|
|
|
__END__
|
|
|
|
=head1 NAME
|
|
|
|
Bugzilla::Search::Saved - A saved search
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
use Bugzilla::Search::Saved;
|
|
|
|
my $query = new Bugzilla::Search::Saved($query_id);
|
|
|
|
my $edit_link = $query->edit_link;
|
|
my $search_url = $query->query;
|
|
my $owner = $query->user;
|
|
my $num_subscribers = $query->shared_with_users;
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
This module exists to represent a L<Bugzilla::Search> that has been
|
|
saved to the database.
|
|
|
|
This is an implementation of L<Bugzilla::Object>, and so has all the
|
|
same methods available as L<Bugzilla::Object>, in addition to what is
|
|
documented below.
|
|
|
|
=head1 METHODS
|
|
|
|
=head2 Constructors and Database Manipulation
|
|
|
|
=over
|
|
|
|
=item C<new>
|
|
|
|
Takes either an id, or the named parameters C<user> and C<name>.
|
|
C<user> can be either a L<Bugzilla::User> object or a numeric user id.
|
|
|
|
See also: L<Bugzilla::Object/new>.
|
|
|
|
=item C<preload>
|
|
|
|
Sets C<link_in_footer> for all given saved searches at once, for the
|
|
currently logged in user. This is much faster than calling this method
|
|
for each saved search individually.
|
|
|
|
=back
|
|
|
|
|
|
=head2 Accessors
|
|
|
|
These return data about the object, without modifying the object.
|
|
|
|
=over
|
|
|
|
=item C<edit_link>
|
|
|
|
A url with which you can edit the search.
|
|
|
|
=item C<url>
|
|
|
|
The CGI parameters for the search, as a string.
|
|
|
|
=item C<link_in_footer>
|
|
|
|
Whether or not this search should be displayed in the footer for the
|
|
I<current user> (not the owner of the search, but the person actually
|
|
using Bugzilla right now).
|
|
|
|
=item C<type>
|
|
|
|
The numeric id of the type of search this is (from L<Bugzilla::Constants>).
|
|
|
|
=item C<shared_with_group>
|
|
|
|
The L<Bugzilla::Group> that this search is shared with. C<undef> if
|
|
this search isn't shared.
|
|
|
|
=item C<shared_with_users>
|
|
|
|
Returns how many users (besides the author of the saved search) are
|
|
using the saved search, i.e. have it displayed in their footer.
|
|
|
|
=back
|