Bug 40933

A big fat commit: MERGE with Bugzilla 3.4, released 28.07.2009.
(Patched with differences from 3.2.4 -> 3.4).
If you want a stable version, use previous revision.


git-svn-id: svn://svn.office.custis.ru/3rdparty/bugzilla.org/trunk@209 6955db30-a419-402b-8a0d-67ecbb4d7f56
custis
vfilippov 2009-07-29 11:21:49 +00:00
parent 1867a99935
commit 9295f56bb5
404 changed files with 33483 additions and 25737 deletions

View File

@ -42,6 +42,7 @@ use Bugzilla::Auth::Persist::Cookie;
use Bugzilla::CGI;
use Bugzilla::DB;
use Bugzilla::Install::Localconfig qw(read_localconfig);
use Bugzilla::JobQueue;
use Bugzilla::Template;
use Bugzilla::User;
use Bugzilla::Error;
@ -51,6 +52,7 @@ use Bugzilla::Flag;
use File::Basename;
use File::Spec::Functions;
use DateTime::TimeZone;
use Safe;
# This creates the request cache for non-mod_perl installations.
@ -82,11 +84,14 @@ use constant SHUTDOWNHTML_EXIT_SILENTLY => [
sub init_page {
(binmode STDOUT, ':utf8') if Bugzilla->params->{'utf8'};
if (${^TAINT}) {
# Some environment variables are not taint safe
delete @::ENV{'PATH', 'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
# Some modules throw undefined errors (notably File::Spec::Win32) if
# PATH is undefined.
$ENV{'PATH'} = '';
}
# IIS prints out warnings to the webpage, so ignore them, or log them
# to a file if the file exists.
@ -223,6 +228,10 @@ sub sudo_request {
# NOTE: If you want to log the start of an sudo session, do it here.
}
sub page_requires_login {
return $_[0]->request_cache->{page_requires_login};
}
sub login {
my ($class, $type) = @_;
@ -233,6 +242,13 @@ sub login {
if (!defined $type || $type == LOGIN_NORMAL) {
$type = $class->params->{'requirelogin'} ? LOGIN_REQUIRED : LOGIN_NORMAL;
}
# Allow templates to know that we're in a page that always requires
# login.
if ($type == LOGIN_REQUIRED) {
$class->request_cache->{page_requires_login} = 1;
}
my $authenticated_user = $authorizer->login($type);
# At this point, we now know if a real person is logged in.
@ -311,6 +327,12 @@ sub logout_request {
# there. Don't rely on it: use Bugzilla->user->login instead!
}
sub job_queue {
my $class = shift;
$class->request_cache->{job_queue} ||= Bugzilla::JobQueue->new();
return $class->request_cache->{job_queue};
}
sub dbh {
my $class = shift;
# If we're not connected, then we must want the main db
@ -346,7 +368,7 @@ sub error_mode {
$class->request_cache->{error_mode} = $newval;
}
return $class->request_cache->{error_mode}
|| Bugzilla::Constants::ERROR_MODE_WEBPAGE;
|| (i_am_cgi() ? ERROR_MODE_WEBPAGE : ERROR_MODE_DIE);
}
sub usage_mode {
@ -371,7 +393,7 @@ sub usage_mode {
$class->request_cache->{usage_mode} = $newval;
}
return $class->request_cache->{usage_mode}
|| Bugzilla::Constants::USAGE_MODE_BROWSER;
|| (i_am_cgi()? USAGE_MODE_BROWSER : USAGE_MODE_CMDLINE);
}
sub installation_mode {
@ -458,6 +480,16 @@ sub hook_args {
return $class->request_cache->{hook_args};
}
sub local_timezone {
my $class = shift;
if (!defined $class->request_cache->{local_timezone}) {
$class->request_cache->{local_timezone} =
DateTime::TimeZone->new(name => 'local');
}
return $class->request_cache->{local_timezone};
}
sub request_cache {
if ($ENV{MOD_PERL}) {
require Apache2::RequestUtil;
@ -602,6 +634,13 @@ Logs in a user, returning a C<Bugzilla::User> object, or C<undef> if there is
no logged in user. See L<Bugzilla::Auth|Bugzilla::Auth>, and
L<Bugzilla::User|Bugzilla::User>.
=item C<page_requires_login>
If the current page always requires the user to log in (for example,
C<enter_bug.cgi> or any page called with C<?GoAheadAndLogIn=1>) then
this will return something true. Otherwise it will return false. (This is
set when you call L</login>.)
=item C<logout($option)>
Logs out the current user, which involves invalidating user sessions and
@ -694,4 +733,16 @@ is unreadable or is not valid perl, we C<die>.
If you are running inside a code hook (see L<Bugzilla::Hook>) this
is how you get the arguments passed to the hook.
=item C<local_timezone>
Returns the local timezone of the Bugzilla installation,
as a DateTime::TimeZone object. This detection is very time
consuming, so we cache this information for future references.
=item C<job_queue>
Returns a L<Bugzilla::JobQueue> that you can use for queueing jobs.
Will throw an error if job queueing is not correctly configured on
this Bugzilla installation.
=back

View File

@ -28,23 +28,26 @@ package Bugzilla::Attachment;
=head1 NAME
Bugzilla::Attachment - a file related to a bug that a user has uploaded
to the Bugzilla server
Bugzilla::Attachment - Bugzilla attachment class.
=head1 SYNOPSIS
use Bugzilla::Attachment;
# Get the attachment with the given ID.
my $attachment = Bugzilla::Attachment->get($attach_id);
my $attachment = new Bugzilla::Attachment($attach_id);
# Get the attachments with the given IDs.
my $attachments = Bugzilla::Attachment->get_list($attach_ids);
my $attachments = Bugzilla::Attachment->new_from_list($attach_ids);
=head1 DESCRIPTION
This module defines attachment objects, which represent files related to bugs
that users upload to the Bugzilla server.
Attachment.pm represents an attachment object. It is an implementation
of L<Bugzilla::Object>, and thus provides all methods that
L<Bugzilla::Object> provides.
The methods that are specific to C<Bugzilla::Attachment> are listed
below.
=cut
@ -55,82 +58,44 @@ use Bugzilla::User;
use Bugzilla::Util;
use Bugzilla::Field;
sub get {
my $invocant = shift;
my $id = shift;
use base qw(Bugzilla::Object);
my $attachments = _retrieve([$id]);
my $self = $attachments->[0];
bless($self, ref($invocant) || $invocant) if $self;
###############################
#### Initialization ####
###############################
return $self;
}
use constant DB_TABLE => 'attachments';
use constant ID_FIELD => 'attach_id';
use constant LIST_ORDER => ID_FIELD;
sub get_list {
my $invocant = shift;
my $ids = shift;
my $attachments = _retrieve($ids);
foreach my $attachment (@$attachments) {
bless($attachment, ref($invocant) || $invocant);
}
return $attachments;
}
sub _retrieve {
my ($ids) = @_;
return [] if scalar(@$ids) == 0;
my @columns = (
'attachments.attach_id AS id',
'attachments.bug_id AS bug_id',
'attachments.description AS description',
'attachments.mimetype AS contenttype',
'attachments.submitter_id AS attacher_id',
Bugzilla->dbh->sql_date_format('attachments.creation_ts',
'%Y.%m.%d %H:%i') . " AS attached",
'attachments.modification_time',
'attachments.filename AS filename',
'attachments.ispatch AS ispatch',
'attachments.isurl AS isurl',
'attachments.isobsolete AS isobsolete',
'attachments.isprivate AS isprivate'
);
my $columns = join(", ", @columns);
sub DB_COLUMNS {
my $dbh = Bugzilla->dbh;
my $records = $dbh->selectall_arrayref(
"SELECT $columns
FROM attachments
WHERE "
. Bugzilla->dbh->sql_in('attach_id', $ids)
. " ORDER BY attach_id",
{ Slice => {} });
return $records;
return qw(
attach_id
bug_id
description
filename
isobsolete
ispatch
isprivate
isurl
mimetype
modification_time
submitter_id),
$dbh->sql_date_format('attachments.creation_ts', '%Y.%m.%d %H:%i') . ' AS creation_ts';
}
###############################
#### Accessors ######
###############################
=pod
=head2 Instance Properties
=over
=item C<id>
the unique identifier for the attachment
=back
=cut
sub id {
my $self = shift;
return $self->{id};
}
=over
=item C<bug_id>
the ID of the bug to which the attachment is attached
@ -139,8 +104,6 @@ the ID of the bug to which the attachment is attached
=cut
# XXX Once Bug.pm slims down sufficiently this should become a reference
# to a bug object.
sub bug_id {
my $self = shift;
return $self->{bug_id};
@ -148,6 +111,24 @@ sub bug_id {
=over
=item C<bug>
the bug object to which the attachment is attached
=back
=cut
sub bug {
my $self = shift;
require Bugzilla::Bug;
$self->{bug} = Bugzilla::Bug->new($self->bug_id);
return $self->{bug};
}
=over
=item C<description>
user-provided text describing the attachment
@ -173,7 +154,7 @@ the attachment's MIME media type
sub contenttype {
my $self = shift;
return $self->{contenttype};
return $self->{mimetype};
}
=over
@ -189,7 +170,7 @@ the user who attached the attachment
sub attacher {
my $self = shift;
return $self->{attacher} if exists $self->{attacher};
$self->{attacher} = new Bugzilla::User($self->{attacher_id});
$self->{attacher} = new Bugzilla::User($self->{submitter_id});
return $self->{attacher};
}
@ -205,7 +186,7 @@ the date and time on which the attacher attached the attachment
sub attached {
my $self = shift;
return $self->{attached};
return $self->{creation_ts};
}
=over
@ -351,7 +332,7 @@ sub data {
FROM attach_data
WHERE id = ?",
undef,
$self->{id});
$self->id);
# If there's no attachment data in the database, the attachment is stored
# in a local file, so retrieve it from there.
@ -396,7 +377,7 @@ sub datasize {
Bugzilla->dbh->selectrow_array("SELECT LENGTH(thedata)
FROM attach_data
WHERE id = ?",
undef, $self->{id}) || 0;
undef, $self->id) || 0;
# If there's no attachment data in the database, either the attachment
# is stored in a local file, and so retrieve its size from the file,
@ -430,6 +411,34 @@ sub flags {
return $self->{flags};
}
=over
=item C<flag_types>
Return all flag types available for this attachment as well as flags
already set, grouped by flag type.
=back
=cut
sub flag_types {
my $self = shift;
return $self->{flag_types} if exists $self->{flag_types};
my $vars = { target_type => 'attachment',
product_id => $self->bug->product_id,
component_id => $self->bug->component_id,
attach_id => $self->id };
$self->{flag_types} = Bugzilla::Flag::_flag_types($vars);
return $self->{flag_types};
}
###############################
#### Validators ######
###############################
# Instance methods; no POD documentation here yet because the only ones so far
# are private.
@ -468,9 +477,7 @@ sub _validate_filename {
sub _validate_data {
my ($throw_error, $hr_vars) = @_;
my $cgi = Bugzilla->cgi;
my $maxsize = $cgi->param('ispatch') ? Bugzilla->params->{'maxpatchsize'}
: Bugzilla->params->{'maxattachmentsize'};
$maxsize *= 1024; # Convert from K
my $fh;
# Skip uploading into a local variable if the user wants to upload huge
# attachments into local files.
@ -514,6 +521,7 @@ sub _validate_data {
}
# Make sure the attachment does not exceed the maximum permitted size
my $maxsize = Bugzilla->params->{'maxattachmentsize'} * 1024; # Convert from K
my $len = $data ? length($data) : 0;
if ($maxsize && $len > $maxsize) {
my $vars = { filesize => sprintf("%.0f", $len/1024) };
@ -547,7 +555,7 @@ Returns: a reference to an array of attachment objects.
=cut
sub get_attachments_by_bug {
my ($class, $bug_id) = @_;
my ($class, $bug_id, $vars) = @_;
my $user = Bugzilla->user;
my $dbh = Bugzilla->dbh;
@ -564,7 +572,25 @@ sub get_attachments_by_bug {
my $attach_ids = $dbh->selectcol_arrayref("SELECT attach_id FROM attachments
WHERE bug_id = ? $and_restriction",
undef, @values);
my $attachments = Bugzilla::Attachment->get_list($attach_ids);
my $attachments = Bugzilla::Attachment->new_from_list($attach_ids);
# To avoid $attachment->flags to run SQL queries itself for each
# attachment listed here, we collect all the data at once and
# populate $attachment->{flags} ourselves.
if ($vars->{preload}) {
$_->{flags} = [] foreach @$attachments;
my %att = map { $_->id => $_ } @$attachments;
my $flags = Bugzilla::Flag->match({ bug_id => $bug_id,
target_type => 'attachment' });
# Exclude flags for private attachments you cannot see.
@$flags = grep {exists $att{$_->attach_id}} @$flags;
push(@{$att{$_->attach_id}->{flags}}, $_) foreach @$flags;
$attachments = [sort {$a->id <=> $b->id} values %att];
}
return $attachments;
}
@ -731,10 +757,9 @@ sub validate_obsolete {
detaint_natural($attachid)
|| ThrowCodeError('invalid_attach_id_to_obsolete', $vars);
my $attachment = Bugzilla::Attachment->get($attachid);
# Make sure the attachment exists in the database.
ThrowUserError('invalid_attach_id', $vars) unless $attachment;
my $attachment = new Bugzilla::Attachment($attachid)
|| ThrowUserError('invalid_attach_id', $vars);
# Check that the user can view and edit this attachment.
$attachment->validate_can_edit($bug->product_id);
@ -756,10 +781,13 @@ sub validate_obsolete {
return @obsolete_attachments;
}
###############################
#### Constructors #####
###############################
=pod
=item C<insert_attachment_for_bug($throw_error, $bug, $user, $timestamp, $hr_vars)>
=item C<create($throw_error, $bug, $user, $timestamp, $hr_vars)>
Description: inserts an attachment from CGI input for the given bug.
@ -776,7 +804,8 @@ Returns: the ID of the new attachment.
=cut
sub insert_attachment_for_bug {
# FIXME: needs to follow the way Object->create() works.
sub create {
my ($class, $throw_error, $bug, $user, $timestamp, $hr_vars) = @_;
my $cgi = Bugzilla->cgi;
@ -934,7 +963,7 @@ sub insert_attachment_for_bug {
$timestamp, $fieldid, 0, 1));
}
my $attachment = Bugzilla::Attachment->get($attachid);
my $attachment = new Bugzilla::Attachment($attachid);
# 1. Add flags, if any. To avoid dying if something goes wrong
# while processing flags, we will eval() flag validation.

View File

@ -26,16 +26,24 @@ use fields qw(
_stack
successful
);
use Hash::Util qw(lock_keys);
use Bugzilla::Hook;
sub new {
my $class = shift;
my $self = $class->SUPER::new(@_);
my $list = shift;
my %methods = map { $_ => "Bugzilla/Auth/Login/$_.pm" } split(',', $list);
lock_keys(%methods);
Bugzilla::Hook::process('auth-login_methods', { modules => \%methods });
$self->{_stack} = [];
foreach my $login_method (split(',', $list)) {
require "Bugzilla/Auth/Login/${login_method}.pm";
push(@{$self->{_stack}},
"Bugzilla::Auth::Login::$login_method"->new(@_));
my $module = $methods{$login_method};
require $module;
$module =~ s|/|::|g;
$module =~ s/.pm$//;
push(@{$self->{_stack}}, $module->new(@_));
}
return $self;
}

View File

@ -161,6 +161,7 @@ sub clear_browser_cookies {
my $cgi = Bugzilla->cgi;
$cgi->remove_cookie('Bugzilla_login');
$cgi->remove_cookie('Bugzilla_logincookie');
$cgi->remove_cookie('sudo');
}
1;

View File

@ -53,14 +53,9 @@ sub check_credentials {
"SELECT cryptpassword FROM profiles WHERE userid = ?",
undef, $user_id);
# Wide characters cause crypt to die
if (Bugzilla->params->{'utf8'}) {
utf8::encode($password) if utf8::is_utf8($password);
}
# Using the internal crypted password as the salt,
# crypt the password the user entered.
my $entered_password_crypted = crypt($password, $real_password_crypted);
my $entered_password_crypted = bz_crypt($password, $real_password_crypted);
return { failure => AUTH_LOGINFAILED }
if $entered_password_crypted ne $real_password_crypted;
@ -69,6 +64,16 @@ sub check_credentials {
# password tokens they may have generated.
Bugzilla::Token::DeletePasswordTokens($user_id, "user_logged_in");
# If their old password was using crypt() or some different hash
# than we're using now, convert the stored password to using
# whatever hashing system we're using now.
my $current_algorithm = PASSWORD_DIGEST_ALGORITHM;
if ($real_password_crypted !~ /{\Q$current_algorithm\E}$/) {
my $new_crypted = bz_crypt($password);
$dbh->do('UPDATE profiles SET cryptpassword = ? WHERE userid = ?',
undef, $new_crypted, $user_id);
}
return $login_data;
}

View File

@ -21,16 +21,24 @@ use fields qw(
_stack
successful
);
use Hash::Util qw(lock_keys);
use Bugzilla::Hook;
sub new {
my $class = shift;
my $list = shift;
my $self = $class->SUPER::new(@_);
my %methods = map { $_ => "Bugzilla/Auth/Verify/$_.pm" } split(',', $list);
lock_keys(%methods);
Bugzilla::Hook::process('auth-verify_methods', { modules => \%methods });
$self->{_stack} = [];
foreach my $verify_method (split(',', $list)) {
require "Bugzilla/Auth/Verify/${verify_method}.pm";
push(@{$self->{_stack}},
"Bugzilla::Auth::Verify::$verify_method"->new(@_));
my $module = $methods{$verify_method};
require $module;
$module =~ s|/|::|g;
$module =~ s/.pm$//;
push(@{$self->{_stack}}, $module->new(@_));
}
return $self;
}

View File

@ -47,14 +47,15 @@ use Bugzilla::Status;
use List::Util qw(min);
use Storable qw(dclone);
use URI;
use URI::QueryParam;
use base qw(Bugzilla::Object Exporter);
@Bugzilla::Bug::EXPORT = qw(
bug_alias_to_id ValidateBugID
bug_alias_to_id
RemoveVotes CheckIfVotedConfirmed
LogActivityEntry
editable_bug_fields
SPECIAL_STATUS_WORKFLOW_ACTIONS
);
#####################################################################
@ -72,7 +73,8 @@ sub DB_COLUMNS {
my @custom = grep {$_->type != FIELD_TYPE_MULTI_SELECT}
Bugzilla->active_custom_fields;
my @custom_names = map {$_->name} @custom;
return qw(
my @columns = (qw(
alias
assigned_to
bug_file_loc
@ -100,7 +102,11 @@ sub DB_COLUMNS {
'reporter AS reporter_id',
$dbh->sql_date_format('creation_ts', '%Y.%m.%d %H:%i') . ' AS creation_ts',
$dbh->sql_date_format('deadline', '%Y-%m-%d') . ' AS deadline',
@custom_names;
@custom_names);
Bugzilla::Hook::process("bug-columns", { columns => \@columns });
return @columns;
}
use constant REQUIRED_CREATE_FIELDS => qw(
@ -145,6 +151,9 @@ sub VALIDATORS {
elsif ($field->type == FIELD_TYPE_FREETEXT) {
$validator = \&_check_freetext_field;
}
elsif ($field->type == FIELD_TYPE_BUG_ID) {
$validator = \&_check_bugid_field;
}
else {
$validator = \&_check_default_field;
}
@ -225,13 +234,6 @@ use constant UPDATE_COMMENT_COLUMNS => qw(
# activity table.
use constant MAX_LINE_LENGTH => 254;
use constant SPECIAL_STATUS_WORKFLOW_ACTIONS => qw(
none
duplicate
change_resolution
clearresolution
);
#####################################################################
sub new {
@ -239,6 +241,11 @@ sub new {
my $class = ref($invocant) || $invocant;
my $param = shift;
# Remove leading "#" mark if we've just been passed an id.
if (!ref $param && $param =~ /^#(\d+)$/) {
$param = $1;
}
# If we get something that looks like a word (not a number),
# make it the "name" param.
if (!defined $param || (!ref($param) && $param !~ /^\d+$/)) {
@ -263,15 +270,126 @@ sub new {
# if the bug wasn't found in the database.
if (!$self) {
my $error_self = {};
if (ref $param) {
$error_self->{bug_id} = $param->{name};
$error_self->{error} = 'InvalidBugId';
}
else {
$error_self->{bug_id} = $param;
$error_self->{error} = 'NotFound';
}
bless $error_self, $class;
$error_self->{'bug_id'} = ref($param) ? $param->{name} : $param;
$error_self->{'error'} = 'NotFound';
return $error_self;
}
return $self;
}
sub check {
my $class = shift;
my ($id, $field) = @_;
ThrowUserError('improper_bug_id_field_value', { field => $field }) unless defined $id;
# Bugzilla::Bug throws lots of special errors, so we don't call
# SUPER::check, we just call our new and do our own checks.
my $self = $class->new(trim($id));
# For error messages, use the id that was returned by new(), because
# it's cleaned up.
$id = $self->id;
if ($self->{error}) {
if ($self->{error} eq 'NotFound') {
ThrowUserError("bug_id_does_not_exist", { bug_id => $id });
}
if ($self->{error} eq 'InvalidBugId') {
ThrowUserError("improper_bug_id_field_value",
{ bug_id => $id,
field => $field });
}
}
unless ($field && $field =~ /^(dependson|blocked|dup_id)$/) {
$self->check_is_visible;
}
return $self;
}
sub check_is_visible {
my $self = shift;
my $user = Bugzilla->user;
if (!$user->can_see_bug($self->id)) {
# The error the user sees depends on whether or not they are
# logged in (i.e. $user->id contains the user's positive integer ID).
if ($user->id) {
ThrowUserError("bug_access_denied", { bug_id => $self->id });
} else {
ThrowUserError("bug_access_query", { bug_id => $self->id });
}
}
}
sub match {
my $class = shift;
my ($params) = @_;
# Allow matching certain fields by name (in addition to matching by ID).
my %translate_fields = (
assigned_to => 'Bugzilla::User',
qa_contact => 'Bugzilla::User',
reporter => 'Bugzilla::User',
product => 'Bugzilla::Product',
component => 'Bugzilla::Component',
);
my %translated;
foreach my $field (keys %translate_fields) {
my @ids;
# Convert names to ids. We use "exists" everywhere since people can
# legally specify "undef" to mean IS NULL (even though most of these
# fields can't be NULL, people can still specify it...).
if (exists $params->{$field}) {
my $names = $params->{$field};
my $type = $translate_fields{$field};
my $param = $type eq 'Bugzilla::User' ? 'login_name' : 'name';
# We call Bugzilla::Object::match directly to avoid the
# Bugzilla::User::match implementation which is different.
my $objects = Bugzilla::Object::match($type, { $param => $names });
push(@ids, map { $_->id } @$objects);
}
# You can also specify ids directly as arguments to this function,
# so include them in the list if they have been specified.
if (exists $params->{"${field}_id"}) {
my $current_ids = $params->{"${field}_id"};
my @id_array = ref $current_ids ? @$current_ids : ($current_ids);
push(@ids, @id_array);
}
# We do this "or" instead of a "scalar(@ids)" to handle the case
# when people passed only invalid object names. Otherwise we'd
# end up with a SUPER::match call with zero criteria (which dies).
if (exists $params->{$field} or exists $params->{"${field}_id"}) {
$translated{$field} = scalar(@ids) == 1 ? $ids[0] : \@ids;
}
}
# The user fields don't have an _id on the end of them in the database,
# but the product & component fields do, so we have to have separate
# code to deal with the different sets of fields here.
foreach my $field (qw(assigned_to qa_contact reporter)) {
delete $params->{"${field}_id"};
$params->{$field} = $translated{$field}
if exists $translated{$field};
}
foreach my $field (qw(product component)) {
delete $params->{$field};
$params->{"${field}_id"} = $translated{$field}
if exists $translated{$field};
}
return $class->SUPER::match(@_);
}
# Docs for create() (there's no POD in this file yet, but we very
# much need this documented right now):
#
@ -424,6 +542,10 @@ sub create {
$dbh->do('INSERT INTO longdescs (' . join(',', @columns) . ")
VALUES ($qmarks)", undef, @values);
Bugzilla::Hook::process('bug-end_of_create', { bug => $bug,
timestamp => $timestamp,
});
$dbh->bz_commit_transaction();
# Because MySQL doesn't support transactions on the fulltext table,
@ -493,6 +615,33 @@ sub run_create_validators {
return $params;
}
sub set_all {
my ($self, $args) = @_;
# For security purposes, and because lots of other checks depend on it,
# we set the product first before anything else.
my $product_change = 0;
if ($args->{product}) {
my $changed = $self->set_product($args->{product},
{ component => $args->{component},
version => $args->{version},
target_milestone => $args->{target_milestone},
change_confirmed => $args->{confirm_product_change},
other_bugs => $args->{other_bugs},
});
# that will be used later to check strict isolation
$product_change = $changed;
}
# add/remove groups
$self->remove_group($_) foreach @{$args->{remove_group}};
$self->add_group($_) foreach @{$args->{add_group}};
# this is temporary until all related code is moved from
# process_bug.cgi to set_all
return $product_change;
}
sub update {
my $self = shift;
@ -502,8 +651,7 @@ sub update {
# inside this function.
my $delta_ts = shift || $dbh->selectrow_array("SELECT NOW()");
my $old_bug = $self->new($self->id);
my $changes = $self->SUPER::update(@_);
my ($changes, $old_bug) = $self->SUPER::update(@_);
# Certain items in $changes have to be fixed so that they hold
# a name instead of an ID.
@ -685,6 +833,25 @@ sub update {
}
}
# See Also
my ($removed_see, $added_see) =
diff_arrays($old_bug->see_also, $self->see_also);
if (scalar @$removed_see) {
$dbh->do('DELETE FROM bug_see_also WHERE bug_id = ? AND '
. $dbh->sql_in('value', [('?') x @$removed_see]),
undef, $self->id, @$removed_see);
}
foreach my $url (@$added_see) {
$dbh->do('INSERT INTO bug_see_also (bug_id, value) VALUES (?,?)',
undef, $self->id, $url);
}
# If any changes were found, record it in the activity log
if (scalar @$removed_see || scalar @$added_see) {
$changes->{see_also} = [join(', ', @$removed_see),
join(', ', @$added_see)];
}
# Log bugs_activity items
# XXX Eventually, when bugs_activity is able to track the dupe_id,
# this code should go below the duplicates-table-updating code below.
@ -734,6 +901,10 @@ sub update {
delete $self->{'_old_assigned_to'};
delete $self->{'_old_qa_contact'};
# Also flush the visible_bugs cache for this bug as the user's
# relationship with this bug may have changed.
delete Bugzilla->user->{_visible_bugs_cache}->{$self->id};
return $changes;
}
@ -811,7 +982,6 @@ sub remove_from_db {
# - keywords
# - longdescs
# - votes
# Also included are custom multi-select fields.
# Also, the attach_data table uses attachments.attach_id as a foreign
# key, and so indirectly depends on a bug deletion too.
@ -844,13 +1014,6 @@ sub remove_from_db {
$dbh->do("DELETE FROM bugs WHERE bug_id = ?", undef, $bug_id);
$dbh->do("DELETE FROM longdescs WHERE bug_id = ?", undef, $bug_id);
# Delete entries from custom multi-select fields.
my @multi_selects = Bugzilla->get_fields({custom => 1, type => FIELD_TYPE_MULTI_SELECT});
foreach my $field (@multi_selects) {
$dbh->do("DELETE FROM bug_" . $field->name . " WHERE bug_id = ?", undef, $bug_id);
}
$dbh->bz_commit_transaction();
# The bugs_fulltext table doesn't support transactions.
@ -1069,9 +1232,10 @@ sub _check_comment {
sub _check_commentprivacy {
my ($invocant, $comment_privacy) = @_;
my $insider_group = Bugzilla->params->{"insidergroup"};
return ($insider_group && Bugzilla->user->in_group($insider_group)
&& $comment_privacy) ? 1 : 0;
if ($comment_privacy && !Bugzilla->user->is_insider) {
ThrowUserError('user_not_insider');
}
return $comment_privacy ? 1 : 0;
}
sub _check_comment_type {
@ -1123,11 +1287,13 @@ sub _check_dependencies {
my %deps_in = (dependson => $depends_on || '', blocked => $blocks || '');
foreach my $type qw(dependson blocked) {
my @bug_ids = split(/[\s,]+/, $deps_in{$type});
my @bug_ids = ref($deps_in{$type})
? @{$deps_in{$type}}
: split(/[\s,]+/, $deps_in{$type});
# Eliminate nulls.
@bug_ids = grep {$_} @bug_ids;
# We do Validate up here to make sure all aliases are converted to IDs.
ValidateBugID($_, $type) foreach @bug_ids;
# We do this up here to make sure all aliases are converted to IDs.
@bug_ids = map { $invocant->check($_, $type)->id } @bug_ids;
my @check_access = @bug_ids;
# When we're updating a bug, only added or removed bug_ids are
@ -1149,11 +1315,10 @@ sub _check_dependencies {
my $user = Bugzilla->user;
foreach my $modified_id (@check_access) {
ValidateBugID($modified_id);
my $delta_bug = $invocant->check($modified_id);
# Under strict isolation, you can't modify a bug if you can't
# edit it, even if you can see it.
if (Bugzilla->params->{"strict_isolation"}) {
my $delta_bug = new Bugzilla::Bug($modified_id);
if (!$user->can_edit_product($delta_bug->{'product_id'})) {
ThrowUserError("illegal_change_deps", {field => $type});
}
@ -1176,17 +1341,18 @@ sub _check_dup_id {
$dupe_of = trim($dupe_of);
$dupe_of || ThrowCodeError('undefined_field', { field => 'dup_id' });
# Validate the bug ID. The second argument will force ValidateBugID() to
# only make sure that the bug exists, and convert the alias to the bug ID
# Validate the bug ID. The second argument will force check() to only
# make sure that the bug exists, and convert the alias to the bug ID
# if a string is passed. Group restrictions are checked below.
ValidateBugID($dupe_of, 'dup_id');
my $dupe_of_bug = $self->check($dupe_of, 'dup_id');
$dupe_of = $dupe_of_bug->id;
# If the dupe is unchanged, we have nothing more to check.
return $dupe_of if ($self->dup_id && $self->dup_id == $dupe_of);
# If we come here, then the duplicate is new. We have to make sure
# that we can view/change it (issue A on bug 96085).
check_is_visible($dupe_of);
$dupe_of_bug->check_is_visible;
# Make sure a loop isn't created when marking this bug
# as duplicate.
@ -1218,7 +1384,6 @@ sub _check_dup_id {
# Should we add the reporter to the CC list of the new bug?
# If he can see the bug...
if ($self->reporter->can_see_bug($dupe_of)) {
my $dupe_of_bug = new Bugzilla::Bug($dupe_of);
# We only add him if he's not the reporter of the other bug.
$self->{_add_dup_cc} = 1
if $dupe_of_bug->reporter->id != $self->reporter->id;
@ -1243,9 +1408,7 @@ sub _check_dup_id {
my $vars = {};
my $template = Bugzilla->template;
# Ask the user what they want to do about the reporter.
$vars->{'cclist_accessible'} = $dbh->selectrow_array(
q{SELECT cclist_accessible FROM bugs WHERE bug_id = ?},
undef, $dupe_of);
$vars->{'cclist_accessible'} = $dupe_of_bug->cclist_accessible;
$vars->{'original_bug_id'} = $dupe_of;
$vars->{'duplicate_bug_id'} = $self->id;
print $cgi->header();
@ -1656,6 +1819,12 @@ sub _check_select_field {
return $value;
}
sub _check_bugid_field {
my ($invocant, $value, $field) = @_;
return undef if !$value;
return $invocant->check($value, $field)->id;
}
#####################################################################
# Class Accessors
#####################################################################
@ -1663,14 +1832,15 @@ sub _check_select_field {
sub fields {
my $class = shift;
return (
my @fields =
(
# Standard Fields
# Keep this ordering in sync with bugzilla.dtd.
qw(bug_id alias creation_ts short_desc delta_ts
reporter_accessible cclist_accessible
classification_id classification
product component version rep_platform op_sys
bug_status resolution dup_id
bug_status resolution dup_id see_also
bug_file_loc status_whiteboard keywords
priority bug_severity target_milestone
dependson blocked votes everconfirmed
@ -1682,6 +1852,9 @@ sub fields {
# Custom Fields
map { $_->name } Bugzilla->active_custom_fields
);
Bugzilla::Hook::process("bug-fields", {'fields' => \@fields} );
return @fields;
}
#####################################################################
@ -1730,11 +1903,6 @@ sub set_assigned_to {
}
sub reset_assigned_to {
my $self = shift;
if (Bugzilla->params->{'commentonreassignbycomponent'}
&& !$self->{added_comments})
{
ThrowUserError('comment_required');
}
my $comp = $self->component_obj;
$self->set_assigned_to($comp->default_assignee);
}
@ -1975,11 +2143,6 @@ sub set_qa_contact {
}
sub reset_qa_contact {
my $self = shift;
if (Bugzilla->params->{'commentonreassignbycomponent'}
&& !$self->{added_comments})
{
ThrowUserError('comment_required');
}
my $comp = $self->component_obj;
$self->set_qa_contact($comp->default_qa_contact);
}
@ -2028,11 +2191,6 @@ sub clear_resolution {
if (!$self->status->is_open) {
ThrowUserError('resolution_cant_clear', { bug_id => $self->id });
}
if (Bugzilla->params->{'commentonclearresolution'}
&& $self->resolution && !$self->{added_comments})
{
ThrowUserError('comment_required');
}
$self->{'resolution'} = '';
$self->_clear_dup_id;
}
@ -2280,6 +2438,99 @@ sub remove_group {
@$current_groups = grep { $_->id != $group->id } @$current_groups;
}
sub add_see_also {
my ($self, $input) = @_;
$input = trim($input);
# We assume that the URL is an HTTP URL if there is no (something)://
# in front.
my $uri = new URI($input);
if (!$uri->scheme) {
# This works better than setting $uri->scheme('http'), because
# that creates URLs like "http:domain.com" and doesn't properly
# differentiate the path from the domain.
$uri = new URI("http://$input");
}
elsif ($uri->scheme ne 'http' && $uri->scheme ne 'https') {
ThrowUserError('bug_url_invalid', { url => $input, reason => 'http' });
}
my $result;
# Launchpad URLs
if ($uri->authority =~ /launchpad.net$/) {
# Launchpad bug URLs can look like various things:
# https://bugs.launchpad.net/ubuntu/+bug/1234
# https://launchpad.net/bugs/1234
# All variations end with either "/bugs/1234" or "/+bug/1234"
if ($uri->path =~ m|bugs?/(\d+)$|) {
# This is the shortest standard URL form for Launchpad bugs,
# and so we reduce all URLs to this.
$result = "https://launchpad.net/bugs/$1";
}
else {
ThrowUserError('bug_url_invalid',
{ url => $input, reason => 'id' });
}
}
# Bugzilla URLs
else {
if ($uri->path !~ /show_bug\.cgi$/) {
ThrowUserError('bug_url_invalid',
{ url => $input, reason => 'show_bug' });
}
my $bug_id = $uri->query_param('id');
# We don't currently allow aliases, because we can't check to see
# if somebody's putting both an alias link and a numeric ID link.
# When we start validating the URL by accessing the other Bugzilla,
# we can allow aliases.
detaint_natural($bug_id);
if (!$bug_id) {
ThrowUserError('bug_url_invalid',
{ url => $input, reason => 'id' });
}
# Make sure that "id" is the only query parameter.
$uri->query("id=$bug_id");
# And remove any # part if there is one.
$uri->fragment(undef);
$result = $uri->canonical->as_string;
}
if (length($result) > MAX_BUG_URL_LENGTH) {
ThrowUserError('bug_url_too_long', { url => $result });
}
# We only add the new URI if it hasn't been added yet. URIs are
# case-sensitive, but most of our DBs are case-insensitive, so we do
# this check case-insensitively.
if (!grep { lc($_) eq lc($result) } @{ $self->see_also }) {
my $privs;
my $can = $self->check_can_change_field('see_also', '', $result, \$privs);
if (!$can) {
ThrowUserError('illegal_change', { field => 'see_also',
newvalue => $result,
privs => $privs });
}
push(@{ $self->see_also }, $result);
}
}
sub remove_see_also {
my ($self, $url) = @_;
my $see_also = $self->see_also;
my @new_see_also = grep { lc($_) ne lc($url) } @$see_also;
my $privs;
my $can = $self->check_can_change_field('see_also', $see_also, \@new_see_also, \$privs);
if (!$can) {
ThrowUserError('illegal_change', { field => 'see_also',
oldvalue => $url,
privs => $privs });
}
$self->{see_also} = \@new_see_also;
}
#####################################################################
# Instance Accessors
#####################################################################
@ -2347,22 +2598,8 @@ sub attachments {
return $self->{'attachments'} if exists $self->{'attachments'};
return [] if $self->{'error'};
my $attachments = Bugzilla::Attachment->get_attachments_by_bug($self->bug_id);
$_->{'flags'} = [] foreach @$attachments;
my %att = map { $_->id => $_ } @$attachments;
# Retrieve all attachment flags at once for this bug, and group them
# by attachment. We populate attachment flags here to avoid querying
# the DB for each attachment individually later.
my $flags = Bugzilla::Flag->match({ 'bug_id' => $self->bug_id,
'target_type' => 'attachment' });
# Exclude flags for private attachments you cannot see.
@$flags = grep {exists $att{$_->attach_id}} @$flags;
push(@{$att{$_->attach_id}->{'flags'}}, $_) foreach @$flags;
$self->{'attachments'} = [sort {$a->id <=> $b->id} values %att];
$self->{'attachments'} =
Bugzilla::Attachment->get_attachments_by_bug($self->bug_id, {preload => 1});
return $self->{'attachments'};
}
@ -2469,28 +2706,12 @@ sub flag_types {
return $self->{'flag_types'} if exists $self->{'flag_types'};
return [] if $self->{'error'};
# The types of flags that can be set on this bug.
# If none, no UI for setting flags will be displayed.
my $flag_types = Bugzilla::FlagType::match(
{'target_type' => 'bug',
'product_id' => $self->{'product_id'},
'component_id' => $self->{'component_id'} });
$_->{'flags'} = [] foreach @$flag_types;
my %flagtypes = map { $_->id => $_ } @$flag_types;
# Retrieve all bug flags at once for this bug and group them
# by flag types.
my $flags = Bugzilla::Flag->match({ 'bug_id' => $self->bug_id,
'target_type' => 'bug' });
# Call the internal 'type_id' variable instead of the method
# to not create a flagtype object.
push(@{$flagtypes{$_->{'type_id'}}->{'flags'}}, $_) foreach @$flags;
$self->{'flag_types'} =
[sort {$a->sortkey <=> $b->sortkey || $a->name cmp $b->name} values %flagtypes];
my $vars = { target_type => 'bug',
product_id => $self->{product_id},
component_id => $self->{component_id},
bug_id => $self->bug_id };
$self->{'flag_types'} = Bugzilla::Flag::_flag_types($vars);
return $self->{'flag_types'};
}
@ -2581,6 +2802,14 @@ sub reporter {
return $self->{'reporter'};
}
sub see_also {
my ($self) = @_;
return [] if $self->{'error'};
$self->{'see_also'} ||= Bugzilla->dbh->selectcol_arrayref(
'SELECT value FROM bug_see_also WHERE bug_id = ?', undef, $self->id);
return $self->{'see_also'};
}
sub status {
my $self = shift;
return undef if $self->{'error'};
@ -2850,12 +3079,18 @@ sub editable_bug_fields {
# XXX - When Bug::update() will be implemented, we should make this routine
# a private method.
# Join with bug_status and bugs tables to show bugs with open statuses first,
# and then the others
sub EmitDependList {
my ($myfield, $targetfield, $bug_id) = (@_);
my $dbh = Bugzilla->dbh;
my $list_ref = $dbh->selectcol_arrayref(
"SELECT $targetfield FROM dependencies
WHERE $myfield = ? ORDER BY $targetfield",
"SELECT $targetfield
FROM dependencies
INNER JOIN bugs ON dependencies.$targetfield = bugs.bug_id
INNER JOIN bug_status ON bugs.bug_status = bug_status.value
WHERE $myfield = ?
ORDER BY is_open DESC, $targetfield",
undef, $bug_id);
return $list_ref;
}
@ -2903,18 +3138,27 @@ sub GetComments {
INNER JOIN profiles
ON profiles.userid = longdescs.who
WHERE longdescs.bug_id = ?';
if ($start) {
$query .= ' AND longdescs.bug_when > ?
AND longdescs.bug_when <= ?';
push(@args, ($start, $end));
$query .= ' AND longdescs.bug_when > ?';
push(@args, $start);
}
if ($end) {
$query .= ' AND longdescs.bug_when <= ?';
push(@args, $end);
}
$query .= " ORDER BY longdescs.bug_when $sort_order";
my $sth = $dbh->prepare($query);
$sth->execute(@args);
# Cache the users we look up
my %users;
while (my $comment_ref = $sth->fetchrow_hashref()) {
my %comment = %$comment_ref;
$comment{'author'} = new Bugzilla::User($comment{'userid'});
$users{$comment{'userid'}} ||= new Bugzilla::User($comment{'userid'});
$comment{'author'} = $users{$comment{'userid'}};
# If raw data is requested, do not format 'special' comments.
$comment{'body'} = format_comment(\%comment) unless $raw;
@ -2929,34 +3173,20 @@ sub GetComments {
return \@comments;
}
# Format language specific comments. This routine must not update
# $comment{'body'} itself, see BugMail::prepare_comments().
# Format language specific comments.
sub format_comment {
my $comment = shift;
my $template = Bugzilla->template_inner;
my $vars = {comment => $comment};
my $body;
if ($comment->{'type'} == CMT_DUPE_OF) {
$body = $comment->{'body'} . "\n\n" .
get_text('bug_duplicate_of', { dupe_of => $comment->{'extra_data'} });
}
elsif ($comment->{'type'} == CMT_HAS_DUPE) {
$body = get_text('bug_has_duplicate', { dupe => $comment->{'extra_data'} });
}
elsif ($comment->{'type'} == CMT_POPULAR_VOTES) {
$body = get_text('bug_confirmed_by_votes');
}
elsif ($comment->{'type'} == CMT_MOVED_TO) {
$body = $comment->{'body'} . "\n\n" .
get_text('bug_moved_to', { login => $comment->{'extra_data'} });
}
else {
$body = $comment->{'body'};
}
$template->process("bug/format_comment.txt.tmpl", $vars, \$body)
|| ThrowTemplateError($template->error());
return $body;
}
# Get the activity of a bug, starting from $starttime (if given).
# This routine assumes ValidateBugID has been previously called.
# This routine assumes Bugzilla::Bug->check has been previously called.
sub GetBugActivity {
my ($bug_id, $attach_id, $starttime) = @_;
my $dbh = Bugzilla->dbh;
@ -3224,7 +3454,7 @@ sub RemoveVotes {
undef, ($votes, $id));
}
# Now return the array containing emails to be sent.
return \@messages;
return @messages;
}
# If a user votes for a bug, or the number of votes required to
@ -3420,59 +3650,6 @@ sub check_can_change_field {
# Field Validation
#
# Validates and verifies a bug ID, making sure the number is a
# positive integer, that it represents an existing bug in the
# database, and that the user is authorized to access that bug.
# We detaint the number here, too.
sub ValidateBugID {
my ($id, $field) = @_;
my $dbh = Bugzilla->dbh;
my $user = Bugzilla->user;
ThrowUserError('improper_bug_id_field_value', { field => $field }) unless defined $id;
# Get rid of leading '#' (number) mark, if present.
$id =~ s/^\s*#//;
# Remove whitespace
$id = trim($id);
# If the ID isn't a number, it might be an alias, so try to convert it.
my $alias = $id;
if (!detaint_natural($id)) {
$id = bug_alias_to_id($alias);
$id || ThrowUserError("improper_bug_id_field_value",
{'bug_id' => $alias,
'field' => $field });
}
# Modify the calling code's original variable to contain the trimmed,
# converted-from-alias ID.
$_[0] = $id;
# First check that the bug exists
$dbh->selectrow_array("SELECT bug_id FROM bugs WHERE bug_id = ?", undef, $id)
|| ThrowUserError("bug_id_does_not_exist", {'bug_id' => $id});
unless ($field && $field =~ /^(dependson|blocked|dup_id)$/) {
check_is_visible($id);
}
}
sub check_is_visible {
my $id = shift;
my $user = Bugzilla->user;
return if $user->can_see_bug($id);
# The error the user sees depends on whether or not they are logged in
# (i.e. $user->id contains the user's positive integer ID).
if ($user->id) {
ThrowUserError("bug_access_denied", {'bug_id' => $id});
} else {
ThrowUserError("bug_access_query", {'bug_id' => $id});
}
}
# Validate and return a hash of dependencies
sub ValidateDependencies {
my $fields = {};

View File

@ -217,6 +217,15 @@ sub Send {
$values{'blocked'} = join(",", @$blockedlist);
my $grouplist = $dbh->selectcol_arrayref(
' SELECT name FROM groups
INNER JOIN bug_group_map
ON groups.id = bug_group_map.group_id
AND bug_group_map.bug_id = ?',
undef, ($id));
$values{'bug_group'} = join(', ', @$grouplist);
my @args = ($id);
# If lastdiffed is NULL, then we don't limit the search on time.
@ -376,7 +385,7 @@ sub Send {
}
}
my ($raw_comments, $anyprivate, $count) = get_comments_by_bug($id, $start, $end);
my ($comments, $anyprivate) = get_comments_by_bug($id, $start, $end);
###########################################################################
# Start of email filtering code
@ -438,7 +447,6 @@ sub Send {
}
}
if (Bugzilla->params->{"supportwatchers"}) {
# Find all those user-watching anyone on the current list, who is not
# on it already themselves.
my $involved = join(",", keys %recipients);
@ -453,8 +461,7 @@ sub Send {
$recipients{$watch->[0]}->{$role} |= BIT_WATCHING
if $bits & BIT_DIRECT;
}
push (@{$watching{$watch->[0]}}, $watch->[1]);
}
push(@{$watching{$watch->[0]}}, $watch->[1]);
}
# Global watcher
@ -471,10 +478,6 @@ sub Send {
my @sent;
my @excluded;
# Some comments are language specific. We cache them here.
my %comments;
my %commentArray;
foreach my $user_id (keys %recipients) {
my %rels_which_want;
my $sent_mail = 0;
@ -483,25 +486,14 @@ sub Send {
# Deleted users must be excluded.
next unless $user;
# What's the language chosen by this user for email?
my $lang = $user->settings->{'lang'}->{'value'};
if ($user->can_see_bug($id)) {
# It's time to format language specific comments.
unless (exists $comments{$lang}) {
Bugzilla->template_inner($lang);
$comments{$lang} = prepare_comments($raw_comments, $count);
$commentArray{$lang} = $raw_comments;
Bugzilla->template_inner("");
}
# Go through each role the user has and see if they want mail in
# that role.
foreach my $relationship (keys %{$recipients{$user_id}}) {
if ($user->wants_bug_mail($id,
$relationship,
$diffs,
$comments{$lang},
$comments,
$deptext,
$changer,
!$start))
@ -519,9 +511,7 @@ sub Send {
# If we are using insiders, and the comment is private, only send
# to insiders
my $insider_ok = 1;
$insider_ok = 0 if (Bugzilla->params->{"insidergroup"} &&
($anyprivate != 0) &&
(!$user->groups->{Bugzilla->params->{"insidergroup"}}));
$insider_ok = 0 if $anyprivate && !$user->is_insider;
# We shouldn't send mail if this is a dependency mail (i.e. there
# is something in @depbugs), and any of the depending bugs are not
@ -551,8 +541,7 @@ sub Send {
fields => \%fielddescription,
diffs => \@diffparts,
diffar => \@diff_array,
newcomm => $comments{$lang} || [],
commarr => $commentArray{$lang} || [],
newcomm => $comments,
anypriv => $anyprivate,
isnew => !$start,
id => $id,
@ -579,11 +568,11 @@ sub sendMail
{
my %arguments = @_;
my ($user, $hlRef, $relRef, $valueRef, $dmhRef, $fdRef,
$diffRef, $diffArray, $newcomments, $commentArray, $anyprivate, $isnew,
$diffRef, $diffArray, $newcomments, $anyprivate, $isnew,
$id, $watchingRef
) = @arguments{qw(
user headers rels values defhead fields
diffs diffar newcomm commarr anypriv isnew
diffs diffar newcomm anypriv isnew
id watch
)};
@ -606,13 +595,12 @@ sub sendMail
($diff->{'fieldname'} eq 'estimated_time' ||
$diff->{'fieldname'} eq 'remaining_time' ||
$diff->{'fieldname'} eq 'work_time' ||
$diff->{'fieldname'} eq 'deadline')) {
if ($user->groups->{Bugzilla->params->{"timetrackinggroup"}}) {
$add_diff = 1;
}
} elsif ($diff->{'isprivate'} &&
Bugzilla->params->{'insidergroup'} &&
!$user->groups->{Bugzilla->params->{'insidergroup'}}) {
$diff->{'fieldname'} eq 'deadline'))
{
$add_diff = 1 if $user->is_timetracker;
} elsif ($diff->{'isprivate'}
&& !$user->is_insider)
{
$add_diff = 0;
} else {
$add_diff = 1;
@ -628,7 +616,7 @@ sub sendMail
}
}
if ($difftext eq "" && $newcomments eq "" && !$isnew) {
if (!$difftext && !$newcomments && !@$newcomments && !$isnew) {
# Whoops, no differences!
return 0;
}
@ -636,18 +624,14 @@ sub sendMail
# If an attachment was created, then add an URL. (Note: the 'g'lobal
# replace should work with comments with multiple attachments.)
if ($newcomments =~ /Created an attachment \(/) {
my $showattachurlbase =
Bugzilla->params->{'urlbase'} . "attachment.cgi?id=";
$newcomments =~ s/(Created an attachment \(id=([0-9]+)\))/$1\n --> \(${showattachurlbase}$2\)/g;
for (@$commentArray)
{
$_->{body} =~ s/Created an attachment \(id=([0-9]+)\)/Created <a href="$showattachurlbase$1">an attachment<\/a>/g;
}
my $showattachurlbase =
Bugzilla->params->{'urlbase'} . "attachment.cgi?id=";
for (@$newcomments)
{
$_->{body} =~ s/Created an attachment \(id=([0-9]+)\)/Created <a href="$showattachurlbase$1">an attachment<\/a>/g;
}
my $diffs = $difftext . "\n\n" . $newcomments;
my $diffs = $difftext;
if ($isnew) {
my $head = "";
foreach my $f (@headerlist) {
@ -656,9 +640,7 @@ sub sendMail
# If there isn't anything to show, don't include this header.
next unless $value;
# Only send estimated_time if it is enabled and the user is in the group.
if (($f ne 'estimated_time' && $f ne 'deadline')
|| $user->groups->{Bugzilla->params->{'timetrackinggroup'}})
{
if (($f ne 'estimated_time' && $f ne 'deadline') || $user->is_timetracker) {
my $desc = $fielddescription{$f};
$head .= multiline_sprintf(FORMAT_DOUBLE, ["$desc:", $value],
FORMAT_2_SIZE);
@ -679,8 +661,6 @@ sub sendMail
push @watchingrel, 'None' unless @watchingrel;
push @watchingrel, map { user_id_to_login($_) } @$watchingRef;
my $threadingmarker = build_thread_marker($id, $user->id, $isnew);
my $vars = {
isnew => $isnew,
to => $user->email,
@ -708,9 +688,8 @@ sub sendMail
reportername => Bugzilla::User->new({name => $values{'reporter'}})->name,
diffs => $diffs,
diffarray => $diffArray,
threadingmarker => $threadingmarker,
newcomments => $newcomments,
commentarray => $commentArray,
new_comments => $newcomments,
threadingmarker => build_thread_marker($id, $user->id, $isnew),
};
my $msg;
@ -749,32 +728,15 @@ sub get_comments_by_bug {
my $raw = 1; # Do not format comments which are not of type CMT_NORMAL.
my $comments = Bugzilla::Bug::GetComments($id, "oldest_to_newest", $start, $end, $raw);
foreach my $comment (@$comments) {
$comment->{count} = $count++;
}
if (Bugzilla->params->{'insidergroup'}) {
$anyprivate = 1 if scalar(grep {$_->{'isprivate'} > 0} @$comments);
}
return ($comments, $anyprivate, $count);
}
# Prepare comments for the given language.
sub prepare_comments {
my ($raw_comments, $count) = @_;
my $result = "";
foreach my $comment (@$raw_comments) {
if ($count) {
$comment->{seqnum} = $count;
$result .= "\n\n--- Comment #$count from " . $comment->{'author'}->identity .
" " . format_time($comment->{'time'}) . " ---\n";
}
# Format language specific comments. We don't update $comment->{'body'}
# directly, otherwise it would grow everytime you call format_comment()
# with a different language as some text may be appended to the existing one.
my $body = Bugzilla::Bug::format_comment($comment);
$result .= ($comment->{'already_wrapped'} ? $body : wrap_comment($body));
$count++;
}
return $result;
return ($comments, $anyprivate);
}
1;

View File

@ -121,6 +121,10 @@ sub canonicalise_query {
# Leave this key out if it's in the exclude list
next if lsearch(\@exclude, $key) != -1;
# Remove the Boolean Charts for standard query.cgi fields
# They are listed in the query URL already
next if $key =~ /^(field|type|value)(-\d+){3}$/;
my $esc_key = url_quote($key);
foreach my $value ($self->param($key)) {
@ -137,7 +141,7 @@ sub canonicalise_query {
sub clean_search_url {
my $self = shift;
# Delete any empty URL parameter
# Delete any empty URL parameter.
my @cgi_params = $self->param;
foreach my $param (@cgi_params) {
@ -156,8 +160,50 @@ sub clean_search_url {
# Delete certain parameters if the associated parameter is empty.
$self->delete('bugidtype') if !$self->param('bug_id');
$self->delete('emailtype1') if !$self->param('email1');
$self->delete('emailtype2') if !$self->param('email2');
# Delete leftovers from the login form
$self->delete('Bugzilla_remember', 'GoAheadAndLogIn');
foreach my $num (1,2) {
# If there's no value in the email field, delete the related fields.
if (!$self->param("email$num")) {
foreach my $field qw(type assigned_to reporter qa_contact
cc longdesc)
{
$self->delete("email$field$num");
}
}
}
# chfieldto is set to "Now" by default in query.cgi. But if none
# of the other chfield parameters are set, it's meaningless.
if (!defined $self->param('chfieldfrom') && !$self->param('chfield')
&& !defined $self->param('chfieldvalue'))
{
$self->delete('chfieldto');
}
# cmdtype "doit" is the default from query.cgi, but it's only meaningful
# if there's a remtype parameter.
if (defined $self->param('cmdtype') && $self->param('cmdtype') eq 'doit'
&& !defined $self->param('remtype'))
{
$self->delete('cmdtype');
}
# "Reuse same sort as last time" is actually the default, so we don't
# need it in the URL.
if ($self->param('order')
&& $self->param('order') eq 'Reuse same sort as last time')
{
$self->delete('order');
}
# And now finally, if query_format is our only parameter, that
# really means we have no parameters, so we should delete query_format.
if ($self->param('query_format') && scalar($self->param()) == 1) {
$self->delete('query_format');
}
}
# Overwrite to ensure nph doesn't get set, and unset HEADERS_ONCE
@ -233,17 +279,41 @@ sub header {
return $self->SUPER::header(@_) || "";
}
# CGI.pm is not utf8-aware and passes data as bytes instead of UTF-8 strings.
sub param {
my $self = shift;
if (Bugzilla->params->{'utf8'} && scalar(@_) == 1) {
if (wantarray) {
return map { _fix_utf8($_) } $self->SUPER::param(@_);
# When we are just requesting the value of a parameter...
if (scalar(@_) == 1) {
my @result = $self->SUPER::param(@_);
# Also look at the URL parameters, after we look at the POST
# parameters. This is to allow things like login-form submissions
# with URL parameters in the form's "target" attribute.
if (!scalar(@result)
&& $self->request_method && $self->request_method eq 'POST')
{
@result = $self->SUPER::url_param(@_);
}
else {
return _fix_utf8(scalar $self->SUPER::param(@_));
# Fix UTF-8-ness of input parameters.
if (Bugzilla->params->{'utf8'}) {
@result = map { _fix_utf8($_) } @result;
}
return wantarray ? @result : $result[0];
}
# And for various other functions in CGI.pm, we need to correctly
# return the URL parameters in addition to the POST parameters when
# asked for the list of parameters.
elsif (!scalar(@_) && $self->request_method
&& $self->request_method eq 'POST')
{
my @post_params = $self->SUPER::param;
my @url_params = $self->url_param;
my %params = map { $_ => 1 } (@post_params, @url_params);
return keys %params;
}
return $self->SUPER::param(@_);
}

View File

@ -382,8 +382,7 @@ sub getSeriesIDs {
sub getVisibleSeries {
my %cats;
# List of groups the user is in; use -1 to make sure it's not empty.
my $grouplist = join(", ", (-1, values(%{Bugzilla->user->groups})));
my $grouplist = Bugzilla->user->groups_as_string;
# Get all visible series
my $dbh = Bugzilla->dbh;

View File

@ -13,77 +13,115 @@
# The Original Code is the Bugzilla Bug Tracking System.
#
# Contributor(s): Tiago R. Mello <timello@async.com.br>
#
# Frédéric Buclin <LpSolit@gmail.com>
use strict;
package Bugzilla::Classification;
use Bugzilla::Constants;
use Bugzilla::Util;
use Bugzilla::Error;
use Bugzilla::Product;
use base qw(Bugzilla::Object);
###############################
#### Initialization ####
###############################
use constant DB_TABLE => 'classifications';
use constant LIST_ORDER => 'sortkey, name';
use constant DB_COLUMNS => qw(
classifications.id
classifications.name
classifications.description
classifications.sortkey
id
name
description
sortkey
);
our $columns = join(", ", DB_COLUMNS);
use constant REQUIRED_CREATE_FIELDS => qw(
name
);
use constant UPDATE_COLUMNS => qw(
name
description
sortkey
);
use constant VALIDATORS => {
name => \&_check_name,
description => \&_check_description,
sortkey => \&_check_sortkey,
};
###############################
#### Constructors #####
###############################
sub remove_from_db {
my $self = shift;
my $dbh = Bugzilla->dbh;
ThrowUserError("classification_not_deletable") if ($self->id == 1);
$dbh->bz_start_transaction();
# Reclassify products to the default classification, if needed.
$dbh->do("UPDATE products SET classification_id = 1
WHERE classification_id = ?", undef, $self->id);
$dbh->do("DELETE FROM classifications WHERE id = ?", undef, $self->id);
$dbh->bz_commit_transaction();
}
###############################
#### Validators ####
###############################
sub _check_name {
my ($invocant, $name) = @_;
$name = trim($name);
$name || ThrowUserError('classification_not_specified');
if (length($name) > MAX_CLASSIFICATION_SIZE) {
ThrowUserError('classification_name_too_long', {'name' => $name});
}
my $classification = new Bugzilla::Classification({name => $name});
if ($classification && (!ref $invocant || $classification->id != $invocant->id)) {
ThrowUserError("classification_already_exists", { name => $classification->name });
}
return $name;
}
sub _check_description {
my ($invocant, $description) = @_;
$description = trim($description || '');
return $description;
}
sub _check_sortkey {
my ($invocant, $sortkey) = @_;
$sortkey ||= 0;
my $stored_sortkey = $sortkey;
if (!detaint_natural($sortkey) || $sortkey > MAX_SMALLINT) {
ThrowUserError('classification_invalid_sortkey', { 'sortkey' => $stored_sortkey });
}
return $sortkey;
}
###############################
#### Methods ####
###############################
sub new {
my $invocant = shift;
my $class = ref($invocant) || $invocant;
my $self = {};
bless($self, $class);
return $self->_init(@_);
}
sub _init {
my $self = shift;
my ($param) = @_;
my $dbh = Bugzilla->dbh;
my $id = $param unless (ref $param eq 'HASH');
my $classification;
if (defined $id) {
detaint_natural($id)
|| ThrowCodeError('param_must_be_numeric',
{function => 'Bugzilla::Classification::_init'});
$classification = $dbh->selectrow_hashref(qq{
SELECT $columns FROM classifications
WHERE id = ?}, undef, $id);
} elsif (defined $param->{'name'}) {
trick_taint($param->{'name'});
$classification = $dbh->selectrow_hashref(qq{
SELECT $columns FROM classifications
WHERE name = ?}, undef, $param->{'name'});
} else {
ThrowCodeError('bad_arg',
{argument => 'param',
function => 'Bugzilla::Classification::_init'});
}
return undef unless (defined $classification);
foreach my $field (keys %$classification) {
$self->{$field} = $classification->{$field};
}
return $self;
}
sub set_name { $_[0]->set('name', $_[1]); }
sub set_description { $_[0]->set('description', $_[1]); }
sub set_sortkey { $_[0]->set('sortkey', $_[1]); }
sub product_count {
my $self = shift;
@ -116,46 +154,9 @@ sub products {
#### Accessors ####
###############################
sub id { return $_[0]->{'id'}; }
sub name { return $_[0]->{'name'}; }
sub description { return $_[0]->{'description'}; }
sub sortkey { return $_[0]->{'sortkey'}; }
###############################
#### Subroutines ####
###############################
sub get_all_classifications {
my $dbh = Bugzilla->dbh;
my $ids = $dbh->selectcol_arrayref(q{
SELECT id FROM classifications ORDER BY sortkey, name});
my @classifications;
foreach my $id (@$ids) {
push @classifications, new Bugzilla::Classification($id);
}
return @classifications;
}
sub check_classification {
my ($class_name) = @_;
unless ($class_name) {
ThrowUserError("classification_not_specified");
}
my $classification =
new Bugzilla::Classification({name => $class_name});
unless ($classification) {
ThrowUserError("classification_doesnt_exist",
{ name => $class_name });
}
return $classification;
}
1;
__END__
@ -174,18 +175,18 @@ Bugzilla::Classification - Bugzilla classification class.
my $id = $classification->id;
my $name = $classification->name;
my $description = $classification->description;
my $sortkey = $classification->sortkey;
my $product_count = $classification->product_count;
my $products = $classification->products;
my $hash_ref = Bugzilla::Classification::get_all_classifications();
my $classification = $hash_ref->{1};
my $classification =
Bugzilla::Classification::check_classification('AcmeClass');
=head1 DESCRIPTION
Classification.pm represents a Classification object.
Classification.pm represents a classification object. It is an
implementation of L<Bugzilla::Object>, and thus provides all methods
that L<Bugzilla::Object> provides.
The methods that are specific to C<Bugzilla::Classification> are listed
below.
A Classification is a higher-level grouping of Products.
@ -193,20 +194,6 @@ A Classification is a higher-level grouping of Products.
=over
=item C<new($param)>
Description: The constructor is used to load an existing
classification by passing a classification
id or classification name using a hash.
Params: $param - If you pass an integer, the integer is the
classification_id from the database that we
want to read in. If you pass in a hash with
'name' key, then the value of the name key
is the name of a classification from the DB.
Returns: A Bugzilla::Classification object.
=item C<product_count()>
Description: Returns the total number of products that belong to
@ -226,27 +213,4 @@ A Classification is a higher-level grouping of Products.
=back
=head1 SUBROUTINES
=over
=item C<get_all_classifications()>
Description: Returns all classifications.
Params: none.
Returns: Bugzilla::Classification object list.
=item C<check_classification($classification_name)>
Description: Checks if the classification name passed in is a
valid classification.
Params: $classification_name - String with a classification name.
Returns: Bugzilla::Classification object.
=back
=cut

View File

@ -34,6 +34,7 @@ use strict;
use base qw(Exporter);
use Bugzilla::Constants;
use Bugzilla::Hook;
use Data::Dumper;
use File::Temp;
@ -54,15 +55,21 @@ our %params;
# Load in the param definitions
sub _load_params {
my $panels = param_panels();
my %hook_panels;
foreach my $panel (keys %$panels) {
my $module = $panels->{$panel};
eval("require $module") || die $@;
my @new_param_list = "$module"->get_param_list();
my @new_param_list = $module->get_param_list();
$hook_panels{lc($panel)} = { params => \@new_param_list };
foreach my $item (@new_param_list) {
$params{$item->{'name'}} = $item;
}
push(@param_list, @new_param_list);
}
# This hook is also called in editparams.cgi. This call here is required
# to make SetParam work.
Bugzilla::Hook::process('config-modify_panels',
{ panels => \%hook_panels });
}
# END INIT CODE
@ -77,7 +84,8 @@ sub param_panels {
$param_panels->{$module} = "Bugzilla::Config::$module" unless $module eq 'Common';
}
# Now check for any hooked params
Bugzilla::Hook::process('config', { config => $param_panels });
Bugzilla::Hook::process('config-add_panels',
{ panel_modules => $param_panels });
return $param_panels;
}

View File

@ -49,20 +49,14 @@ sub get_param_list {
{
name => 'allowemailchange',
type => 'b',
default => 0
default => 1
},
{
name => 'allowuserdeletion',
type => 'b',
default => 0
},
{
name => 'supportwatchers',
type => 'b',
default => 0
} );
});
return @param_list;
}

View File

@ -65,13 +65,6 @@ sub get_param_list {
default => 0
},
{
name => 'maxpatchsize',
type => 't',
default => '1000',
checker => \&check_numeric
},
{
name => 'maxattachmentsize',
type => 't',

View File

@ -80,24 +80,12 @@ sub get_param_list {
default => 0
},
{
name => 'commentonclearresolution',
type => 'b',
default => 0
},
{
name => 'commentonchange_resolution',
type => 'b',
default => 0
},
{
name => 'commentonreassignbycomponent',
type => 'b',
default => 0
},
{
name => 'commentonduplicate',
type => 'b',

View File

@ -53,12 +53,6 @@ sub get_param_list {
default => 0
},
{
name => 'showallproducts',
type => 'b',
default => 0
},
{
name => 'usetargetmilestone',
type => 'b',
@ -89,6 +83,12 @@ sub get_param_list {
default => 0
},
{
name => 'use_see_also',
type => 'b',
default => 1
},
{
name => 'defaultpriority',
type => 's',

View File

@ -35,7 +35,6 @@ package Bugzilla::Config::Common;
use strict;
use Socket;
use Time::Zone;
use Bugzilla::Util;
use Bugzilla::Constants;
@ -49,8 +48,8 @@ use base qw(Exporter);
check_sslbase check_priority check_severity check_platform
check_opsys check_shadowdb check_urlbase check_webdotbase
check_netmask check_user_verify_class check_image_converter
check_mail_delivery_method check_notification check_timezone check_utf8
check_bug_status check_smtp_auth
check_mail_delivery_method check_notification check_utf8
check_bug_status check_smtp_auth check_theschwartz_available
check_maxattachmentsize
);
@ -278,10 +277,7 @@ sub check_user_verify_class {
for my $class (split /,\s*/, $list) {
my $res = check_multi($class, $entry);
return $res if $res;
if ($class eq 'DB') {
# No params
}
elsif ($class eq 'RADIUS') {
if ($class eq 'RADIUS') {
eval "require Authen::Radius";
return "Error requiring Authen::Radius: '$@'" if $@;
return "RADIUS servername (RADIUS_server) is missing" unless Bugzilla->params->{"RADIUS_server"};
@ -293,9 +289,6 @@ sub check_user_verify_class {
return "LDAP servername (LDAPserver) is missing" unless Bugzilla->params->{"LDAPserver"};
return "LDAPBaseDN is empty" unless Bugzilla->params->{"LDAPBaseDN"};
}
else {
return "Unknown user_verify_class '$class' in check_user_verify_class";
}
}
return "";
}
@ -352,14 +345,6 @@ sub check_notification {
return "";
}
sub check_timezone {
my $tz = shift;
unless (defined(tz_offset($tz))) {
return "must be empty or a legal timezone name, such as PDT or JST";
}
return "";
}
sub check_smtp_auth {
my $username = shift;
if ($username) {
@ -369,6 +354,15 @@ sub check_smtp_auth {
return "";
}
sub check_theschwartz_available {
if (!eval { require TheSchwartz; require Daemon::Generic; }) {
return "Using the job queue requires that you have certain Perl"
. " modules installed. See the output of checksetup.pl"
. " for more information";
}
return "";
}
# OK, here are the parameter definitions themselves.
#
# Each definition is a hash with keys:

View File

@ -87,13 +87,6 @@ sub get_param_list {
default => '/'
},
{
name => 'timezone',
type => 't',
default => '',
checker => \&check_timezone
},
{
name => 'utf8',
type => 'b',

View File

@ -57,6 +57,13 @@ sub get_param_list {
default => 'bugzilla-daemon'
},
{
name => 'use_mailer_queue',
type => 'b',
default => 0,
checker => \&check_theschwartz_available,
},
{
name => 'sendmailnow',
type => 'b',
@ -90,7 +97,6 @@ sub get_param_list {
default => 7,
checker => \&check_numeric
},
{
name => 'globalwatchers',
type => 't',

View File

@ -77,7 +77,7 @@ sub get_param_list {
{
name => 'specific_search_allow_empty_words',
type => 'b',
default => 0
default => 1
}
);

View File

@ -101,6 +101,7 @@ use File::Basename;
POS_EVENTS
EVT_OTHER EVT_ADDED_REMOVED EVT_COMMENT EVT_ATTACHMENT EVT_ATTACHMENT_DATA
EVT_PROJ_MANAGEMENT EVT_OPENED_CLOSED EVT_KEYWORD EVT_CC EVT_DEPEND_BLOCK
EVT_BUG_CREATED
NEG_EVENTS
EVT_UNCONFIRMED EVT_CHANGED_BY_ME
@ -122,6 +123,8 @@ use File::Basename;
FIELD_TYPE_MULTI_SELECT
FIELD_TYPE_TEXTAREA
FIELD_TYPE_DATETIME
FIELD_TYPE_BUG_ID
FIELD_TYPE_BUG_URLS
USAGE_MODE_BROWSER
USAGE_MODE_CMDLINE
@ -149,9 +152,16 @@ use File::Basename;
MAX_SMALLINT
MAX_LEN_QUERY_NAME
MAX_CLASSIFICATION_SIZE
MAX_PRODUCT_SIZE
MAX_MILESTONE_SIZE
MAX_COMPONENT_SIZE
MAX_FIELD_VALUE_SIZE
MAX_FREETEXT_LENGTH
MAX_BUG_URL_LENGTH
PASSWORD_DIGEST_ALGORITHM
PASSWORD_SALT_LENGTH
);
@Bugzilla::Constants::EXPORT_OK = qw(contenttypes);
@ -159,7 +169,7 @@ use File::Basename;
# CONSTANTS
#
# Bugzilla version
use constant BUGZILLA_VERSION => "3.2.4";
use constant BUGZILLA_VERSION => "3.4";
# These are unique values that are unlikely to match a string or a number,
# to be used in criteria for match() functions and other things. They start
@ -306,11 +316,12 @@ use constant EVT_OPENED_CLOSED => 6;
use constant EVT_KEYWORD => 7;
use constant EVT_CC => 8;
use constant EVT_DEPEND_BLOCK => 9;
use constant EVT_BUG_CREATED => 10;
use constant POS_EVENTS => EVT_OTHER, EVT_ADDED_REMOVED, EVT_COMMENT,
EVT_ATTACHMENT, EVT_ATTACHMENT_DATA,
EVT_PROJ_MANAGEMENT, EVT_OPENED_CLOSED, EVT_KEYWORD,
EVT_CC, EVT_DEPEND_BLOCK;
EVT_CC, EVT_DEPEND_BLOCK, EVT_BUG_CREATED;
use constant EVT_UNCONFIRMED => 50;
use constant EVT_CHANGED_BY_ME => 51;
@ -352,6 +363,8 @@ use constant FIELD_TYPE_SINGLE_SELECT => 2;
use constant FIELD_TYPE_MULTI_SELECT => 3;
use constant FIELD_TYPE_TEXTAREA => 4;
use constant FIELD_TYPE_DATETIME => 5;
use constant FIELD_TYPE_BUG_ID => 6;
use constant FIELD_TYPE_BUG_URLS => 7;
# The maximum number of days a token will remain valid.
use constant MAX_TOKEN_AGE => 3;
@ -421,15 +434,36 @@ use constant MAX_SMALLINT => 32767;
# The longest that a saved search name can be.
use constant MAX_LEN_QUERY_NAME => 64;
# The longest classification name allowed.
use constant MAX_CLASSIFICATION_SIZE => 64;
# The longest product name allowed.
use constant MAX_PRODUCT_SIZE => 64;
# The longest milestone name allowed.
use constant MAX_MILESTONE_SIZE => 20;
# The longest component name allowed.
use constant MAX_COMPONENT_SIZE => 64;
# The maximum length for values of <select> fields.
use constant MAX_FIELD_VALUE_SIZE => 64;
# Maximum length allowed for free text fields.
use constant MAX_FREETEXT_LENGTH => 255;
# The longest a bug URL in a BUG_URLS field can be.
use constant MAX_BUG_URL_LENGTH => 255;
# This is the name of the algorithm used to hash passwords before storing
# them in the database. This can be any string that is valid to pass to
# Perl's "Digest" module. Note that if you change this, it won't take
# effect until a user changes his password.
use constant PASSWORD_DIGEST_ALGORITHM => 'SHA-256';
# How long of a salt should we use? Note that if you change this, none
# of your users will be able to log in until they reset their passwords.
use constant PASSWORD_SALT_LENGTH => 8;
sub bz_locations {
# We know that Bugzilla/Constants.pm must be in %INC at this point.
# So the only question is, what's the name of the directory

View File

@ -52,7 +52,6 @@ use Storable qw(dclone);
use constant BLOB_TYPE => DBI::SQL_BLOB;
use constant ISOLATION_LEVEL => 'REPEATABLE READ';
use constant GROUPBY_REGEXP => '(?:.*\s+AS\s+)?(\w+(\.\w+)?)(?:\s+(ASC|DESC))?$';
# Set default values for what used to be the enum types. These values
# are no longer stored in localconfig. If we are upgrading from a
@ -274,7 +273,7 @@ EOT
# List of abstract methods we are checking the derived class implements
our @_abstract_methods = qw(REQUIRED_VERSION PROGRAM_NAME DBD_VERSION
new sql_regexp sql_not_regexp sql_limit sql_to_days
sql_date_format sql_interval);
sql_date_format sql_interval bz_explain);
# This overridden import method will check implementation of inherited classes
# for missing implementation of abstract methods
@ -342,6 +341,12 @@ sub sql_string_concat {
return '(' . join(' || ', @params) . ')';
}
sub sql_string_until {
my ($self, $string, $substring) = @_;
return "SUBSTRING($string FROM 1 FOR " .
$self->sql_position($substring, $string) . " - 1)";
}
sub sql_in {
my ($self, $column_name, $in_list_ref) = @_;
return " $column_name IN (" . join(',', @$in_list_ref) . ") ";
@ -391,6 +396,15 @@ sub bz_last_key {
$table, $column);
}
sub bz_check_regexp {
my ($self, $pattern) = @_;
eval { $self->do("SELECT " . $self->sql_regexp($self->quote("a"), $pattern, 1)) };
$@ && ThrowUserError('illegal_regexp',
{ value => $pattern, dberror => $self->errstr });
}
#####################################################################
# Database Setup
#####################################################################
@ -493,8 +507,7 @@ sub bz_add_fk {
my $col_def = $self->bz_column_info($table, $column);
if (!$col_def->{REFERENCES}) {
$self->_check_references($table, $column, $def->{TABLE},
$def->{COLUMN});
$self->_check_references($table, $column, $def);
print get_text('install_fk_add',
{ table => $table, column => $column, fk => $def })
. "\n" if Bugzilla->usage_mode == USAGE_MODE_CMDLINE;
@ -672,12 +685,18 @@ sub bz_add_field_tables {
my ($self, $field) = @_;
$self->_bz_add_field_table($field->name,
$self->_bz_schema->FIELD_TABLE_SCHEMA);
if ( $field->type == FIELD_TYPE_MULTI_SELECT ) {
$self->_bz_add_field_table('bug_' . $field->name,
$self->_bz_schema->FIELD_TABLE_SCHEMA, $field->type);
if ($field->type == FIELD_TYPE_MULTI_SELECT) {
my $ms_table = "bug_" . $field->name;
$self->_bz_add_field_table($ms_table,
$self->_bz_schema->MULTI_SELECT_VALUE_TABLE);
}
$self->bz_add_fk($ms_table, 'bug_id', {TABLE => 'bugs',
COLUMN => 'bug_id',
DELETE => 'CASCADE'});
$self->bz_add_fk($ms_table, 'value', {TABLE => $field->name,
COLUMN => 'value'});
}
}
sub bz_drop_field_tables {
@ -1191,33 +1210,57 @@ sub _bz_populate_enum_table {
# This is used before adding a foreign key to a column, to make sure
# that the database won't fail adding the key.
sub _check_references {
my ($self, $table, $column, $foreign_table, $foreign_column) = @_;
my ($self, $table, $column, $fk) = @_;
my $foreign_table = $fk->{TABLE};
my $foreign_column = $fk->{COLUMN};
# We use table aliases because sometimes we join a table to itself,
# and we can't use the same table name on both sides of the join.
# We also can't use the words "table" or "foreign" because those are
# reserved words.
my $bad_values = $self->selectcol_arrayref(
"SELECT DISTINCT $table.$column
FROM $table LEFT JOIN $foreign_table
ON $table.$column = $foreign_table.$foreign_column
WHERE $foreign_table.$foreign_column IS NULL
AND $table.$column IS NOT NULL");
"SELECT DISTINCT tabl.$column
FROM $table AS tabl LEFT JOIN $foreign_table AS forn
ON tabl.$column = forn.$foreign_column
WHERE forn.$foreign_column IS NULL
AND tabl.$column IS NOT NULL");
if (@$bad_values) {
my $values = join(', ', @$bad_values);
print <<EOT;
ERROR: There are invalid values for the $column column in the $table
table. (These values do not exist in the $foreign_table table, in the
$foreign_column column.)
Before continuing with checksetup, you will need to fix these values,
either by deleting these rows from the database, or changing the values
of $column in $table to point to valid values in $foreign_table.$foreign_column.
The bad values from the $table.$column column are:
$values
EOT
# I just picked a number above 2, to be considered "abnormal exit."
exit 3;
my $delete_action = $fk->{DELETE} || '';
if ($delete_action eq 'CASCADE') {
$self->do("DELETE FROM $table WHERE $column IN ("
. join(',', ('?') x @$bad_values) . ")",
undef, @$bad_values);
if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
print "\n", get_text('install_fk_invalid_fixed',
{ table => $table, column => $column,
foreign_table => $foreign_table,
foreign_column => $foreign_column,
'values' => $bad_values, action => 'delete' }), "\n";
}
}
elsif ($delete_action eq 'SET NULL') {
$self->do("UPDATE $table SET $column = NULL
WHERE $column IN ("
. join(',', ('?') x @$bad_values) . ")",
undef, @$bad_values);
if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
print "\n", get_text('install_fk_invalid_fixed',
{ table => $table, column => $column,
foreign_table => $foreign_table,
foreign_column => $foreign_column,
'values' => $bad_values, action => 'null' }), "\n";
}
}
else {
print "\n", get_text('install_fk_invalid',
{ table => $table, column => $column,
foreign_table => $foreign_table,
foreign_column => $foreign_column,
'values' => $bad_values }), "\n";
# I just picked a number above 2, to be considered "abnormal exit"
exit 3
}
}
}
@ -1518,6 +1561,11 @@ Abstract method, should be overridden by database specific code.
=item C<$pattern> - the regular expression to search for (scalar)
=item C<$nocheck> - true if the pattern should not be tested; false otherwise (boolean)
=item C<$real_pattern> - the real regular expression to search for.
This argument is used when C<$pattern> is a placeholder ('?').
=back
=item B<Returns>
@ -1540,13 +1588,7 @@ Abstract method, should be overridden by database specific code.
=item B<Params>
=over
=item C<$expr> - SQL expression for the text to be searched (scalar)
=item C<$pattern> - the regular expression to search for (scalar)
=back
Same as L</sql_regexp>.
=item B<Returns>
@ -1774,6 +1816,25 @@ Formatted SQL for concatenating specified strings
=back
=item C<sql_string_until>
=over
=item B<Description>
Returns SQL for truncating a string at the first occurrence of a certain
substring.
=item B<Params>
Note that both parameters need to be sql-quoted.
=item C<$string> The string we're truncating
=item C<$substring> The substring we're truncating at.
=back
=item C<sql_fulltext_search>
=over

View File

@ -50,6 +50,7 @@ use Bugzilla::Error;
use Bugzilla::DB::Schema::Mysql;
use List::Util qw(max);
use Text::ParseWords;
# This is how many comments of MAX_COMMENT_LENGTH we expect on a single bug.
# In reality, you could have a LOT more comments than this, because
@ -63,7 +64,7 @@ sub new {
my ($class, $user, $pass, $host, $dbname, $port, $sock) = @_;
# construct the DSN from the parameters we got
my $dsn = "DBI:mysql:host=$host;database=$dbname";
my $dsn = "dbi:mysql:host=$host;database=$dbname";
$dsn .= ";port=$port" if $port;
$dsn .= ";mysql_socket=$sock" if $sock;
@ -79,6 +80,9 @@ sub new {
# a prefix 'private_'. See DBI documentation.
$self->{private_bz_tables_locked} = "";
# Needed by TheSchwartz
$self->{private_bz_dsn} = $dsn;
bless ($self, $class);
# Bug 321645 - disable MySQL strict mode, if set
@ -125,13 +129,19 @@ sub sql_group_concat {
}
sub sql_regexp {
my ($self, $expr, $pattern) = @_;
my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_;
$real_pattern ||= $pattern;
$self->bz_check_regexp($real_pattern) if !$nocheck;
return "$expr REGEXP $pattern";
}
sub sql_not_regexp {
my ($self, $expr, $pattern) = @_;
my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_;
$real_pattern ||= $pattern;
$self->bz_check_regexp($real_pattern) if !$nocheck;
return "$expr NOT REGEXP $pattern";
}
@ -156,8 +166,21 @@ sub sql_fulltext_search {
my ($self, $column, $text) = @_;
# Add the boolean mode modifier if the search string contains
# boolean operators.
my $mode = ($text =~ /[+\-<>()~*\"]/ ? "IN BOOLEAN MODE" : "");
# boolean operators at the start or end of a word.
my $mode = '';
if ($text =~ /(?:^|\W)[+\-<>~"()]/ || $text =~ /[()"*](?:$|\W)/) {
$mode = 'IN BOOLEAN MODE';
# quote un-quoted compound words
my @words = quotewords('[\s()]+', 'delimiters', $text);
foreach my $word (@words) {
# match words that have non-word chars in the middle of them
if ($word =~ /\w\W+\w/ && $word !~ m/"/) {
$word = '"' . $word . '"';
}
}
$text = join('', @words);
}
# quote the text for use in the MATCH AGAINST expression
$text = $self->quote($text);
@ -221,6 +244,30 @@ sub sql_group_by {
return "GROUP BY $needed_columns";
}
sub bz_explain {
my ($self, $sql) = @_;
my $sth = $self->prepare("EXPLAIN $sql");
$sth->execute();
my $columns = $sth->{'NAME'};
my $lengths = $sth->{'mysql_max_length'};
my $format_string = '|';
my $i = 0;
foreach my $column (@$columns) {
# Sometimes the column name is longer than the contents.
my $length = max($lengths->[$i], length($column));
$format_string .= ' %-' . $length . 's |';
$i++;
}
my $first_row = sprintf($format_string, @$columns);
my @explain_rows = ($first_row, '-' x length($first_row));
while (my $row = $sth->fetchrow_arrayref) {
my @fixed = map { defined $_ ? $_ : 'NULL' } @$row;
push(@explain_rows, sprintf($format_string, @fixed));
}
return join("\n", @explain_rows);
}
sub _bz_get_initial_schema {
my ($self) = @_;
@ -746,6 +793,46 @@ EOT
if (Bugzilla->params->{'utf8'} && !$self->bz_db_is_utf8) {
$self->_alter_db_charset_to_utf8();
}
$self->_fix_defaults();
}
# When you import a MySQL 3/4 mysqldump into MySQL 5, columns that
# aren't supposed to have defaults will have defaults. This is only
# a minor issue, but it makes our tests fail, and it's good to keep
# the DB actually consistent with what DB::Schema thinks the database
# looks like. So we remove defaults from columns that aren't supposed
# to have them
sub _fix_defaults {
my $self = shift;
my $maj_version = substr($self->bz_server_version, 0, 1);
return if $maj_version < 5;
# The oldest column that could have this problem is bugs.assigned_to,
# so if it doesn't have the problem, we just skip doing this entirely.
my $assi_def = $self->_bz_raw_column_info('bugs', 'assigned_to');
my $assi_default = $assi_def->{COLUMN_DEF};
# This "ne ''" thing is necessary because _raw_column_info seems to
# return COLUMN_DEF as an empty string for columns that don't have
# a default.
return unless (defined $assi_default && $assi_default ne '');
foreach my $table ($self->_bz_real_schema->get_table_list()) {
foreach my $column ($self->bz_table_columns($table)) {
my $abs_def = $self->bz_column_info($table, $column);
if (!defined $abs_def->{DEFAULT}) {
# Get the exact default from the database without any
# "fixing" by bz_column_info_real.
my $raw_info = $self->_bz_raw_column_info($table, $column);
my $raw_default = $raw_info->{COLUMN_DEF};
if (defined $raw_default) {
$self->bz_alter_column_raw($table, $column, $abs_def);
$raw_default = "''" if $raw_default eq '';
print "Removed incorrect DB default: $raw_default\n";
}
}
} # foreach $column
} # foreach $table
}
# There is a bug in MySQL 4.1.0 - 4.1.15 that makes certain SELECT
@ -837,6 +924,12 @@ backwards-compatibility anyway, for versions of Bugzilla before 2.20.
sub bz_column_info_real {
my ($self, $table, $column) = @_;
my $col_data = $self->_bz_raw_column_info($table, $column);
return $self->_bz_schema->column_info_to_column($col_data);
}
sub _bz_raw_column_info {
my ($self, $table, $column) = @_;
# DBD::mysql does not support selecting a specific column,
# so we have to get all the columns on the table and find
@ -852,7 +945,7 @@ sub bz_column_info_real {
if (!defined $col_data) {
return undef;
}
return $self->_bz_schema->column_info_to_column($col_data);
return $col_data;
}
=item C<bz_index_info_real($table, $index)>

View File

@ -52,7 +52,6 @@ use base qw(Bugzilla::DB);
use constant EMPTY_STRING => '__BZ_EMPTY_STR__';
use constant ISOLATION_LEVEL => 'READ COMMITTED';
use constant BLOB_TYPE => { ora_type => ORA_BLOB };
use constant GROUPBY_REGEXP => '((CASE\s+WHEN.+END)|(TO_CHAR\(.+\))|(\(SCORE.+\))|(\(MATCH.+\))|(\w+(\.\w+)?))(\s+AS\s+)?(.*)?$';
sub new {
my ($class, $user, $pass, $host, $dbname, $port) = @_;
@ -65,13 +64,15 @@ sub new {
$ENV{'NLS_LANG'} = '.AL32UTF8' if Bugzilla->params->{'utf8'};
# construct the DSN from the parameters we got
my $dsn = "DBI:Oracle:host=$host;sid=$dbname";
my $dsn = "dbi:Oracle:host=$host;sid=$dbname";
$dsn .= ";port=$port" if $port;
my $attrs = { FetchHashKeyName => 'NAME_lc',
LongReadLen => ( Bugzilla->params->{'maxattachmentsize'}
|| 1000 ) * 1024,
};
my $self = $class->db_new($dsn, $user, $pass, $attrs);
# Needed by TheSchwartz
$self->{private_bz_dsn} = $dsn;
bless ($self, $class);
@ -95,14 +96,39 @@ sub bz_last_key {
return $last_insert_id;
}
sub bz_check_regexp {
my ($self, $pattern) = @_;
eval { $self->do("SELECT 1 FROM DUAL WHERE "
. $self->sql_regexp($self->quote("a"), $pattern, 1)) };
$@ && ThrowUserError('illegal_regexp',
{ value => $pattern, dberror => $self->errstr });
}
sub bz_explain {
my ($self, $sql) = @_;
my $sth = $self->prepare("EXPLAIN PLAN FOR $sql");
$sth->execute();
my $explain = $self->selectcol_arrayref(
"SELECT PLAN_TABLE_OUTPUT FROM TABLE(DBMS_XPLAN.DISPLAY)");
return join("\n", @$explain);
}
sub sql_regexp {
my ($self, $expr, $pattern) = @_;
my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_;
$real_pattern ||= $pattern;
$self->bz_check_regexp($real_pattern) if !$nocheck;
return "REGEXP_LIKE($expr, $pattern)";
}
sub sql_not_regexp {
my ($self, $expr, $pattern) = @_;
my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_;
$real_pattern ||= $pattern;
$self->bz_check_regexp($real_pattern) if !$nocheck;
return "NOT REGEXP_LIKE($expr, $pattern)"
}
@ -122,6 +148,13 @@ sub sql_string_concat {
return 'CONCAT(' . join(', ', @params) . ')';
}
sub sql_string_until {
my ($self, $string, $substring) = @_;
return "SUBSTR($string, 1, "
. $self->sql_position($substring, $string)
. " - 1)";
}
sub sql_to_days {
my ($self, $date) = @_;
@ -185,6 +218,15 @@ sub sql_in {
return "( " . join(" OR ", @in_str) . " )";
}
sub _bz_add_field_table {
my ($self, $name, $schema_ref, $type) = @_;
$self->SUPER::_bz_add_field_table($name, $schema_ref);
if (defined($type) && $type == FIELD_TYPE_MULTI_SELECT) {
my $uk_name = "UK_" . $self->_bz_schema->_hash_identifier($name . '_value');
$self->do("ALTER TABLE $name ADD CONSTRAINT $uk_name UNIQUE(value)");
}
}
sub bz_drop_table {
my ($self, $name) = @_;
my $table_exists = $self->bz_table_info($name);
@ -522,7 +564,7 @@ sub bz_setup_database {
}
my $tr_str = "CREATE OR REPLACE TRIGGER $trigger_name"
. " AFTER UPDATE ON ". $to_table
. " AFTER UPDATE OF $to_column ON $to_table "
. " REFERENCING "
. " NEW AS NEW "
. " OLD AS OLD "

View File

@ -60,7 +60,7 @@ sub new {
$dbname ||= 'template1';
# construct the DSN from the parameters we got
my $dsn = "DBI:Pg:dbname=$dbname";
my $dsn = "dbi:Pg:dbname=$dbname";
$dsn .= ";host=$host" if $host;
$dsn .= ";port=$port" if $port;
@ -75,6 +75,8 @@ sub new {
# all class local variables stored in DBI derived class needs to have
# a prefix 'private_'. See DBI documentation.
$self->{private_bz_tables_locked} = "";
# Needed by TheSchwartz
$self->{private_bz_dsn} = $dsn;
bless ($self, $class);
@ -93,13 +95,19 @@ sub bz_last_key {
}
sub sql_regexp {
my ($self, $expr, $pattern) = @_;
my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_;
$real_pattern ||= $pattern;
$self->bz_check_regexp($real_pattern) if !$nocheck;
return "$expr ~* $pattern";
}
sub sql_not_regexp {
my ($self, $expr, $pattern) = @_;
my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_;
$real_pattern ||= $pattern;
$self->bz_check_regexp($real_pattern) if !$nocheck;
return "$expr !~* $pattern"
}
@ -167,6 +175,12 @@ sub bz_sequence_exists {
return $exists || 0;
}
sub bz_explain {
my ($self, $sql) = @_;
my $explain = $self->selectcol_arrayref("EXPLAIN ANALYZE $sql");
return join("\n", @$explain);
}
#####################################################################
# Custom Database Setup
#####################################################################

View File

@ -210,6 +210,26 @@ use constant SCHEMA_VERSION => '2.00';
use constant ADD_COLUMN => 'ADD COLUMN';
# This is a reasonable default that's true for both PostgreSQL and MySQL.
use constant MAX_IDENTIFIER_LEN => 63;
use constant FIELD_TABLE_SCHEMA => {
FIELDS => [
id => {TYPE => 'SMALLSERIAL', NOTNULL => 1,
PRIMARYKEY => 1},
value => {TYPE => 'varchar(64)', NOTNULL => 1},
sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0},
isactive => {TYPE => 'BOOLEAN', NOTNULL => 1,
DEFAULT => 'TRUE'},
visibility_value_id => {TYPE => 'INT2'},
],
# Note that bz_add_field_table should prepend the table name
# to these index names.
INDEXES => [
value_idx => {FIELDS => ['value'], TYPE => 'UNIQUE'},
sortkey_idx => ['sortkey', 'value'],
visibility_value_id_idx => ['visibility_value_id'],
],
};
use constant ABSTRACT_SCHEMA => {
# BUG-RELATED TABLES
@ -307,13 +327,21 @@ use constant ABSTRACT_SCHEMA => {
bugs_activity => {
FIELDS => [
bug_id => {TYPE => 'INT3', NOTNULL => 1},
attach_id => {TYPE => 'INT3'},
bug_id => {TYPE => 'INT3', NOTNULL => 1,
REFERENCES => {TABLE => 'bugs',
COLUMN => 'bug_id',
DELETE => 'CASCADE'}},
attach_id => {TYPE => 'INT3',
REFERENCES => {TABLE => 'attachments',
COLUMN => 'attach_id',
DELETE => 'CASCADE'}},
who => {TYPE => 'INT3', NOTNULL => 1,
REFERENCES => {TABLE => 'profiles',
COLUMN => 'userid'}},
bug_when => {TYPE => 'DATETIME', NOTNULL => 1},
fieldid => {TYPE => 'INT3', NOTNULL => 1},
fieldid => {TYPE => 'INT3', NOTNULL => 1,
REFERENCES => {TABLE => 'fielddefs',
COLUMN => 'id'}},
added => {TYPE => 'TINYTEXT'},
removed => {TYPE => 'TINYTEXT'},
],
@ -327,7 +355,10 @@ use constant ABSTRACT_SCHEMA => {
cc => {
FIELDS => [
bug_id => {TYPE => 'INT3', NOTNULL => 1},
bug_id => {TYPE => 'INT3', NOTNULL => 1,
REFERENCES => {TABLE => 'bugs',
COLUMN => 'bug_id',
DELETE => 'CASCADE'}},
who => {TYPE => 'INT3', NOTNULL => 1,
REFERENCES => {TABLE => 'profiles',
COLUMN => 'userid',
@ -367,8 +398,14 @@ use constant ABSTRACT_SCHEMA => {
dependencies => {
FIELDS => [
blocked => {TYPE => 'INT3', NOTNULL => 1},
dependson => {TYPE => 'INT3', NOTNULL => 1},
blocked => {TYPE => 'INT3', NOTNULL => 1,
REFERENCES => {TABLE => 'bugs',
COLUMN => 'bug_id',
DELETE => 'CASCADE'}},
dependson => {TYPE => 'INT3', NOTNULL => 1,
REFERENCES => {TABLE => 'bugs',
COLUMN => 'bug_id',
DELETE => 'CASCADE'}},
],
INDEXES => [
dependencies_blocked_idx => ['blocked'],
@ -382,7 +419,10 @@ use constant ABSTRACT_SCHEMA => {
REFERENCES => {TABLE => 'profiles',
COLUMN => 'userid',
DELETE => 'CASCADE'}},
bug_id => {TYPE => 'INT3', NOTNULL => 1},
bug_id => {TYPE => 'INT3', NOTNULL => 1,
REFERENCES => {TABLE => 'bugs',
COLUMN => 'bug_id',
DELETE => 'CASCADE'}},
vote_count => {TYPE => 'INT2', NOTNULL => 1},
],
INDEXES => [
@ -395,7 +435,10 @@ use constant ABSTRACT_SCHEMA => {
FIELDS => [
attach_id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
PRIMARYKEY => 1},
bug_id => {TYPE => 'INT3', NOTNULL => 1},
bug_id => {TYPE => 'INT3', NOTNULL => 1,
REFERENCES => {TABLE => 'bugs',
COLUMN => 'bug_id',
DELETE => 'CASCADE'}},
creation_ts => {TYPE => 'DATETIME', NOTNULL => 1},
modification_time => {TYPE => 'DATETIME', NOTNULL => 1},
description => {TYPE => 'TINYTEXT', NOTNULL => 1},
@ -422,16 +465,36 @@ use constant ABSTRACT_SCHEMA => {
attach_data => {
FIELDS => [
id => {TYPE => 'INT3', NOTNULL => 1,
PRIMARYKEY => 1},
PRIMARYKEY => 1,
REFERENCES => {TABLE => 'attachments',
COLUMN => 'attach_id',
DELETE => 'CASCADE'}},
thedata => {TYPE => 'LONGBLOB', NOTNULL => 1},
],
},
duplicates => {
FIELDS => [
dupe_of => {TYPE => 'INT3', NOTNULL => 1},
dupe_of => {TYPE => 'INT3', NOTNULL => 1,
REFERENCES => {TABLE => 'bugs',
COLUMN => 'bug_id',
DELETE => 'CASCADE'}},
dupe => {TYPE => 'INT3', NOTNULL => 1,
PRIMARYKEY => 1},
PRIMARYKEY => 1,
REFERENCES => {TABLE => 'bugs',
COLUMN => 'bug_id',
DELETE => 'CASCADE'}},
],
},
bug_see_also => {
FIELDS => [
bug_id => {TYPE => 'INT3', NOTNULL => 1},
value => {TYPE => 'varchar(255)', NOTNULL => 1},
],
INDEXES => [
bug_see_also_bug_id_idx => {FIELDS => [qw(bug_id value)],
TYPE => 'UNIQUE'},
],
},
@ -453,8 +516,15 @@ use constant ABSTRACT_SCHEMA => {
keywords => {
FIELDS => [
bug_id => {TYPE => 'INT3', NOTNULL => 1},
keywordid => {TYPE => 'INT2', NOTNULL => 1},
bug_id => {TYPE => 'INT3', NOTNULL => 1,
REFERENCES => {TABLE => 'bugs',
COLUMN => 'bug_id',
DELETE => 'CASCADE'}},
keywordid => {TYPE => 'INT2', NOTNULL => 1,
REFERENCES => {TABLE => 'keyworddefs',
COLUMN => 'id',
DELETE => 'CASCADE'}},
],
INDEXES => [
keywords_bug_id_idx => {FIELDS => [qw(bug_id keywordid)],
@ -471,14 +541,27 @@ use constant ABSTRACT_SCHEMA => {
FIELDS => [
id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
PRIMARYKEY => 1},
type_id => {TYPE => 'INT2', NOTNULL => 1},
type_id => {TYPE => 'INT2', NOTNULL => 1,
REFERENCES => {TABLE => 'flagtypes',
COLUMN => 'id',
DELETE => 'CASCADE'}},
status => {TYPE => 'char(1)', NOTNULL => 1},
bug_id => {TYPE => 'INT3', NOTNULL => 1},
attach_id => {TYPE => 'INT3'},
bug_id => {TYPE => 'INT3', NOTNULL => 1,
REFERENCES => {TABLE => 'bugs',
COLUMN => 'bug_id',
DELETE => 'CASCADE'}},
attach_id => {TYPE => 'INT3',
REFERENCES => {TABLE => 'attachments',
COLUMN => 'attach_id',
DELETE => 'CASCADE'}},
creation_date => {TYPE => 'DATETIME', NOTNULL => 1},
modification_date => {TYPE => 'DATETIME'},
setter_id => {TYPE => 'INT3'},
requestee_id => {TYPE => 'INT3'},
setter_id => {TYPE => 'INT3',
REFERENCES => {TABLE => 'profiles',
COLUMN => 'userid'}},
requestee_id => {TYPE => 'INT3',
REFERENCES => {TABLE => 'profiles',
COLUMN => 'userid'}},
],
INDEXES => [
flags_bug_id_idx => [qw(bug_id attach_id)],
@ -508,8 +591,12 @@ use constant ABSTRACT_SCHEMA => {
DEFAULT => 'FALSE'},
sortkey => {TYPE => 'INT2', NOTNULL => 1,
DEFAULT => '0'},
grant_group_id => {TYPE => 'INT3'},
request_group_id => {TYPE => 'INT3'},
grant_group_id => {TYPE => 'INT3',
REFERENCES => {TABLE => 'groups',
COLUMN => 'id'}},
request_group_id => {TYPE => 'INT3',
REFERENCES => {TABLE => 'groups',
COLUMN => 'id'}},
],
},
@ -518,9 +605,18 @@ use constant ABSTRACT_SCHEMA => {
# to be set for them.
flaginclusions => {
FIELDS => [
type_id => {TYPE => 'INT2', NOTNULL => 1},
product_id => {TYPE => 'INT2'},
component_id => {TYPE => 'INT2'},
type_id => {TYPE => 'INT2', NOTNULL => 1,
REFERENCES => {TABLE => 'flagtypes',
COLUMN => 'id',
DELETE => 'CASCADE'}},
product_id => {TYPE => 'INT2',
REFERENCES => {TABLE => 'products',
COLUMN => 'id',
DELETE => 'CASCADE'}},
component_id => {TYPE => 'INT2',
REFERENCES => {TABLE => 'components',
COLUMN => 'id',
DELETE => 'CASCADE'}},
],
INDEXES => [
flaginclusions_type_id_idx =>
@ -530,9 +626,18 @@ use constant ABSTRACT_SCHEMA => {
flagexclusions => {
FIELDS => [
type_id => {TYPE => 'INT2', NOTNULL => 1},
product_id => {TYPE => 'INT2'},
component_id => {TYPE => 'INT2'},
type_id => {TYPE => 'INT2', NOTNULL => 1,
REFERENCES => {TABLE => 'flagtypes',
COLUMN => 'id',
DELETE => 'CASCADE'}},
product_id => {TYPE => 'INT2',
REFERENCES => {TABLE => 'products',
COLUMN => 'id',
DELETE => 'CASCADE'}},
component_id => {TYPE => 'INT2',
REFERENCES => {TABLE => 'components',
COLUMN => 'id',
DELETE => 'CASCADE'}},
],
INDEXES => [
flagexclusions_type_id_idx =>
@ -562,11 +667,21 @@ use constant ABSTRACT_SCHEMA => {
DEFAULT => 'FALSE'},
enter_bug => {TYPE => 'BOOLEAN', NOTNULL => 1,
DEFAULT => 'FALSE'},
buglist => {TYPE => 'BOOLEAN', NOTNULL => 1,
DEFAULT => 'FALSE'},
visibility_field_id => {TYPE => 'INT3',
REFERENCES => {TABLE => 'fielddefs',
COLUMN => 'id'}},
visibility_value_id => {TYPE => 'INT2'},
value_field_id => {TYPE => 'INT3',
REFERENCES => {TABLE => 'fielddefs',
COLUMN => 'id'}},
],
INDEXES => [
fielddefs_name_idx => {FIELDS => ['name'],
TYPE => 'UNIQUE'},
fielddefs_sortkey_idx => ['sortkey'],
fielddefs_value_field_id_idx => ['value_field_id'],
],
},
@ -578,7 +693,10 @@ use constant ABSTRACT_SCHEMA => {
id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
PRIMARYKEY => 1},
value => {TYPE => 'varchar(64)', NOTNULL => 1},
product_id => {TYPE => 'INT2', NOTNULL => 1},
product_id => {TYPE => 'INT2', NOTNULL => 1,
REFERENCES => {TABLE => 'products',
COLUMN => 'id',
DELETE => 'CASCADE'}},
],
INDEXES => [
versions_product_id_idx => {FIELDS => [qw(product_id value)],
@ -590,7 +708,10 @@ use constant ABSTRACT_SCHEMA => {
FIELDS => [
id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
PRIMARYKEY => 1},
product_id => {TYPE => 'INT2', NOTNULL => 1},
product_id => {TYPE => 'INT2', NOTNULL => 1,
REFERENCES => {TABLE => 'products',
COLUMN => 'id',
DELETE => 'CASCADE'}},
value => {TYPE => 'varchar(20)', NOTNULL => 1},
sortkey => {TYPE => 'INT2', NOTNULL => 1,
DEFAULT => 0},
@ -606,98 +727,65 @@ use constant ABSTRACT_SCHEMA => {
bug_status => {
FIELDS => [
id => {TYPE => 'SMALLSERIAL', NOTNULL => 1,
PRIMARYKEY => 1},
value => {TYPE => 'varchar(64)', NOTNULL => 1},
sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0},
isactive => {TYPE => 'BOOLEAN', NOTNULL => 1,
DEFAULT => 'TRUE'},
@{ dclone(FIELD_TABLE_SCHEMA->{FIELDS}) },
is_open => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'},
],
INDEXES => [
bug_status_value_idx => {FIELDS => ['value'],
TYPE => 'UNIQUE'},
bug_status_sortkey_idx => ['sortkey', 'value'],
bug_status_visibility_value_id_idx => ['visibility_value_id'],
],
},
resolution => {
FIELDS => [
id => {TYPE => 'SMALLSERIAL', NOTNULL => 1,
PRIMARYKEY => 1},
value => {TYPE => 'varchar(64)', NOTNULL => 1},
sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0},
isactive => {TYPE => 'BOOLEAN', NOTNULL => 1,
DEFAULT => 'TRUE'},
],
FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}),
INDEXES => [
resolution_value_idx => {FIELDS => ['value'],
TYPE => 'UNIQUE'},
resolution_sortkey_idx => ['sortkey', 'value'],
resolution_visibility_value_id_idx => ['visibility_value_id'],
],
},
bug_severity => {
FIELDS => [
id => {TYPE => 'SMALLSERIAL', NOTNULL => 1,
PRIMARYKEY => 1},
value => {TYPE => 'varchar(64)', NOTNULL => 1},
sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0},
isactive => {TYPE => 'BOOLEAN', NOTNULL => 1,
DEFAULT => 'TRUE'},
],
FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}),
INDEXES => [
bug_severity_value_idx => {FIELDS => ['value'],
TYPE => 'UNIQUE'},
bug_severity_sortkey_idx => ['sortkey', 'value'],
bug_severity_visibility_value_id_idx => ['visibility_value_id'],
],
},
priority => {
FIELDS => [
id => {TYPE => 'SMALLSERIAL', NOTNULL => 1,
PRIMARYKEY => 1},
value => {TYPE => 'varchar(64)', NOTNULL => 1},
sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0},
isactive => {TYPE => 'BOOLEAN', NOTNULL => 1,
DEFAULT => 'TRUE'},
],
FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}),
INDEXES => [
priority_value_idx => {FIELDS => ['value'],
TYPE => 'UNIQUE'},
priority_sortkey_idx => ['sortkey', 'value'],
priority_visibility_value_id_idx => ['visibility_value_id'],
],
},
rep_platform => {
FIELDS => [
id => {TYPE => 'SMALLSERIAL', NOTNULL => 1,
PRIMARYKEY => 1},
value => {TYPE => 'varchar(64)', NOTNULL => 1},
sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0},
isactive => {TYPE => 'BOOLEAN', NOTNULL => 1,
DEFAULT => 'TRUE'},
],
FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}),
INDEXES => [
rep_platform_value_idx => {FIELDS => ['value'],
TYPE => 'UNIQUE'},
rep_platform_sortkey_idx => ['sortkey', 'value'],
rep_platform_visibility_value_id_idx => ['visibility_value_id'],
],
},
op_sys => {
FIELDS => [
id => {TYPE => 'SMALLSERIAL', NOTNULL => 1,
PRIMARYKEY => 1},
value => {TYPE => 'varchar(64)', NOTNULL => 1},
sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0},
isactive => {TYPE => 'BOOLEAN', NOTNULL => 1,
DEFAULT => 'TRUE'},
],
FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}),
INDEXES => [
op_sys_value_idx => {FIELDS => ['value'],
TYPE => 'UNIQUE'},
op_sys_sortkey_idx => ['sortkey', 'value'],
op_sys_visibility_value_id_idx => ['visibility_value_id'],
],
},
@ -739,6 +827,8 @@ use constant ABSTRACT_SCHEMA => {
INDEXES => [
profiles_login_name_idx => {FIELDS => ['login_name'],
TYPE => 'UNIQUE'},
profiles_extern_id_idx => {FIELDS => ['extern_id'],
TYPE => 'UNIQUE'}
],
},
@ -842,7 +932,10 @@ use constant ABSTRACT_SCHEMA => {
REFERENCES => {TABLE => 'profiles',
COLUMN => 'userid',
DELETE => 'CASCADE'}},
component_id => {TYPE => 'INT2', NOTNULL => 1},
component_id => {TYPE => 'INT2', NOTNULL => 1,
REFERENCES => {TABLE => 'components',
COLUMN => 'id',
DELETE => 'CASCADE'}},
],
INDEXES => [
component_cc_user_id_idx => {FIELDS => [qw(component_id user_id)],
@ -911,8 +1004,14 @@ use constant ABSTRACT_SCHEMA => {
group_control_map => {
FIELDS => [
group_id => {TYPE => 'INT3', NOTNULL => 1},
product_id => {TYPE => 'INT3', NOTNULL => 1},
group_id => {TYPE => 'INT3', NOTNULL => 1,
REFERENCES => {TABLE => 'groups',
COLUMN => 'id',
DELETE => 'CASCADE'}},
product_id => {TYPE => 'INT2', NOTNULL => 1,
REFERENCES => {TABLE => 'products',
COLUMN => 'id',
DELETE => 'CASCADE'}},
entry => {TYPE => 'BOOLEAN', NOTNULL => 1},
membercontrol => {TYPE => 'BOOLEAN', NOTNULL => 1},
othercontrol => {TYPE => 'BOOLEAN', NOTNULL => 1},
@ -940,8 +1039,14 @@ use constant ABSTRACT_SCHEMA => {
# if GRANT_REGEXP - record was created by evaluating a regexp
user_group_map => {
FIELDS => [
user_id => {TYPE => 'INT3', NOTNULL => 1},
group_id => {TYPE => 'INT3', NOTNULL => 1},
user_id => {TYPE => 'INT3', NOTNULL => 1,
REFERENCES => {TABLE => 'profiles',
COLUMN => 'userid',
DELETE => 'CASCADE'}},
group_id => {TYPE => 'INT3', NOTNULL => 1,
REFERENCES => {TABLE => 'groups',
COLUMN => 'id',
DELETE => 'CASCADE'}},
isbless => {TYPE => 'BOOLEAN', NOTNULL => 1,
DEFAULT => 'FALSE'},
grant_type => {TYPE => 'INT1', NOTNULL => 1,
@ -963,8 +1068,14 @@ use constant ABSTRACT_SCHEMA => {
# if GROUP_VISIBLE - member groups may see grantor group
group_group_map => {
FIELDS => [
member_id => {TYPE => 'INT3', NOTNULL => 1},
grantor_id => {TYPE => 'INT3', NOTNULL => 1},
member_id => {TYPE => 'INT3', NOTNULL => 1,
REFERENCES => {TABLE => 'groups',
COLUMN => 'id',
DELETE => 'CASCADE'}},
grantor_id => {TYPE => 'INT3', NOTNULL => 1,
REFERENCES => {TABLE => 'groups',
COLUMN => 'id',
DELETE => 'CASCADE'}},
grant_type => {TYPE => 'INT1', NOTNULL => 1,
DEFAULT => GROUP_MEMBERSHIP},
],
@ -979,8 +1090,14 @@ use constant ABSTRACT_SCHEMA => {
# in order to see a bug.
bug_group_map => {
FIELDS => [
bug_id => {TYPE => 'INT3', NOTNULL => 1},
group_id => {TYPE => 'INT3', NOTNULL => 1},
bug_id => {TYPE => 'INT3', NOTNULL => 1,
REFERENCES => {TABLE => 'bugs',
COLUMN => 'bug_id',
DELETE => 'CASCADE'}},
group_id => {TYPE => 'INT3', NOTNULL => 1,
REFERENCES => {TABLE => 'groups',
COLUMN => 'id',
DELETE => 'CASCADE'}},
],
INDEXES => [
bug_group_map_bug_id_idx =>
@ -993,8 +1110,14 @@ use constant ABSTRACT_SCHEMA => {
# in order to see a named query somebody else shares.
namedquery_group_map => {
FIELDS => [
namedquery_id => {TYPE => 'INT3', NOTNULL => 1},
group_id => {TYPE => 'INT3', NOTNULL => 1},
namedquery_id => {TYPE => 'INT3', NOTNULL => 1,
REFERENCES => {TABLE => 'namedqueries',
COLUMN => 'id',
DELETE => 'CASCADE'}},
group_id => {TYPE => 'INT3', NOTNULL => 1,
REFERENCES => {TABLE => 'groups',
COLUMN => 'id',
DELETE => 'CASCADE'}},
],
INDEXES => [
namedquery_group_map_namedquery_id_idx =>
@ -1005,8 +1128,14 @@ use constant ABSTRACT_SCHEMA => {
category_group_map => {
FIELDS => [
category_id => {TYPE => 'INT2', NOTNULL => 1},
group_id => {TYPE => 'INT3', NOTNULL => 1},
category_id => {TYPE => 'INT2', NOTNULL => 1,
REFERENCES => {TABLE => 'series_categories',
COLUMN => 'id',
DELETE => 'CASCADE'}},
group_id => {TYPE => 'INT3', NOTNULL => 1,
REFERENCES => {TABLE => 'groups',
COLUMN => 'id',
DELETE => 'CASCADE'}},
],
INDEXES => [
category_group_map_category_id_idx =>
@ -1064,7 +1193,10 @@ use constant ABSTRACT_SCHEMA => {
id => {TYPE => 'SMALLSERIAL', NOTNULL => 1,
PRIMARYKEY => 1},
name => {TYPE => 'varchar(64)', NOTNULL => 1},
product_id => {TYPE => 'INT2', NOTNULL => 1},
product_id => {TYPE => 'INT2', NOTNULL => 1,
REFERENCES => {TABLE => 'products',
COLUMN => 'id',
DELETE => 'CASCADE'}},
initialowner => {TYPE => 'INT3', NOTNULL => 1,
REFERENCES => {TABLE => 'profiles',
COLUMN => 'userid'}},
@ -1089,9 +1221,18 @@ use constant ABSTRACT_SCHEMA => {
FIELDS => [
series_id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
PRIMARYKEY => 1},
creator => {TYPE => 'INT3'},
category => {TYPE => 'INT2', NOTNULL => 1},
subcategory => {TYPE => 'INT2', NOTNULL => 1},
creator => {TYPE => 'INT3',
REFERENCES => {TABLE => 'profiles',
COLUMN => 'userid',
DELETE => 'SET NULL'}},
category => {TYPE => 'INT2', NOTNULL => 1,
REFERENCES => {TABLE => 'series_categories',
COLUMN => 'id',
DELETE => 'CASCADE'}},
subcategory => {TYPE => 'INT2', NOTNULL => 1,
REFERENCES => {TABLE => 'series_categories',
COLUMN => 'id',
DELETE => 'CASCADE'}},
name => {TYPE => 'varchar(64)', NOTNULL => 1},
frequency => {TYPE => 'INT2', NOTNULL => 1},
last_viewed => {TYPE => 'DATETIME'},
@ -1108,7 +1249,10 @@ use constant ABSTRACT_SCHEMA => {
series_data => {
FIELDS => [
series_id => {TYPE => 'INT3', NOTNULL => 1},
series_id => {TYPE => 'INT3', NOTNULL => 1,
REFERENCES => {TABLE => 'series',
COLUMN => 'series_id',
DELETE => 'CASCADE'}},
series_date => {TYPE => 'DATETIME', NOTNULL => 1},
series_value => {TYPE => 'INT3', NOTNULL => 1},
],
@ -1196,7 +1340,10 @@ use constant ABSTRACT_SCHEMA => {
FIELDS => [
quipid => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
PRIMARYKEY => 1},
userid => {TYPE => 'INT3'},
userid => {TYPE => 'INT3',
REFERENCES => {TABLE => 'profiles',
COLUMN => 'userid',
DELETE => 'SET NULL'}},
quip => {TYPE => 'MEDIUMTEXT', NOTNULL => 1},
approved => {TYPE => 'BOOLEAN', NOTNULL => 1,
DEFAULT => 'TRUE'},
@ -1229,7 +1376,10 @@ use constant ABSTRACT_SCHEMA => {
setting_value => {
FIELDS => [
name => {TYPE => 'varchar(32)', NOTNULL => 1},
name => {TYPE => 'varchar(32)', NOTNULL => 1,
REFERENCES => {TABLE => 'setting',
COLUMN => 'name',
DELETE => 'CASCADE'}},
value => {TYPE => 'varchar(32)', NOTNULL => 1},
sortindex => {TYPE => 'INT2', NOTNULL => 1},
],
@ -1256,6 +1406,93 @@ use constant ABSTRACT_SCHEMA => {
],
},
# THESCHWARTZ TABLES
# ------------------
# Note: In the standard TheSchwartz schema, most integers are unsigned,
# but we didn't implement unsigned ints for Bugzilla schemas, so we
# just create signed ints, which should be fine.
ts_funcmap => {
FIELDS => [
funcid => {TYPE => 'INTSERIAL', PRIMARYKEY => 1, NOTNULL => 1},
funcname => {TYPE => 'varchar(255)', NOTNULL => 1},
],
INDEXES => [
ts_funcmap_funcname_idx => {FIELDS => ['funcname'],
TYPE => 'UNIQUE'},
],
},
ts_job => {
FIELDS => [
# In a standard TheSchwartz schema, this is a BIGINT, but we
# don't have those and I didn't want to add them just for this.
jobid => {TYPE => 'INTSERIAL', PRIMARYKEY => 1,
NOTNULL => 1},
funcid => {TYPE => 'INT4', NOTNULL => 1},
# In standard TheSchwartz, this is a MEDIUMBLOB.
arg => {TYPE => 'LONGBLOB'},
uniqkey => {TYPE => 'varchar(255)'},
insert_time => {TYPE => 'INT4'},
run_after => {TYPE => 'INT4', NOTNULL => 1},
grabbed_until => {TYPE => 'INT4', NOTNULL => 1},
priority => {TYPE => 'INT2'},
coalesce => {TYPE => 'varchar(255)'},
],
INDEXES => [
ts_job_funcid_idx => {FIELDS => [qw(funcid uniqkey)],
TYPE => 'UNIQUE'},
# In a standard TheSchewartz schema, these both go in the other
# direction, but there's no reason to have three indexes that
# all start with the same column, and our naming scheme doesn't
# allow it anyhow.
ts_job_run_after_idx => [qw(run_after funcid)],
ts_job_coalesce_idx => [qw(coalesce funcid)],
],
},
ts_note => {
FIELDS => [
# This is a BIGINT in standard TheSchwartz schemas.
jobid => {TYPE => 'INT4', NOTNULL => 1},
notekey => {TYPE => 'varchar(255)'},
value => {TYPE => 'LONGBLOB'},
],
INDEXES => [
ts_note_jobid_idx => {FIELDS => [qw(jobid notekey)],
TYPE => 'UNIQUE'},
],
},
ts_error => {
FIELDS => [
error_time => {TYPE => 'INT4', NOTNULL => 1},
jobid => {TYPE => 'INT4', NOTNULL => 1},
message => {TYPE => 'varchar(255)', NOTNULL => 1},
funcid => {TYPE => 'INT4', NOTNULL => 1, DEFAULT => 0},
],
INDEXES => [
ts_error_funcid_idx => [qw(funcid error_time)],
ts_error_error_time_idx => ['error_time'],
ts_error_jobid_idx => ['jobid'],
],
},
ts_exitstatus => {
FIELDS => [
jobid => {TYPE => 'INTSERIAL', PRIMARYKEY => 1,
NOTNULL => 1},
funcid => {TYPE => 'INT4', NOTNULL => 1, DEFAULT => 0},
status => {TYPE => 'INT2'},
completion_time => {TYPE => 'INT4'},
delete_after => {TYPE => 'INT4'},
],
INDEXES => [
ts_exitstatus_funcid_idx => ['funcid'],
ts_exitstatus_delete_after_idx => ['delete_after'],
],
},
# SCHEMA STORAGE
# --------------
@ -1268,23 +1505,7 @@ use constant ABSTRACT_SCHEMA => {
};
use constant FIELD_TABLE_SCHEMA => {
FIELDS => [
id => {TYPE => 'SMALLSERIAL', NOTNULL => 1,
PRIMARYKEY => 1},
value => {TYPE => 'varchar(64)', NOTNULL => 1},
sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0},
isactive => {TYPE => 'BOOLEAN', NOTNULL => 1,
DEFAULT => 'TRUE'},
],
# Note that bz_add_field_table should prepend the table name
# to these index names.
INDEXES => [
value_idx => {FIELDS => ['value'], TYPE => 'UNIQUE'},
sortkey_idx => ['sortkey', 'value'],
],
};
# Foreign Keys are added in Bugzilla::DB::bz_add_field_tables
use constant MULTI_SELECT_VALUE_TABLE => {
FIELDS => [
bug_id => {TYPE => 'INT3', NOTNULL => 1},

View File

@ -152,7 +152,7 @@ sub get_fk_ddl {
if ( $update =~ /CASCADE/i ){
my $tr_str = "CREATE OR REPLACE TRIGGER ${fk_name}_UC"
. " AFTER UPDATE ON ". $to_table
. " AFTER UPDATE OF $to_column ON $to_table "
. " REFERENCING "
. " NEW AS NEW "
. " OLD AS OLD "

View File

@ -73,9 +73,11 @@ use strict;
use base qw(Exporter Bugzilla::Object);
@Bugzilla::Field::EXPORT = qw(check_field get_field_id get_legal_field_values);
use Bugzilla::Util;
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Util;
use Scalar::Util qw(blessed);
###############################
#### Initialization ####
@ -84,17 +86,21 @@ use Bugzilla::Error;
use constant DB_TABLE => 'fielddefs';
use constant LIST_ORDER => 'sortkey, name';
use constant DB_COLUMNS => (
'id',
'name',
'description',
'type',
'custom',
'mailhead',
'sortkey',
'obsolete',
'classificate',
'enter_bug',
use constant DB_COLUMNS => qw(
id
name
description
type
custom
mailhead
sortkey
obsolete
classificate
enter_bug
buglist
visibility_field_id
visibility_value_id
value_field_id
);
use constant REQUIRED_CREATE_FIELDS => qw(name description);
@ -103,11 +109,18 @@ use constant VALIDATORS => {
custom => \&_check_custom,
description => \&_check_description,
enter_bug => \&_check_enter_bug,
buglist => \&Bugzilla::Object::check_boolean,
mailhead => \&_check_mailhead,
obsolete => \&_check_obsolete,
classificate => \&_check_obsolete,
sortkey => \&_check_sortkey,
type => \&_check_type,
visibility_field_id => \&_check_visibility_field_id,
};
use constant UPDATE_VALIDATORS => {
value_field_id => \&_check_value_field_id,
visibility_value_id => \&_check_control_value,
};
use constant UPDATE_COLUMNS => qw(
@ -117,6 +130,12 @@ use constant UPDATE_COLUMNS => qw(
obsolete
classificate
enter_bug
buglist
visibility_field_id
visibility_value_id
value_field_id
type
);
# How various field types translate into SQL data definitions.
@ -128,32 +147,49 @@ use constant SQL_DEFINITIONS => {
DEFAULT => "'---'" },
FIELD_TYPE_TEXTAREA, { TYPE => 'MEDIUMTEXT' },
FIELD_TYPE_DATETIME, { TYPE => 'DATETIME' },
FIELD_TYPE_BUG_ID, { TYPE => 'INT3' },
};
# Field definitions for the fields that ship with Bugzilla.
# These are used by populate_field_definitions to populate
# the fielddefs table.
use constant DEFAULT_FIELDS => (
{name => 'bug_id', desc => 'Bug #', in_new_bugmail => 1},
{name => 'short_desc', desc => 'Summary', in_new_bugmail => 1},
{name => 'classification', desc => 'Classification', in_new_bugmail => 1},
{name => 'product', desc => 'Product', in_new_bugmail => 1},
{name => 'version', desc => 'Version', in_new_bugmail => 1},
{name => 'rep_platform', desc => 'Platform', in_new_bugmail => 1},
{name => 'bug_id', desc => 'Bug #', in_new_bugmail => 1,
buglist => 1},
{name => 'short_desc', desc => 'Summary', in_new_bugmail => 1,
buglist => 1},
{name => 'classification', desc => 'Classification', in_new_bugmail => 1,
buglist => 1},
{name => 'product', desc => 'Product', in_new_bugmail => 1,
type => FIELD_TYPE_SINGLE_SELECT, buglist => 1},
{name => 'version', desc => 'Version', in_new_bugmail => 1,
buglist => 1},
{name => 'rep_platform', desc => 'Platform', in_new_bugmail => 1,
type => FIELD_TYPE_SINGLE_SELECT, buglist => 1},
{name => 'bug_file_loc', desc => 'URL', in_new_bugmail => 1},
{name => 'op_sys', desc => 'OS/Version', in_new_bugmail => 1},
{name => 'bug_status', desc => 'Status', in_new_bugmail => 1},
{name => 'op_sys', desc => 'OS/Version', in_new_bugmail => 1,
type => FIELD_TYPE_SINGLE_SELECT, buglist => 1},
{name => 'bug_status', desc => 'Status', in_new_bugmail => 1,
type => FIELD_TYPE_SINGLE_SELECT, buglist => 1},
{name => 'status_whiteboard', desc => 'Status Whiteboard',
in_new_bugmail => 1},
{name => 'keywords', desc => 'Keywords', in_new_bugmail => 1},
{name => 'resolution', desc => 'Resolution'},
{name => 'bug_severity', desc => 'Severity', in_new_bugmail => 1},
{name => 'priority', desc => 'Priority', in_new_bugmail => 1},
{name => 'component', desc => 'Component', in_new_bugmail => 1},
{name => 'assigned_to', desc => 'AssignedTo', in_new_bugmail => 1},
{name => 'reporter', desc => 'ReportedBy', in_new_bugmail => 1},
{name => 'votes', desc => 'Votes'},
{name => 'qa_contact', desc => 'QAContact', in_new_bugmail => 1},
in_new_bugmail => 1, buglist => 1},
{name => 'keywords', desc => 'Keywords', in_new_bugmail => 1,
buglist => 1},
{name => 'resolution', desc => 'Resolution',
type => FIELD_TYPE_SINGLE_SELECT, buglist => 1},
{name => 'bug_severity', desc => 'Severity', in_new_bugmail => 1,
type => FIELD_TYPE_SINGLE_SELECT, buglist => 1},
{name => 'priority', desc => 'Priority', in_new_bugmail => 1,
type => FIELD_TYPE_SINGLE_SELECT, buglist => 1},
{name => 'component', desc => 'Component', in_new_bugmail => 1,
buglist => 1},
{name => 'assigned_to', desc => 'AssignedTo', in_new_bugmail => 1,
buglist => 1},
{name => 'reporter', desc => 'ReportedBy', in_new_bugmail => 1,
buglist => 1},
{name => 'votes', desc => 'Votes', buglist => 1},
{name => 'qa_contact', desc => 'QAContact', in_new_bugmail => 1,
buglist => 1},
{name => 'cc', desc => 'CC', in_new_bugmail => 1},
{name => 'dependson', desc => 'Depends on', in_new_bugmail => 1},
{name => 'blocked', desc => 'Blocks', in_new_bugmail => 1},
@ -166,31 +202,53 @@ use constant DEFAULT_FIELDS => (
{name => 'attachments.isprivate', desc => 'Attachment is private'},
{name => 'attachments.submitter', desc => 'Attachment creator'},
{name => 'target_milestone', desc => 'Target Milestone'},
{name => 'creation_ts', desc => 'Creation date', in_new_bugmail => 1},
{name => 'delta_ts', desc => 'Last changed date', in_new_bugmail => 1},
{name => 'target_milestone', desc => 'Target Milestone',
buglist => 1},
{name => 'creation_ts', desc => 'Creation date',
in_new_bugmail => 1, buglist => 1},
{name => 'delta_ts', desc => 'Last changed date',
in_new_bugmail => 1, buglist => 1},
{name => 'longdesc', desc => 'Comment'},
{name => 'longdescs.isprivate', desc => 'Comment is private'},
{name => 'alias', desc => 'Alias'},
{name => 'alias', desc => 'Alias', buglist => 1},
{name => 'everconfirmed', desc => 'Ever Confirmed'},
{name => 'reporter_accessible', desc => 'Reporter Accessible'},
{name => 'cclist_accessible', desc => 'CC Accessible'},
{name => 'bug_group', desc => 'Group'},
{name => 'estimated_time', desc => 'Estimated Hours', in_new_bugmail => 1},
{name => 'remaining_time', desc => 'Remaining Hours'},
{name => 'deadline', desc => 'Deadline', in_new_bugmail => 1},
{name => 'bug_group', desc => 'Group', in_new_bugmail => 1},
{name => 'estimated_time', desc => 'Estimated Hours',
in_new_bugmail => 1, buglist => 1},
{name => 'remaining_time', desc => 'Remaining Hours', buglist => 1},
{name => 'deadline', desc => 'Deadline',
in_new_bugmail => 1, buglist => 1},
{name => 'commenter', desc => 'Commenter'},
{name => 'flagtypes.name', desc => 'Flag'},
{name => 'requestees.login_name', desc => 'Flag Requestee'},
{name => 'setters.login_name', desc => 'Flag Setter'},
{name => 'work_time', desc => 'Hours Worked'},
{name => 'percentage_complete', desc => 'Percentage Complete'},
{name => 'work_time', desc => 'Hours Worked', buglist => 1},
{name => 'percentage_complete', desc => 'Percentage Complete',
buglist => 1},
{name => 'content', desc => 'Content'},
{name => 'attach_data.thedata', desc => 'Attachment data'},
{name => 'attachments.isurl', desc => 'Attachment is a URL'},
{name => "owner_idle_time", desc => "Time Since Assignee Touched"},
{name => 'see_also', desc => "See Also",
type => FIELD_TYPE_BUG_URLS},
);
################
# Constructors #
################
# Override match to add is_select.
sub match {
my $self = shift;
my ($params) = @_;
if (delete $params->{is_select}) {
$params->{type} = [FIELD_TYPE_SINGLE_SELECT, FIELD_TYPE_MULTI_SELECT];
}
return $self->SUPER::match(@_);
}
##############
# Validators #
##############
@ -256,11 +314,51 @@ sub _check_type {
my $saved_type = $type;
# The constant here should be updated every time a new,
# higher field type is added.
(detaint_natural($type) && $type <= FIELD_TYPE_DATETIME)
(detaint_natural($type) && $type <= FIELD_TYPE_BUG_URLS)
|| ThrowCodeError('invalid_customfield_type', { type => $saved_type });
return $type;
}
sub _check_value_field_id {
my ($invocant, $field_id, $is_select) = @_;
$is_select = $invocant->is_select if !defined $is_select;
if ($field_id && !$is_select) {
ThrowUserError('field_value_control_select_only');
}
return $invocant->_check_visibility_field_id($field_id);
}
sub _check_visibility_field_id {
my ($invocant, $field_id) = @_;
$field_id = trim($field_id);
return undef if !$field_id;
my $field = Bugzilla::Field->check({ id => $field_id });
if (blessed($invocant) && $field->id == $invocant->id) {
ThrowUserError('field_cant_control_self', { field => $field });
}
if (!$field->is_select) {
ThrowUserError('field_control_must_be_select',
{ field => $field });
}
return $field->id;
}
sub _check_control_value {
my ($invocant, $value_id, $field_id) = @_;
my $field;
if (blessed $invocant) {
$field = $invocant->visibility_field;
}
elsif ($field_id) {
$field = $invocant->new($field_id);
}
# When no field is set, no value is set.
return undef if !$field;
my $value_obj = Bugzilla::Field::Choice->type($field)
->check({ id => $value_id });
return $value_obj->id;
}
=pod
=head2 Instance Properties
@ -376,25 +474,163 @@ sub enter_bug { return $_[0]->{enter_bug} }
=over
=item C<legal_values>
=item C<buglist>
A reference to an array with valid active values for this field.
A boolean specifying whether or not this field is selectable
as a display or order column in buglist.cgi
=back
=cut
sub buglist { return $_[0]->{buglist} }
=over
=item C<is_select>
True if this is a C<FIELD_TYPE_SINGLE_SELECT> or C<FIELD_TYPE_MULTI_SELECT>
field. It is only safe to call L</legal_values> if this is true.
=item C<legal_values>
Valid values for this field, as an array of L<Bugzilla::Field::Choice>
objects.
=back
=cut
sub is_select {
return ($_[0]->type == FIELD_TYPE_SINGLE_SELECT
|| $_[0]->type == FIELD_TYPE_MULTI_SELECT) ? 1 : 0
}
sub legal_values {
my $self = shift;
if (!defined $self->{'legal_values'}) {
$self->{'legal_values'} = get_legal_field_values($self->name);
require Bugzilla::Field::Choice;
my @values = Bugzilla::Field::Choice->type($self)->get_all();
$self->{'legal_values'} = \@values;
}
return $self->{'legal_values'};
}
=pod
=over
=item C<visibility_field>
What field controls this field's visibility? Returns a C<Bugzilla::Field>
object representing the field that controls this field's visibility.
Returns undef if there is no field that controls this field's visibility.
=back
=cut
sub visibility_field {
my $self = shift;
if ($self->{visibility_field_id}) {
$self->{visibility_field} ||=
$self->new($self->{visibility_field_id});
}
return $self->{visibility_field};
}
=pod
=over
=item C<visibility_value>
If we have a L</visibility_field>, then what value does that field have to
be set to in order to show this field? Returns a L<Bugzilla::Field::Choice>
or undef if there is no C<visibility_field> set.
=back
=cut
sub visibility_value {
my $self = shift;
if ($self->{visibility_field_id}) {
require Bugzilla::Field::Choice;
$self->{visibility_value} ||=
Bugzilla::Field::Choice->type($self->visibility_field)->new(
$self->{visibility_value_id});
}
return $self->{visibility_value};
}
=pod
=over
=item C<controls_visibility_of>
An arrayref of C<Bugzilla::Field> objects, representing fields that this
field controls the visibility of.
=back
=cut
sub controls_visibility_of {
my $self = shift;
$self->{controls_visibility_of} ||=
Bugzilla::Field->match({ visibility_field_id => $self->id });
return $self->{controls_visibility_of};
}
=pod
=over
=item C<value_field>
The Bugzilla::Field that controls the list of values for this field.
Returns undef if there is no field that controls this field's visibility.
=back
=cut
sub value_field {
my $self = shift;
if ($self->{value_field_id}) {
$self->{value_field} ||= $self->new($self->{value_field_id});
}
return $self->{value_field};
}
=pod
=over
=item C<controls_values_of>
An arrayref of C<Bugzilla::Field> objects, representing fields that this
field controls the values of.
=back
=cut
sub controls_values_of {
my $self = shift;
$self->{controls_values_of} ||=
Bugzilla::Field->match({ value_field_id => $self->id });
return $self->{controls_values_of};
}
=pod
=head2 Instance Mutators
These set the particular field that they are named after.
@ -415,6 +651,14 @@ They will throw an error if you try to set the values to something invalid.
=item C<set_in_new_bugmail>
=item C<set_buglist>
=item C<set_visibility_field>
=item C<set_visibility_value>
=item C<set_value_field>
=back
=cut
@ -425,6 +669,26 @@ sub set_obsolete { $_[0]->set('obsolete', $_[1]); }
sub set_classificate { $_[0]->set('classificate', $_[1]); }
sub set_sortkey { $_[0]->set('sortkey', $_[1]); }
sub set_in_new_bugmail { $_[0]->set('mailhead', $_[1]); }
sub set_buglist { $_[0]->set('buglist', $_[1]); }
sub set_visibility_field {
my ($self, $value) = @_;
$self->set('visibility_field_id', $value);
delete $self->{visibility_field};
delete $self->{visibility_value};
}
sub set_visibility_value {
my ($self, $value) = @_;
$self->set('visibility_value_id', $value);
delete $self->{visibility_value};
}
sub set_value_field {
my ($self, $value) = @_;
$self->set('value_field_id', $value);
delete $self->{value_field};
}
# This is only used internally by upgrade code in Bugzilla::Field.
sub _set_type { $_[0]->set('type', $_[1]); }
=pod
@ -499,9 +763,7 @@ sub remove_from_db {
$dbh->bz_drop_column('bugs', $name);
}
if ($type == FIELD_TYPE_SINGLE_SELECT
|| $type == FIELD_TYPE_MULTI_SELECT)
{
if ($self->is_select) {
# Delete the table that holds the legal values for this field.
$dbh->bz_drop_field_tables($self);
}
@ -536,6 +798,9 @@ will be added to the C<bugs> table if it does not exist. Defaults to 0.
=item C<enter_bug> - boolean - Whether this field is
editable on the bug creation form. Defaults to 0.
=item C<buglist> - boolean - Whether this field is
selectable as a display or order column in bug lists. Defaults to 0.
C<obsolete> - boolean - Whether this field is obsolete. Defaults to 0.
C<classificate> - boolean - Whether this field's legal values must be restricted to bug classification.
@ -559,9 +824,7 @@ sub create {
$dbh->bz_add_column('bugs', $name, SQL_DEFINITIONS->{$type});
}
if ($type == FIELD_TYPE_SINGLE_SELECT
|| $type == FIELD_TYPE_MULTI_SELECT)
{
if ($field->is_select) {
# Create the table that holds the legal values for this field.
$dbh->bz_add_field_tables($field);
}
@ -586,9 +849,28 @@ sub run_create_validators {
"SELECT MAX(sortkey) + 100 FROM fielddefs") || 100;
}
$params->{visibility_value_id} =
$class->_check_control_value($params->{visibility_value_id},
$params->{visibility_field_id});
my $type = $params->{type} || 0;
$params->{value_field_id} =
$class->_check_value_field_id($params->{value_field_id},
($type == FIELD_TYPE_SINGLE_SELECT
|| $type == FIELD_TYPE_MULTI_SELECT) ? 1 : 0);
return $params;
}
sub update {
my $self = shift;
my $changes = $self->SUPER::update(@_);
my $dbh = Bugzilla->dbh;
if ($changes->{value_field_id} && $self->is_select) {
$dbh->do("UPDATE " . $self->name . " SET visibility_value_id = NULL");
}
return $changes;
}
=pod
@ -643,6 +925,8 @@ sub populate_field_definitions {
if ($field) {
$field->set_description($def->{desc});
$field->set_in_new_bugmail($def->{in_new_bugmail});
$field->set_buglist($def->{buglist});
$field->_set_type($def->{type}) if $def->{type};
$field->update();
}
else {
@ -650,8 +934,7 @@ sub populate_field_definitions {
$def->{mailhead} = $def->{in_new_bugmail};
delete $def->{in_new_bugmail};
}
$def->{description} = $def->{desc};
delete $def->{desc};
$def->{description} = delete $def->{desc};
Bugzilla::Field->create($def);
}
}

445
Bugzilla/Field/Choice.pm Normal file
View File

@ -0,0 +1,445 @@
# -*- 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 Initial Developer of the Original Code is NASA.
# Portions created by NASA are Copyright (C) 2006 San Jose State
# University Foundation. All Rights Reserved.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org>
use strict;
package Bugzilla::Field::Choice;
use base qw(Bugzilla::Object);
use Bugzilla::Config qw(SetParam write_params);
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Field;
use Bugzilla::Util qw(trim detaint_natural);
use Scalar::Util qw(blessed);
##################
# Initialization #
##################
use constant DB_COLUMNS => qw(
id
value
sortkey
visibility_value_id
);
use constant UPDATE_COLUMNS => qw(
value
sortkey
visibility_value_id
);
use constant NAME_FIELD => 'value';
use constant LIST_ORDER => 'sortkey, value';
use constant REQUIRED_CREATE_FIELDS => qw(value);
use constant VALIDATORS => {
value => \&_check_value,
sortkey => \&_check_sortkey,
visibility_value_id => \&_check_visibility_value_id,
};
use constant CLASS_MAP => {
bug_status => 'Bugzilla::Status',
product => 'Bugzilla::Product',
};
use constant DEFAULT_MAP => {
op_sys => 'defaultopsys',
rep_platform => 'defaultplatform',
priority => 'defaultpriority',
bug_severity => 'defaultseverity',
};
#################
# Class Factory #
#################
# Bugzilla::Field::Choice is actually an abstract base class. Every field
# type has its own dynamically-generated class for its values. This allows
# certain fields to have special types, like how bug_status's values
# are Bugzilla::Status objects.
sub type {
my ($class, $field) = @_;
my $field_obj = blessed $field ? $field : Bugzilla::Field->check($field);
my $field_name = $field_obj->name;
if ($class->CLASS_MAP->{$field_name}) {
return $class->CLASS_MAP->{$field_name};
}
# For generic classes, we use a lowercase class name, so as
# not to interfere with any real subclasses we might make some day.
my $package = "Bugzilla::Field::Choice::$field_name";
Bugzilla->request_cache->{"field_$package"} = $field_obj;
# This package only needs to be created once. We check if the DB_TABLE
# glob for this package already exists, which tells us whether or not
# we need to create the package (this works even under mod_perl, where
# this package definition will persist across requests)).
if (!defined *{"${package}::DB_TABLE"}) {
eval <<EOC;
package $package;
use base qw(Bugzilla::Field::Choice);
use constant DB_TABLE => '$field_name';
EOC
}
return $package;
}
################
# Constructors #
################
# We just make new() enforce this, which should give developers
# the understanding that you can't use Bugzilla::Field::Choice
# without calling type().
sub new {
my $class = shift;
if ($class eq 'Bugzilla::Field::Choice') {
ThrowCodeError('field_choice_must_use_type');
}
$class->SUPER::new(@_);
}
#########################
# Database Manipulation #
#########################
# Our subclasses can take more arguments than we normally accept.
# So, we override create() to remove arguments that aren't valid
# columns. (Normally Bugzilla::Object dies if you pass arguments
# that aren't valid columns.)
sub create {
my $class = shift;
my ($params) = @_;
foreach my $key (keys %$params) {
if (!grep {$_ eq $key} $class->DB_COLUMNS) {
delete $params->{$key};
}
}
return $class->SUPER::create(@_);
}
sub update {
my $self = shift;
my $dbh = Bugzilla->dbh;
my $fname = $self->field->name;
$dbh->bz_start_transaction();
my ($changes, $old_self) = $self->SUPER::update(@_);
if (exists $changes->{value}) {
my ($old, $new) = @{ $changes->{value} };
if ($self->field->type == FIELD_TYPE_MULTI_SELECT) {
$dbh->do("UPDATE bug_$fname SET value = ? WHERE value = ?",
undef, $new, $old);
}
else {
$dbh->do("UPDATE bugs SET $fname = ? WHERE $fname = ?",
undef, $new, $old);
}
if ($old_self->is_default) {
my $param = $self->DEFAULT_MAP->{$self->field->name};
SetParam($param, $self->name);
write_params();
}
}
$dbh->bz_commit_transaction();
return wantarray ? ($changes, $old_self) : $changes;
}
sub remove_from_db {
my $self = shift;
if ($self->is_default) {
ThrowUserError('fieldvalue_is_default',
{ field => $self->field, value => $self,
param_name => $self->DEFAULT_MAP->{$self->field->name},
});
}
if ($self->is_static) {
ThrowUserError('fieldvalue_not_deletable',
{ field => $self->field, value => $self });
}
if ($self->bug_count) {
ThrowUserError("fieldvalue_still_has_bugs",
{ field => $self->field, value => $self });
}
$self->_check_if_controller();
$self->SUPER::remove_from_db();
}
# Factored out to make life easier for subclasses.
sub _check_if_controller {
my $self = shift;
my $vis_fields = $self->controls_visibility_of_fields;
my $values = $self->controlled_values;
if (@$vis_fields || scalar(keys %$values)) {
ThrowUserError('fieldvalue_is_controller',
{ value => $self, fields => [map($_->name, @$vis_fields)],
vals => $values });
}
}
#############
# Accessors #
#############
sub sortkey { return $_[0]->{'sortkey'}; }
sub bug_count {
my $self = shift;
return $self->{bug_count} if defined $self->{bug_count};
my $dbh = Bugzilla->dbh;
my $fname = $self->field->name;
my $count;
if ($self->field->type == FIELD_TYPE_MULTI_SELECT) {
$count = $dbh->selectrow_array("SELECT COUNT(*) FROM bug_$fname
WHERE value = ?", undef, $self->name);
}
else {
$count = $dbh->selectrow_array("SELECT COUNT(*) FROM bugs
WHERE $fname = ?",
undef, $self->name);
}
$self->{bug_count} = $count;
return $count;
}
sub field {
my $invocant = shift;
my $class = ref $invocant || $invocant;
my $cache = Bugzilla->request_cache;
# This is just to make life easier for subclasses. Our auto-generated
# subclasses from type() already have this set.
$cache->{"field_$class"} ||=
new Bugzilla::Field({ name => $class->DB_TABLE });
return $cache->{"field_$class"};
}
sub is_default {
my $self = shift;
my $name = $self->DEFAULT_MAP->{$self->field->name};
# If it doesn't exist in DEFAULT_MAP, then there is no parameter
# related to this field.
return 0 unless $name;
return ($self->name eq Bugzilla->params->{$name}) ? 1 : 0;
}
sub is_static {
my $self = shift;
# If we need to special-case Resolution for *anything* else, it should
# get its own subclass.
if ($self->field->name eq 'resolution') {
return grep($_ eq $self->name, ('', 'FIXED', 'MOVED', 'DUPLICATE'))
? 1 : 0;
}
elsif ($self->field->custom) {
return $self->name eq '---' ? 1 : 0;
}
return 0;
}
sub controls_visibility_of_fields {
my $self = shift;
$self->{controls_visibility_of_fields} ||= Bugzilla::Field->match(
{ visibility_field_id => $self->field->id,
visibility_value_id => $self->id });
return $self->{controls_visibility_of_fields};
}
sub visibility_value {
my $self = shift;
if ($self->{visibility_value_id}) {
$self->{visibility_value} ||=
Bugzilla::Field::Choice->type($self->field->value_field)->new(
$self->{visibility_value_id});
}
return $self->{visibility_value};
}
sub controlled_values {
my $self = shift;
return $self->{controlled_values} if defined $self->{controlled_values};
my $fields = $self->field->controls_values_of;
my %controlled_values;
foreach my $field (@$fields) {
$controlled_values{$field->name} =
Bugzilla::Field::Choice->type($field)
->match({ visibility_value_id => $self->id });
}
$self->{controlled_values} = \%controlled_values;
return $self->{controlled_values};
}
############
# Mutators #
############
sub set_name { $_[0]->set('value', $_[1]); }
sub set_sortkey { $_[0]->set('sortkey', $_[1]); }
sub set_visibility_value {
my ($self, $value) = @_;
$self->set('visibility_value_id', $value);
delete $self->{visibility_value};
}
##############
# Validators #
##############
sub _check_value {
my ($invocant, $value) = @_;
my $field = $invocant->field;
$value = trim($value);
# Make sure people don't rename static values
if (blessed($invocant) && $value ne $invocant->name
&& $invocant->is_static)
{
ThrowUserError('fieldvalue_not_editable',
{ field => $field, old_value => $invocant });
}
ThrowUserError('fieldvalue_undefined') if !defined $value || $value eq "";
ThrowUserError('fieldvalue_name_too_long', { value => $value })
if length($value) > MAX_FIELD_VALUE_SIZE;
my $exists = $invocant->type($field)->new({ name => $value });
if ($exists && (!blessed($invocant) || $invocant->id != $exists->id)) {
ThrowUserError('fieldvalue_already_exists',
{ field => $field, value => $exists });
}
return $value;
}
sub _check_sortkey {
my ($invocant, $value) = @_;
$value = trim($value);
return 0 if !$value;
# Store for the error message in case detaint_natural clears it.
my $orig_value = $value;
detaint_natural($value)
|| ThrowUserError('fieldvalue_sortkey_invalid',
{ sortkey => $orig_value,
field => $invocant->field });
return $value;
}
sub _check_visibility_value_id {
my ($invocant, $value_id) = @_;
$value_id = trim($value_id);
my $field = $invocant->field->value_field;
return undef if !$field || !$value_id;
my $value_obj = Bugzilla::Field::Choice->type($field)
->check({ id => $value_id });
return $value_obj->id;
}
1;
__END__
=head1 NAME
Bugzilla::Field::Choice - A legal value for a <select>-type field.
=head1 SYNOPSIS
my $field = new Bugzilla::Field({name => 'bug_status'});
my $choice = new Bugzilla::Field::Choice->type($field)->new(1);
my $choices = Bugzilla::Field::Choice->type($field)->new_from_list([1,2,3]);
my $choices = Bugzilla::Field::Choice->type($field)->get_all();
my $choices = Bugzilla::Field::Choice->type($field->match({ sortkey => 10 });
=head1 DESCRIPTION
This is an implementation of L<Bugzilla::Object>, but with a twist.
You can't call any class methods (such as C<new>, C<create>, etc.)
directly on C<Bugzilla::Field::Choice> itself. Instead, you have to
call C<Bugzilla::Field::Choice-E<gt>type($field)> to get the class
you're going to instantiate, and then you call the methods on that.
We do that because each field has its own database table for its values, so
each value type needs its own class.
See the L</SYNOPSIS> for examples of how this works.
=head1 METHODS
=head2 Class Factory
In object-oriented design, a "class factory" is a method that picks
and returns the right class for you, based on an argument that you pass.
=over
=item C<type>
Takes a single argument, which is either the name of a field from the
C<fielddefs> table, or a L<Bugzilla::Field> object representing a field.
Returns an appropriate subclass of C<Bugzilla::Field::Choice> that you
can now call class methods on (like C<new>, C<create>, C<match>, etc.)
B<NOTE>: YOU CANNOT CALL CLASS METHODS ON C<Bugzilla::Field::Choice>. You
must call C<type> to get a class you can call methods on.
=back
=head2 Accessors
These are in addition to the standard L<Bugzilla::Object> accessors.
=over
=item C<sortkey>
The key that determines the sort order of this item.
=item C<field>
The L<Bugzilla::Field> object that this field value belongs to.
=item C<controlled_values>
Tells you which values in B<other> fields appear (become visible) when this
value is set in its field.
Returns a hashref of arrayrefs. The hash keys are the names of fields,
and the values are arrays of C<Bugzilla::Field::Choice> objects,
representing values that this value controls the visibility of, for
that field.
=back

View File

@ -180,7 +180,7 @@ sub attachment {
return undef unless $self->attach_id;
require Bugzilla::Attachment;
$self->{'attachment'} ||= Bugzilla::Attachment->get($self->attach_id);
$self->{'attachment'} ||= new Bugzilla::Attachment($self->attach_id);
return $self->{'attachment'};
}
@ -515,7 +515,7 @@ sub snapshot {
'attach_id' => $attach_id });
my @summaries;
foreach my $flag (@$flags) {
my $summary = $flag->type->name . $flag->status;
my $summary = $flag->setter->nick . ':' . $flag->type->name . $flag->status;
$summary .= "(" . $flag->requestee->login . ")" if $flag->requestee;
push(@summaries, $summary);
}
@ -625,10 +625,13 @@ sub update_activity {
my ($bug_id, $attach_id, $timestamp, $old_summaries, $new_summaries) = @_;
my $dbh = Bugzilla->dbh;
$old_summaries = join(", ", @$old_summaries);
$new_summaries = join(", ", @$new_summaries);
my ($removed, $added) = diff_strings($old_summaries, $new_summaries);
if ($removed ne $added) {
my ($removed, $added) = diff_arrays($old_summaries, $new_summaries);
if (scalar @$removed || scalar @$added) {
# Remove flag requester/setter information
foreach (@$removed, @$added) { s/^[^:]+:// }
$removed = join(", ", @$removed);
$added = join(", ", @$added);
my $field_id = get_field_id('flagtypes.name');
$dbh->do('INSERT INTO bugs_activity
(bug_id, attach_id, who, bug_when, fieldid, removed, added)
@ -1106,14 +1109,13 @@ sub notify {
foreach my $to (keys %recipients) {
# Add threadingmarker to allow flag notification emails to be the
# threaded similar to normal bug change emails.
my $user_id = $recipients{$to} ? $recipients{$to}->id : 0;
my $threadingmarker = build_thread_marker($bug->id, $user_id);
my $thread_user_id = $recipients{$to} ? $recipients{$to}->id : 0;
my $vars = { 'flag' => $flag,
'to' => $to,
'bug' => $bug,
'attachment' => $attachment,
'threadingmarker' => $threadingmarker };
'threadingmarker' => build_thread_marker($bug->id, $thread_user_id) };
my $lang = $recipients{$to} ?
$recipients{$to}->settings->{'lang'}->{'value'} : $default_lang;
@ -1159,6 +1161,44 @@ sub CancelRequests {
\@old_summaries, \@new_summaries);
}
# This is an internal function used by $bug->flag_types
# and $attachment->flag_types to collect data about available
# flag types and existing flags set on them. You should never
# call this function directly.
sub _flag_types {
my $vars = shift;
my $target_type = $vars->{target_type};
my $flags;
# Retrieve all existing flags for this bug/attachment.
if ($target_type eq 'bug') {
my $bug_id = delete $vars->{bug_id};
$flags = Bugzilla::Flag->match({target_type => 'bug', bug_id => $bug_id});
}
elsif ($target_type eq 'attachment') {
my $attach_id = delete $vars->{attach_id};
$flags = Bugzilla::Flag->match({attach_id => $attach_id});
}
else {
ThrowCodeError('bad_arg', {argument => 'target_type',
function => 'Bugzilla::Flag::_flag_types'});
}
# Get all available flag types for the given product and component.
my $flag_types = Bugzilla::FlagType::match($vars);
$_->{flags} = [] foreach @$flag_types;
my %flagtypes = map { $_->id => $_ } @$flag_types;
# Group existing flags per type.
# Call the internal 'type_id' variable instead of the method
# to not create a flagtype object.
push(@{$flagtypes{$_->{type_id}}->{flags}}, $_) foreach @$flags;
return [sort {$a->sortkey <=> $b->sortkey || $a->name cmp $b->name} values %flagtypes];
}
=head1 SEE ALSO
=over

View File

@ -103,7 +103,7 @@ sub grant_direct {
my ($self, $type) = @_;
$self->{grant_direct} ||= {};
return $self->{grant_direct}->{$type}
if defined $self->{members_direct}->{$type};
if defined $self->{grant_direct}->{$type};
my $dbh = Bugzilla->dbh;
my $ids = $dbh->selectcol_arrayref(
@ -198,7 +198,30 @@ sub is_active_bug_group {
sub _rederive_regexp {
my ($self) = @_;
RederiveRegexp($self->user_regexp, $self->id);
my $dbh = Bugzilla->dbh;
my $sth = $dbh->prepare("SELECT userid, login_name, group_id
FROM profiles
LEFT JOIN user_group_map
ON user_group_map.user_id = profiles.userid
AND group_id = ?
AND grant_type = ?
AND isbless = 0");
my $sthadd = $dbh->prepare("INSERT INTO user_group_map
(user_id, group_id, grant_type, isbless)
VALUES (?, ?, ?, 0)");
my $sthdel = $dbh->prepare("DELETE FROM user_group_map
WHERE user_id = ? AND group_id = ?
AND grant_type = ? and isbless = 0");
$sth->execute($self->id, GRANT_REGEXP);
my $regexp = $self->user_regexp;
while (my ($uid, $login, $present) = $sth->fetchrow_array) {
if ($regexp ne '' and $login =~ /$regexp/i) {
$sthadd->execute($uid, $self->id, GRANT_REGEXP) unless $present;
} else {
$sthdel->execute($uid, $self->id, GRANT_REGEXP) if $present;
}
}
}
sub members_non_inherited {
@ -215,6 +238,33 @@ sub members_non_inherited {
return $self->{members_non_inherited};
}
sub flatten_group_membership {
my ($self, @groups) = @_;
my $dbh = Bugzilla->dbh;
my $sth;
my @groupidstocheck = @groups;
my %groupidschecked = ();
$sth = $dbh->prepare("SELECT member_id FROM group_group_map
WHERE grantor_id = ?
AND grant_type = " . GROUP_MEMBERSHIP);
while (my $node = shift @groupidstocheck) {
$sth->execute($node);
my $member;
while (($member) = $sth->fetchrow_array) {
if (!$groupidschecked{$member}) {
$groupidschecked{$member} = 1;
push @groupidstocheck, $member;
push @groups, $member unless grep $_ == $member, @groups;
}
}
}
return \@groups;
}
################################
##### Module Subroutines ###
################################
@ -266,35 +316,6 @@ sub ValidateGroupName {
return $ret;
}
# This sub is not perldoc'ed because we expect it to go away and
# just become the _rederive_regexp private method.
sub RederiveRegexp {
my ($regexp, $gid) = @_;
my $dbh = Bugzilla->dbh;
my $sth = $dbh->prepare("SELECT userid, login_name, group_id
FROM profiles
LEFT JOIN user_group_map
ON user_group_map.user_id = profiles.userid
AND group_id = ?
AND grant_type = ?
AND isbless = 0");
my $sthadd = $dbh->prepare("INSERT INTO user_group_map
(user_id, group_id, grant_type, isbless)
VALUES (?, ?, ?, 0)");
my $sthdel = $dbh->prepare("DELETE FROM user_group_map
WHERE user_id = ? AND group_id = ?
AND grant_type = ? and isbless = 0");
$sth->execute($gid, GRANT_REGEXP);
while (my ($uid, $login, $present) = $sth->fetchrow_array()) {
if (($regexp =~ /\S+/) && ($login =~ m/$regexp/i))
{
$sthadd->execute($uid, $gid, GRANT_REGEXP) unless $present;
} else {
$sthdel->execute($uid, $gid, GRANT_REGEXP) if $present;
}
}
}
###############################
### Validators ###
###############################
@ -400,4 +421,12 @@ Returns an arrayref of L<Bugzilla::User> objects representing people who are
the group regular expression, or they have been actually added to the
group manually.
=item C<flatten_group_membership>
Accepts a list of groups and returns a list of all the groups whose members
inherit membership in any group on the list. So, we can determine if a user
is in any of the groups input to flatten_group_membership by querying the
user_group_map for any user with DIRECT or REGEXP membership IN() the list
of groups returned.
=back

View File

@ -170,6 +170,74 @@ This describes what hooks exist in Bugzilla currently. They are mostly
in alphabetical order, but some related hooks are near each other instead
of being alphabetical.
=head2 auth-login_methods
This allows you to add new login types to Bugzilla.
(See L<Bugzilla::Auth::Login>.)
Params:
=over
=item C<modules>
This is a hash--a mapping from login-type "names" to the actual module on
disk. The keys will be all the values that were passed to
L<Bugzilla::Auth/login> for the C<Login> parameter. The values are the
actual path to the module on disk. (For example, if the key is C<DB>, the
value is F<Bugzilla/Auth/Login/DB.pm>.)
For your extension, the path will start with
F<extensions/yourextension/lib/>. (See the code in the example extension.)
If your login type is in the hash as a key, you should set that key to the
right path to your module. That module's C<new> method will be called,
probably with empty parameters. If your login type is I<not> in the hash,
you should not set it.
You will be prevented from adding new keys to the hash, so make sure your
key is in there before you modify it. (In other words, you can't add in
login methods that weren't passed to L<Bugzilla::Auth/login>.)
=back
=head2 auth-verify_methods
This works just like L</auth-login_methods> except it's for
login verification methods (See L<Bugzilla::Auth::Verify>.) It also
takes a C<modules> parameter, just like L</auth-login_methods>.
=head2 bug-columns
This allows you to add new fields that will show up in every L<Bugzilla::Bug>
object. Note that you will also need to use the L</bug-fields> hook in
conjunction with this hook to make this work.
Params:
=over
=item C<columns> - An arrayref containing an array of column names. Push
your column name(s) onto the array.
=back
=head2 bug-end_of_create
This happens at the end of L<Bugzilla::Bug/create>, after all other changes are
made to the database. This occurs inside a database transaction.
Params:
=over
=item C<bug> - The changed bug object, with all fields set to their updated
values.
=item C<timestamp> - The timestamp used for all updates in this transaction.
=back
=head2 bug-end_of_update
This happens at the end of L<Bugzilla::Bug/update>, after all other changes are
@ -189,6 +257,23 @@ C<$changes-E<gt>{field} = [old, new]>
=back
=head2 bug-fields
Allows the addition of database fields from the bugs table to the standard
list of allowable fields in a L<Bugzilla::Bug> object, so that
you can call the field as a method.
Note: You should add here the names of any fields you added in L</bug-columns>.
Params:
=over
=item C<columns> - A arrayref containing an array of column names. Push
your column name(s) onto the array.
=back
=head2 buglist-columns
This happens in buglist.cgi after the standard columns have been defined and
@ -233,6 +318,51 @@ See L</buglist-columns>.
=back
=head2 config-add_panels
If you want to add new panels to the Parameters administrative interface,
this is where you do it.
Params:
=over
=item C<panel_modules>
A hashref, where the keys are the "name" of the module and the value
is the Perl module containing that config module. For example, if
the name is C<Auth>, the value would be C<Bugzilla::Config::Auth>.
For your extension, the Perl module name must start with
C<extensions::yourextension::lib>. (See the code in the example
extension.)
=back
=head2 config-modify_panels
This is how you modify already-existing panels in the Parameters
administrative interface. For example, if you wanted to add a new
Auth method (modifying Bugzilla::Config::Auth) this is how you'd
do it.
Params:
=over
=item C<panels>
A hashref, where the keys are lower-case panel "names" (like C<auth>,
C<admin>, etc.) and the values are hashrefs. The hashref contains a
single key, C<params>. C<params> is an arrayref--the return value from
C<get_param_list> for that module. You can modify C<params> and
your changes will be reflected in the interface.
Adding new keys to C<panels> will have no effect. You should use
L</config-add_panels> if you want to add new panels.
=back
=head2 enter_bug-entrydefaultvars
This happens right before the template is loaded on enter_bug.cgi.
@ -336,6 +466,18 @@ database when run.
=back
=head2 mailer-before_send
Called right before L<Bugzilla::Mailer> sends a message to the MTA.
Params:
=over
=item C<email> - The C<Email::MIME> object that's about to be sent.
=back
=head2 product-confirm_delete
Called before displaying the confirmation message when deleting a product.

View File

@ -63,6 +63,8 @@ sub SETTINGS {
# 2007-07-02 altlist@gmail.com -- Bug 225731
quote_replies => { options => ['quoted_reply', 'simple_reply', 'off'],
default => "quoted_reply" },
# 2008-08-27 LpSolit@gmail.com -- Bug 182238
timezone => { subclass => 'Timezone', default => 'local' },
# 2008-12-22 vfilippov@custis.ru -- Custis Bug 17481
remind_me_about_worktime => { options => ['on', 'off'], default => 'on' },
remind_me_about_flags => { options => ['on', 'off'], default => 'on' },
@ -275,7 +277,7 @@ sub create_admin {
my $admin_group = new Bugzilla::Group({ name => 'admin' });
my $admin_inheritors =
Bugzilla::User->flatten_group_membership($admin_group->id);
Bugzilla::Group->flatten_group_membership($admin_group->id);
my $admin_group_ids = join(',', @$admin_inheritors);
my ($admin_count) = $dbh->selectrow_array(

View File

@ -91,6 +91,22 @@ sub update_fielddefs_definition {
}
}
$dbh->bz_add_column('fielddefs', 'visibility_field_id', {TYPE => 'INT3'});
$dbh->bz_add_column('fielddefs', 'visibility_value_id', {TYPE => 'INT2'});
$dbh->bz_add_column('fielddefs', 'value_field_id', {TYPE => 'INT3'});
$dbh->bz_add_index('fielddefs', 'fielddefs_value_field_id_idx',
['value_field_id']);
# Bug 344878
if (!$dbh->bz_column_info('fielddefs', 'buglist')) {
$dbh->bz_add_column('fielddefs', 'buglist',
{TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'});
# Set non-multiselect custom fields as valid buglist fields
# Note that default fields will be handled in Field.pm
$dbh->do('UPDATE fielddefs SET buglist = 1 WHERE custom = 1 AND type != ' . FIELD_TYPE_MULTI_SELECT);
}
# Remember, this is not the function for adding general table changes.
# That is below. Add new changes to the fielddefs table above this
# comment.
@ -531,6 +547,22 @@ sub update_table_definitions {
$dbh->bz_alter_column('series', 'query',
{ TYPE => 'MEDIUMTEXT', NOTNULL => 1 });
# Add FK to multi select field tables
_add_foreign_keys_to_multiselects();
# 2008-07-28 tfu@redhat.com - Bug 431669
$dbh->bz_alter_column('group_control_map', 'product_id',
{ TYPE => 'INT2', NOTNULL => 1 });
# 2008-09-07 LpSolit@gmail.com - Bug 452893
_fix_illegal_flag_modification_dates();
_add_visiblity_value_to_value_tables();
# 2009-03-02 arbingersys@gmail.com - Bug 423613
$dbh->bz_add_index('profiles', 'profiles_extern_id_idx',
{TYPE => 'UNIQUE', FIELDS => [qw(extern_id)]});
################################################################
# New --TABLE-- changes should go *** A B O V E *** this point #
################################################################
@ -2986,7 +3018,25 @@ sub _initialize_workflow {
# Make sure the bug status used by the 'duplicate_or_move_bug_status'
# parameter has all the required transitions set.
Bugzilla::Status::add_missing_bug_status_transitions();
my $dup_status = Bugzilla->params->{'duplicate_or_move_bug_status'};
my $status_id = $dbh->selectrow_array(
'SELECT id FROM bug_status WHERE value = ?', undef, $dup_status);
# There's a minor chance that this status isn't in the DB.
$status_id || return;
my $missing_statuses = $dbh->selectcol_arrayref(
'SELECT id FROM bug_status
LEFT JOIN status_workflow ON old_status = id
AND new_status = ?
WHERE old_status IS NULL', undef, $status_id);
my $sth = $dbh->prepare('INSERT INTO status_workflow
(old_status, new_status) VALUES (?, ?)');
foreach my $old_status_id (@$missing_statuses) {
next if ($old_status_id == $status_id);
$sth->execute($old_status_id, $status_id);
}
}
sub _make_lang_setting_dynamic {
@ -3085,6 +3135,25 @@ sub _check_content_length {
}
}
sub _add_foreign_keys_to_multiselects {
my $dbh = Bugzilla->dbh;
my $names = $dbh->selectcol_arrayref(
'SELECT name
FROM fielddefs
WHERE type = ' . FIELD_TYPE_MULTI_SELECT);
foreach my $name (@$names) {
$dbh->bz_add_fk("bug_$name", "bug_id", {TABLE => 'bugs',
COLUMN => 'bug_id',
DELETE => 'CASCADE',});
$dbh->bz_add_fk("bug_$name", "value", {TABLE => $name,
COLUMN => 'value',
DELETE => 'RESTRICT',});
}
}
sub _populate_bugs_fulltext
{
my $dbh = Bugzilla->dbh;
@ -3140,6 +3209,29 @@ sub _populate_bugs_fulltext
}
}
sub _fix_illegal_flag_modification_dates {
my $dbh = Bugzilla->dbh;
my $rows = $dbh->do('UPDATE flags SET modification_date = creation_date
WHERE modification_date < creation_date');
# If no rows are affected, $dbh->do returns 0E0 instead of 0.
print "$rows flags had an illegal modification date. Fixed!\n" if ($rows =~ /^\d+$/);
}
sub _add_visiblity_value_to_value_tables {
my $dbh = Bugzilla->dbh;
my @standard_fields =
qw(bug_status resolution priority bug_severity op_sys rep_platform);
my $custom_fields = $dbh->selectcol_arrayref(
'SELECT name FROM fielddefs WHERE custom = 1 AND type IN(?,?)',
undef, FIELD_TYPE_SINGLE_SELECT, FIELD_TYPE_MULTI_SELECT);
foreach my $field (@standard_fields, @$custom_fields) {
$dbh->bz_add_column($field, 'visibility_value_id', {TYPE => 'INT2'});
$dbh->bz_add_index($field, "${field}_visibility_value_id_idx",
['visibility_value_id']);
}
}
1;
__END__

View File

@ -64,6 +64,7 @@ sub FILESYSTEM {
my $libdir = bz_locations()->{'libpath'};
my $extlib = bz_locations()->{'ext_libpath'};
my $skinsdir = bz_locations()->{'skinsdir'};
my $localconfig = bz_locations()->{'localconfig'};
my $ws_group = Bugzilla->localconfig->{'webservergroup'};
@ -116,8 +117,11 @@ sub FILESYSTEM {
'customfield.pl' => { perms => $owner_executable },
'email_in.pl' => { perms => $ws_executable },
'sanitycheck.pl' => { perms => $ws_executable },
'jobqueue.pl' => { perms => $owner_executable },
'install-module.pl' => { perms => $owner_executable },
"$localconfig.old" => { perms => $owner_readable },
'docs/makedocs.pl' => { perms => $owner_executable },
'docs/style.css' => { perms => $ws_readable },
'docs/*/rel_notes.txt' => { perms => $ws_readable },
@ -294,12 +298,8 @@ EOT
# It's harmless if it isn't accessible...
"$datadir/.htaccess" => { perms => $ws_readable, contents => <<EOT
# Nothing in this directory is retrievable unless overridden by an .htaccess
# in a subdirectory; the only exception is duplicates.rdf, which is used by
# duplicates.xul and must be accessible from the web server
# in a subdirectory.
deny from all
<Files duplicates.rdf>
allow from all
</Files>
EOT
@ -378,6 +378,11 @@ EOT
unlink "$datadir/versioncache";
}
if (-e "$datadir/duplicates.rdf") {
print "Removing duplicates.rdf...\n";
unlink "$datadir/duplicates.rdf";
unlink "$datadir/duplicates-old.rdf";
}
}
# A simple helper for creating "empty" CSS files.

View File

@ -199,13 +199,6 @@ EOT
},
);
use constant OLD_LOCALCONFIG_VARS => qw(
mysqlpath
contenttypes
pages
severities platforms opsys priorities
);
sub read_localconfig {
my ($include_deprecated) = @_;
my $filename = bz_locations()->{'localconfig'};
@ -233,9 +226,27 @@ Please fix the error in your 'localconfig' file. Alternately, rename your
EOT
}
my @vars = map($_->{name}, LOCALCONFIG_VARS);
push(@vars, OLD_LOCALCONFIG_VARS) if $include_deprecated;
foreach my $var (@vars) {
my @read_symbols;
if ($include_deprecated) {
# First we have to get the whole symbol table
my $safe_root = $s->root;
my %safe_package;
{ no strict 'refs'; %safe_package = %{$safe_root . "::"}; }
# And now we read the contents of every var in the symbol table.
# However:
# * We only include symbols that start with an alphanumeric
# character. This excludes symbols like "_<./localconfig"
# that show up in some perls.
# * We ignore the INC symbol, which exists in every package.
# * Perl 5.10 imports a lot of random symbols that all
# contain "::", and we want to ignore those.
@read_symbols = grep { /^[A-Za-z0-1]/ and !/^INC$/ and !/::/ }
(keys %safe_package);
}
else {
@read_symbols = map($_->{name}, LOCALCONFIG_VARS);
}
foreach my $var (@read_symbols) {
my $glob = $s->varglob($var);
# We can't get the type of a variable out of a Safe automatically.
# We can only get the glob itself. So we figure out its type this
@ -302,11 +313,6 @@ sub update_localconfig {
}
}
my @old_vars;
foreach my $name (OLD_LOCALCONFIG_VARS) {
push(@old_vars, $name) if defined $localconfig->{$name};
}
if (!$localconfig->{'interdiffbin'} && $output) {
print <<EOT
@ -319,30 +325,41 @@ as well), you should install patchutils from:
EOT
}
my @old_vars;
foreach my $var (keys %$localconfig) {
push(@old_vars, $var) if !grep($_->{name} eq $var, LOCALCONFIG_VARS);
}
my $filename = bz_locations->{'localconfig'};
# Move any custom or old variables into a separate file.
if (scalar @old_vars) {
my $filename_old = "$filename.old";
open(my $old_file, ">>$filename_old") || die "$filename_old: $!";
local $Data::Dumper::Purity = 1;
foreach my $var (@old_vars) {
print $old_file Data::Dumper->Dump([$localconfig->{$var}],
["*$var"]) . "\n\n";
}
close $old_file;
my $oldstuff = join(', ', @old_vars);
print <<EOT
The following variables are no longer used in $filename, and
should be removed: $oldstuff
have been moved to $filename_old: $oldstuff
EOT
}
if (scalar @new_vars) {
my $filename = bz_locations->{'localconfig'};
my $fh = new IO::File($filename, '>>') || die "$filename: $!";
$fh->seek(0, SEEK_END);
# Re-write localconfig
open(my $fh, ">$filename") || die "$filename: $!";
foreach my $var (LOCALCONFIG_VARS) {
if (grep($_ eq $var->{name}, @new_vars)) {
print $fh "\n", $var->{desc},
Data::Dumper->Dump([$localconfig->{$var->{name}}],
["*$var->{name}"]);
}
}
if (@new_vars) {
my $newstuff = join(', ', @new_vars);
print <<EOT;
@ -417,31 +434,46 @@ variables defined in localconfig, it will print out a warning.
=over
=item C<read_localconfig($include_deprecated)>
=item C<read_localconfig>
Description: Reads the localconfig file and returns all valid
values in a hashref.
=over
Params: C<$include_deprecated> - C<true> if you want the returned
hashref to also include variables listed in
C<OLD_LOCALCONFIG_VARS>, if they exist. Generally
this is only for use by C<update_localconfig>.
=item B<Description>
Returns: A hashref of the localconfig variables. If an array
is defined, it will be an arrayref in the returned hash. If a
hash is defined, it will be a hashref in the returned hash.
Only includes variables specified in C<LOCALCONFIG_VARS>
(and C<OLD_LOCALCONFIG_VARS> if C<$include_deprecated> is
specified).
Reads the localconfig file and returns all valid values in a hashref.
=item C<update_localconfig({ output =E<gt> 1 })>
=item B<Params>
=over
=item C<$include_deprecated>
C<true> if you want the returned hashref to include *any* variable
currently defined in localconfig, even if it doesn't exist in
C<LOCALCONFIG_VARS>. Generally this is is only for use
by L</update_localconfig>.
=back
=item B<Returns>
A hashref of the localconfig variables. If an array is defined in
localconfig, it will be an arrayref in the returned hash. If a
hash is defined, it will be a hashref in the returned hash.
Only includes variables specified in C<LOCALCONFIG_VARS>, unless
C<$include_deprecated> is true.
=back
=item C<update_localconfig>
Description: Adds any new variables to localconfig that aren't
currently defined there. Also optionally prints out
a message about vars that *should* be there and aren't.
Exits the program if it adds any new vars.
Params: C<output> - C<true> if the function should display informational
Params: C<$output> - C<true> if the function should display informational
output and warnings. It will always display errors or
any message which would cause program execution to halt.

View File

@ -25,6 +25,7 @@ package Bugzilla::Install::Requirements;
use strict;
use Bugzilla::Constants;
use Bugzilla::Install::Util qw(vers_cmp install_string);
use List::Util qw(max);
use Safe;
@ -40,7 +41,9 @@ our @EXPORT = qw(
install_command
);
use Bugzilla::Constants;
# This is how many *'s are in the top of each "box" message printed
# by checksetup.pl.
use constant TABLE_WIDTH => 71;
# The below two constants are subroutines so that they can implement
# a hook. Other than that they are actually constants.
@ -64,11 +67,31 @@ sub REQUIRED_MODULES {
# Require CGI 3.21 for -httponly support, see bug 368502.
version => (vers_cmp($perl_ver, '5.10') > -1) ? '3.33' : '3.21'
},
{
package => 'Digest-SHA',
module => 'Digest::SHA',
version => 0
},
{
package => 'TimeDate',
module => 'Date::Format',
version => '2.21'
},
# 0.28 fixed some important bugs in DateTime.
{
package => 'DateTime',
module => 'DateTime',
version => '0.28'
},
# 0.79 is required to work on Windows Vista and Windows Server 2008.
# As correctly detecting the flavor of Windows is not easy,
# we require this version for all Windows installations.
# 0.71 fixes a major bug affecting all platforms.
{
package => 'DateTime-TimeZone',
module => 'DateTime::TimeZone',
version => ON_WINDOWS ? '0.79' : '0.71'
},
{
package => 'PathTools',
module => 'File::Spec',
@ -79,15 +102,18 @@ sub REQUIRED_MODULES {
module => 'DBI',
version => '1.41'
},
# 2.22 fixes various problems related to UTF8 strings in hash keys,
# as well as line endings on Windows.
{
package => 'Template-Toolkit',
module => 'Template',
version => '2.15'
version => '2.22'
},
{
package => 'Email-Send',
module => 'Email::Send',
version => ON_WINDOWS ? '2.16' : '2.00'
version => ON_WINDOWS ? '2.16' : '2.00',
blacklist => ['^2\.196$']
},
{
package => 'Email-MIME',
@ -105,6 +131,11 @@ sub REQUIRED_MODULES {
module => 'Email::MIME::Modifier',
version => '1.442'
},
{
package => 'URI',
module => 'URI',
version => 0
},
);
my $all_modules = _get_extension_requirements(
@ -231,6 +262,20 @@ sub OPTIONAL_MODULES {
feature => 'Inbound Email'
},
# Mail Queueing
{
package => 'TheSchwartz',
module => 'TheSchwartz',
version => 0,
feature => 'Mail Queueing',
},
{
package => 'Daemon-Generic',
module => 'Daemon::Generic',
version => 0,
feature => 'Mail Queueing',
},
# mod_perl
{
package => 'mod_perl',
@ -332,143 +377,90 @@ sub _get_activestate_build_id {
sub print_module_instructions {
my ($check_results, $output) = @_;
# We only print these notes if we have to.
if ((!$output && @{$check_results->{missing}})
|| ($output && $check_results->{any_missing}))
{
if (ON_WINDOWS) {
# First we print the long explanatory messages.
print "\n* NOTE: You must run any commands listed below as "
. ROOT_USER . ".\n\n";
my $perl_ver = sprintf('%vd', $^V);
# URL when running Perl 5.8.x.
my $url_to_theory58S = 'http://theoryx5.uwinnipeg.ca/ppms';
my $repo_up_cmd =
'* *';
# Packages for Perl 5.10 are not compatible with Perl 5.8.
if (vers_cmp($perl_ver, '5.10') > -1) {
$url_to_theory58S = 'http://cpan.uwinnipeg.ca/PPMPackages/10xx/';
}
# ActivePerl older than revision 819 require an additional command.
if (_get_activestate_build_id() < 819) {
$repo_up_cmd = <<EOT;
* *
* Then you have to do (also as an Administrator): *
* *
* ppm repo up theory58S *
* *
* Do that last command over and over until you see "theory58S" at the *
* top of the displayed list. *
EOT
}
print <<EOT;
***********************************************************************
* Note For Windows Users *
***********************************************************************
* In order to install the modules listed below, you first have to run *
* the following command as an Administrator: *
* *
* ppm repo add theory58S $url_to_theory58S
$repo_up_cmd
***********************************************************************
EOT
}
}
# Required Modules
if (my @missing = @{$check_results->{missing}}) {
print <<EOT;
***********************************************************************
* REQUIRED MODULES *
***********************************************************************
* Bugzilla requires you to install some Perl modules which are either *
* missing from your system, or the version on your system is too old. *
* *
* The latest versions of each module can be installed by running the *
* commands below. *
***********************************************************************
EOT
print "COMMANDS:\n\n";
foreach my $package (@missing) {
my $command = install_command($package);
print " $command\n";
}
print "\n";
if (scalar @{$check_results->{missing}}) {
print install_string('modules_message_required');
}
if (!$check_results->{one_dbd}) {
print <<EOT;
***********************************************************************
* DATABASE ACCESS *
***********************************************************************
* In order to access your database, Bugzilla requires that the *
* correct "DBD" module be installed for the database that you are *
* running. *
* *
* Pick and run the correct command below for the database that you *
* plan to use with Bugzilla. *
***********************************************************************
COMMANDS:
EOT
my %db_modules = %{DB_MODULE()};
foreach my $db (keys %db_modules) {
my $command = install_command($db_modules{$db}->{dbd});
printf "%10s: \%s\n", $db_modules{$db}->{name}, $command;
print ' ' x 12 . "Minimum version required: "
. $db_modules{$db}->{dbd}->{version} . "\n";
}
print "\n";
print install_string('modules_message_db');
}
return unless $output;
if (my @missing = @{$check_results->{optional}}) {
print <<EOT;
**********************************************************************
* OPTIONAL MODULES *
**********************************************************************
* Certain Perl modules are not required by Bugzilla, but by *
* installing the latest version you gain access to additional *
* features. *
* *
* The optional modules you do not have installed are listed below, *
* with the name of the feature they enable. If you want to install *
* one of these modules, just run the appropriate command in the *
* "COMMANDS TO INSTALL" section. *
**********************************************************************
EOT
if (my @missing = @{$check_results->{optional}} and $output) {
print install_string('modules_message_optional');
# Now we have to determine how large the table cols will be.
my $longest_name = max(map(length($_->{package}), @missing));
# The first column header is at least 11 characters long.
$longest_name = 11 if $longest_name < 11;
# The table is 71 characters long. There are seven mandatory
# The table is TABLE_WIDTH characters long. There are seven mandatory
# characters (* and space) in the string. So, we have a total
# of 64 characters to work with.
my $remaining_space = 64 - $longest_name;
print '*' x 71 . "\n";
# of TABLE_WIDTH - 7 characters to work with.
my $remaining_space = (TABLE_WIDTH - 7) - $longest_name;
print '*' x TABLE_WIDTH . "\n";
printf "* \%${longest_name}s * %-${remaining_space}s *\n",
'MODULE NAME', 'ENABLES FEATURE(S)';
print '*' x 71 . "\n";
print '*' x TABLE_WIDTH . "\n";
foreach my $package (@missing) {
printf "* \%${longest_name}s * %-${remaining_space}s *\n",
$package->{package}, $package->{feature};
}
print '*' x 71 . "\n";
}
print "COMMANDS TO INSTALL:\n\n";
# We only print the PPM repository note if we have to.
if ((!$output && @{$check_results->{missing}})
|| ($output && $check_results->{any_missing}))
{
if (ON_WINDOWS) {
my $perl_ver = sprintf('%vd', $^V);
# URL when running Perl 5.8.x.
my $url_to_theory58S = 'http://theoryx5.uwinnipeg.ca/ppms';
# Packages for Perl 5.10 are not compatible with Perl 5.8.
if (vers_cmp($perl_ver, '5.10') > -1) {
$url_to_theory58S = 'http://cpan.uwinnipeg.ca/PPMPackages/10xx/';
}
print install_string('ppm_repo_add',
{ theory_url => $url_to_theory58S });
# ActivePerls older than revision 819 require an additional command.
if (_get_activestate_build_id() < 819) {
print install_string('ppm_repo_up');
}
}
# If any output was required, we want to close the "table"
print "*" x TABLE_WIDTH . "\n";
}
# And now we print the actual installation commands.
if (my @missing = @{$check_results->{optional}} and $output) {
print install_string('commands_optional') . "\n\n";
foreach my $module (@missing) {
my $command = install_command($module);
printf "%15s: $command\n", $module->{package};
}
print "\n";
}
if (!$check_results->{one_dbd}) {
print install_string('commands_dbd') . "\n";
my %db_modules = %{DB_MODULE()};
foreach my $db (keys %db_modules) {
my $command = install_command($db_modules{$db}->{dbd});
printf "%10s: \%s\n", $db_modules{$db}->{name}, $command;
}
print "\n";
}
if (my @missing = @{$check_results->{missing}}) {
print install_string('commands_required') . "\n";
foreach my $package (@missing) {
my $command = install_command($package);
print " $command\n";
}
}
if ($output && $check_results->{any_missing} && !ON_WINDOWS) {

View File

@ -31,6 +31,7 @@ use Bugzilla::Constants;
use File::Basename;
use POSIX qw(setlocale LC_CTYPE);
use Safe;
use Scalar::Util qw(tainted);
use base qw(Exporter);
our @EXPORT_OK = qw(
@ -109,7 +110,7 @@ sub install_string {
foreach my $key (@replace_keys) {
my $replacement = $vars->{$key};
die "'$key' in '$string_id' is tainted: '$replacement'"
if is_tainted($replacement);
if tainted($replacement);
# We don't want people to start getting clever and inserting
# ##variable## into their values. So we check if any other
# key is listed in the *replacement* string, before doing
@ -354,10 +355,6 @@ sub trick_taint {
return (defined($_[0]));
}
sub is_tainted {
return not eval { my $foo = join('',@_), kill 0; 1; };
}
__END__
=head1 NAME

57
Bugzilla/Job/Mailer.pm Normal file
View File

@ -0,0 +1,57 @@
# -*- 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 Mozilla Corporation.
# Portions created by the Initial Developer are Copyright (C) 2008
# Mozilla Corporation. All Rights Reserved.
#
# Contributor(s):
# Mark Smith <mark@mozilla.com>
# Max Kanat-Alexander <mkanat@bugzilla.org>
package Bugzilla::Job::Mailer;
use strict;
use Bugzilla::Mailer;
BEGIN { eval "use base qw(TheSchwartz::Worker)"; }
# The longest we expect a job to possibly take, in seconds.
use constant grab_for => 300;
# We don't want email to fail permanently very easily. Retry for 30 days.
use constant max_retries => 725;
# The first few retries happen quickly, but after that we wait an hour for
# each retry.
sub retry_delay {
my $num_retries = shift;
if ($num_retries < 5) {
return (10, 30, 60, 300, 600)[$num_retries];
}
# One hour
return 60*60;
}
sub work {
my ($class, $job) = @_;
my $msg = $job->arg->{msg};
my $success = eval { MessageToMTA($msg, 1); 1; };
if (!$success) {
$job->failed($@);
undef $@;
}
else {
$job->completed;
}
}
1;

108
Bugzilla/JobQueue.pm Normal file
View File

@ -0,0 +1,108 @@
# -*- 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 Mozilla Corporation.
# Portions created by the Initial Developer are Copyright (C) 2008
# Mozilla Corporation. All Rights Reserved.
#
# Contributor(s):
# Mark Smith <mark@mozilla.com>
# Max Kanat-Alexander <mkanat@bugzilla.org>
package Bugzilla::JobQueue;
use strict;
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Install::Util qw(install_string);
BEGIN { eval "use base qw(TheSchwartz)"; }
# This maps job names for Bugzilla::JobQueue to the appropriate modules.
# If you add new types of jobs, you should add a mapping here.
use constant JOB_MAP => {
send_mail => 'Bugzilla::Job::Mailer',
};
sub new {
my $class = shift;
if (!eval { require TheSchwartz; }) {
ThrowCodeError('jobqueue_not_configured');
}
my $lc = Bugzilla->localconfig;
my $self = $class->SUPER::new(
databases => [{
dsn => Bugzilla->dbh->{private_bz_dsn},
user => $lc->{db_user},
pass => $lc->{db_pass},
prefix => 'ts_',
}],
);
return $self;
}
# A way to get access to the underlying databases directly.
sub bz_databases {
my $self = shift;
my @hashes = keys %{ $self->{databases} };
return map { $self->driver_for($_) } @hashes;
}
# inserts a job into the queue to be processed and returns immediately
sub insert {
my $self = shift;
my $job = shift;
my $mapped_job = JOB_MAP->{$job};
ThrowCodeError('jobqueue_no_job_mapping', { job => $job })
if !$mapped_job;
unshift(@_, $mapped_job);
my $retval = $self->SUPER::insert(@_);
# XXX Need to get an error message here if insert fails, but
# I don't see any way to do that in TheSchwartz.
ThrowCodeError('jobqueue_insert_failed', { job => $job, errmsg => $@ })
if !$retval;
return $retval;
}
1;
__END__
=head1 NAME
Bugzilla::JobQueue - Interface between Bugzilla and TheSchwartz.
=head1 SYNOPSIS
use Bugzilla;
my $obj = Bugzilla->job_queue();
$obj->insert('send_mail', { msg => $message });
=head1 DESCRIPTION
Certain tasks should be done asyncronously. The job queue system allows
Bugzilla to use some sort of service to schedule jobs to happen asyncronously.
=head2 Inserting a Job
See the synopsis above for an easy to follow example on how to insert a
job into the queue. Give it a name and some arguments and the job will
be sent away to be done later.

View File

@ -0,0 +1,99 @@
# -*- 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 Mozilla Corporation.
# Portions created by the Initial Developer are Copyright (C) 2008
# Mozilla Corporation. All Rights Reserved.
#
# Contributor(s):
# Mark Smith <mark@mozilla.com>
# Max Kanat-Alexander <mkanat@bugzilla.org>
# XXX In order to support Windows, we have to make gd_redirect_output
# use Log4Perl or something instead of calling "logger". We probably
# also need to use Win32::Daemon or something like that to daemonize.
package Bugzilla::JobQueue::Runner;
use strict;
use File::Basename;
use Pod::Usage;
use Bugzilla::Constants;
use Bugzilla::JobQueue;
use Bugzilla::Util qw(get_text);
BEGIN { eval "use base qw(Daemon::Generic)"; }
# Required because of a bug in Daemon::Generic where it won't use the
# "version" key from DAEMON_CONFIG.
our $VERSION = BUGZILLA_VERSION;
use constant DAEMON_CONFIG => (
progname => basename($0),
pidfile => bz_locations()->{datadir} . '/' . basename($0) . '.pid',
version => BUGZILLA_VERSION,
);
sub gd_preconfig {
return DAEMON_CONFIG;
}
sub gd_usage {
pod2usage({ -verbose => 0, -exitval => 'NOEXIT' });
return 0
}
sub gd_check {
my $self = shift;
# Get a count of all the jobs currently in the queue.
my $jq = Bugzilla->job_queue();
my @dbs = $jq->bz_databases();
my $count = 0;
foreach my $driver (@dbs) {
$count += $driver->select_one('SELECT COUNT(*) FROM ts_job', []);
}
print get_text('job_queue_depth', { count => $count }) . "\n";
}
sub gd_run {
my $self = shift;
my $jq = Bugzilla->job_queue();
$jq->set_verbose($self->{debug});
foreach my $module (values %{ Bugzilla::JobQueue::JOB_MAP() }) {
eval "use $module";
$jq->can_do($module);
}
$jq->work;
}
1;
__END__
=head1 NAME
Bugzilla::JobQueue::Runner - A class representing the daemon that runs the
job queue.
=head1 SYNOPSIS
use Bugzilla::JobQueue::Runner;
Bugzilla::JobQueue::Runner->new();
=head1 DESCRIPTION
This is a subclass of L<Daemon::Generic> that is used by L<jobqueue>
to run the Bugzilla job queue.

View File

@ -39,6 +39,7 @@ use base qw(Exporter);
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Hook;
use Bugzilla::Util;
use Date::Format qw(time2str);
@ -52,10 +53,15 @@ use Email::MIME::Modifier;
use Email::Send;
sub MessageToMTA {
my ($msg) = (@_);
my ($msg, $send_now) = (@_);
my $method = Bugzilla->params->{'mail_delivery_method'};
return if $method eq 'None';
if (Bugzilla->params->{'use_mailer_queue'} and !$send_now) {
Bugzilla->job_queue->insert('send_mail', { msg => $msg });
return;
}
my $email;
if (ref $msg) {
$email = $msg;
@ -71,6 +77,17 @@ sub MessageToMTA {
$email = new Email::MIME($msg);
}
# We add this header to uniquely identify all email that we
# send as coming from this Bugzilla installation.
#
# We don't use correct_urlbase, because we want this URL to
# *always* be the same for this Bugzilla, in every email,
# and some emails we send when we're logged out (in which case
# some emails might get urlbase while the logged-in emails might
# get sslbase). Also, we want this to stay the same even if
# the admin changes the "ssl" parameter.
$email->header_set('X-Bugzilla-URL', Bugzilla->params->{'urlbase'});
# We add this header to mark the mail as "auto-generated" and
# thus to hopefully avoid auto replies.
$email->header_set('Auto-Submitted', 'auto-generated');
@ -157,6 +174,8 @@ sub MessageToMTA {
Debug => Bugzilla->params->{'smtp_debug'};
}
Bugzilla::Hook::process('mailer-before_send', { email => $email });
if ($method eq "Test") {
my $filename = bz_locations()->{'datadir'} . '/mailer.testfile';
open TESTFILE, '>>', $filename;

View File

@ -63,7 +63,10 @@ sub _init {
my $name_field = $class->NAME_FIELD;
my $id_field = $class->ID_FIELD;
my $id = $param unless (ref $param eq 'HASH');
my $id = $param;
if (ref $param eq 'HASH') {
$id = $param->{id};
}
my $object;
if (defined $id) {
@ -114,12 +117,10 @@ sub check {
if (!ref $param) {
$param = { name => $param };
}
# Don't allow empty names.
if (exists $param->{name}) {
$param->{name} = trim($param->{name});
$param->{name} || ThrowUserError('object_name_not_specified',
{ class => $class });
}
# Don't allow empty names or ids.
my $check_param = exists $param->{id} ? $param->{id} : $param->{name};
$check_param = trim($check_param);
$check_param || ThrowUserError('object_not_specified', { class => $class });
my $obj = $class->new($param)
|| ThrowUserError('object_does_not_exist', {%$param, class => $class});
return $obj;
@ -155,9 +156,30 @@ sub match {
return [$class->get_all] if !$criteria;
my (@terms, @values);
my (@terms, @values, $postamble);
foreach my $field (keys %$criteria) {
my $value = $criteria->{$field};
# allow for LIMIT and OFFSET expressions via the criteria.
next if $field eq 'OFFSET';
if ( $field eq 'LIMIT' ) {
next unless defined $value;
$postamble = $dbh->sql_limit( $value, $criteria->{OFFSET} );
next;
}
elsif ( $field eq 'WHERE' ) {
# the WHERE value is a hashref where the keys are
# "column_name operator ?" and values are the placeholder's
# value (either a scalar or an array of values).
foreach my $k (keys %$value) {
push(@terms, $k);
my @this_value = ref($value->{$k}) ? @{ $value->{$k} }
: ($value->{$k});
push(@values, @this_value);
}
next;
}
if (ref $value eq 'ARRAY') {
# IN () is invalid SQL, and if we have an empty list
# to match against, we're just returning an empty
@ -180,12 +202,12 @@ sub match {
}
}
my $where = join(' AND ', @terms);
return $class->_do_list_select($where, \@values);
my $where = join(' AND ', @terms) if scalar @terms;
return $class->_do_list_select($where, \@values, $postamble);
}
sub _do_list_select {
my ($class, $where, $values) = @_;
my ($class, $where, $values, $postamble) = @_;
my $table = $class->DB_TABLE;
my $cols = join(',', $class->DB_COLUMNS);
my $order = $class->LIST_ORDER;
@ -196,6 +218,8 @@ sub _do_list_select {
}
$sql .= " ORDER BY $order";
$sql .= " $postamble" if $postamble;
my $dbh = Bugzilla->dbh;
my $objects = $dbh->selectall_arrayref($sql, {Slice=>{}}, @$values);
bless ($_, $class) foreach @$objects;
@ -237,6 +261,14 @@ sub set {
$self->{$field} = $value;
}
sub set_all {
my ($self, $params) = @_;
foreach my $key (keys %$params) {
my $method = "set_$key";
$self->$method($params->{$key});
}
}
sub update {
my $self = shift;
@ -280,9 +312,22 @@ sub update {
$dbh->bz_commit_transaction();
if (wantarray) {
return (\%changes, $old_self);
}
return \%changes;
}
sub remove_from_db {
my $self = shift;
my $table = $self->DB_TABLE;
my $id_field = $self->ID_FIELD;
Bugzilla->dbh->do("DELETE FROM $table WHERE $id_field = ?",
undef, $self->id);
undef $self;
}
###############################
#### Subroutines ######
###############################
@ -511,7 +556,9 @@ as the value in the L</ID_FIELD> column).
If you pass in a hashref, you can pass a C<name> key. The
value of the C<name> key is the case-insensitive name of the object
(from L</NAME_FIELD>) in the DB.
(from L</NAME_FIELD>) in the DB. You can also pass in an C<id> key
which will be interpreted as the id of the object you want (overriding the
C<name> key).
B<Additional Parameters Available for Subclasses>
@ -601,6 +648,26 @@ There are two special values, the constants C<NULL> and C<NOT_NULL>,
which means "give me objects where this field is NULL or NOT NULL,
respectively."
In addition to the column keys, there are a few special keys that
can be used to rig the underlying database queries. These are
C<LIMIT>, C<OFFSET>, and C<WHERE>.
The value for the C<LIMIT> key is expected to be an integer defining
the number of objects to return, while the value for C<OFFSET> defines
the position, relative to the number of objects the query would normally
return, at which to begin the result set. If C<OFFSET> is defined without
a corresponding C<LIMIT> it is silently ignored.
The C<WHERE> key provides a mechanism for adding arbitrary WHERE
clauses to the underlying query. Its value is expected to a hash
reference whose keys are the columns, operators and placeholders, and the
values are the placeholders' bind value. For example:
WHERE => { 'some_column >= ?' => $some_value }
would constrain the query to only those objects in the table whose
'some_column' column has a value greater than or equal to $some_value.
If you don't specify any criteria, calling this function is the same
as doing C<[$class-E<gt>get_all]>.
@ -698,6 +765,8 @@ updated, and they will only be updated if their values have changed.
=item B<Returns>
B<In scalar context:>
A hashref showing what changed during the update. The keys are the column
names from L</UPDATE_COLUMNS>. If a field was not changed, it will not be
in the hash at all. If the field was changed, the key will point to an arrayref.
@ -706,14 +775,27 @@ will be the new value.
If there were no changes, we return a reference to an empty hash.
=back
B<In array context:>
Returns a list, where the first item is the above hashref. The second item
is the object as it was in the database before update() was called. (This
is mostly useful to subclasses of C<Bugzilla::Object> that are implementing
C<update>.)
=back
=head2 Subclass Helpers
=item C<remove_from_db>
These functions are intended only for use by subclasses. If
you call them from anywhere else, they will throw a C<CodeError>.
Removes this object from the database. Will throw an error if you can't
remove it for some reason. The object will then be destroyed, as it is
not safe to use the object after it has been removed from the database.
=back
=head2 Mutators
These are used for updating the values in objects, before calling
C<update>.
=over
@ -734,9 +816,11 @@ C<set> will call it with C<($value, $field)> as arguments, after running
the validator for this particular field. C<_set_global_validator> does not
return anything.
See L</VALIDATORS> for more information.
B<NOTE>: This function is intended only for use by subclasses. If
you call it from anywhere else, it will throw a C<CodeError>.
=item B<Params>
=over
@ -752,6 +836,27 @@ be the same as the name of the field in L</VALIDATORS>, if it exists there.
=back
=item C<set_all>
=over
=item B<Description>
This is a convenience function which is simpler than calling many different
C<set_> functions in a row. You pass a hashref of parameters and it calls
C<set_$key($value)> for every item in the hashref.
=item B<Params>
Takes a hashref of the fields that need to be set, pointing to the value
that should be passed to the C<set_> function that is called.
=item B<Returns> (nothing)
=back
=back
=head2 Simple Validators

View File

@ -13,22 +13,27 @@
# The Original Code is the Bugzilla Bug Tracking System.
#
# Contributor(s): Tiago R. Mello <timello@async.com.br>
# Frédéric Buclin <LpSolit@gmail.com>
use strict;
package Bugzilla::Product;
use Bugzilla::Version;
use Bugzilla::Milestone;
use Bugzilla::Constants;
use Bugzilla::Util;
use Bugzilla::Group;
use Bugzilla::Error;
use Bugzilla::Group;
use Bugzilla::Version;
use Bugzilla::Milestone;
use Bugzilla::Field;
use Bugzilla::Status;
use Bugzilla::Install::Requirements;
use Bugzilla::Mailer;
use Bugzilla::Series;
use base qw(Bugzilla::Object);
# Currently, we only implement enough of the Bugzilla::Field::Choice
# interface to control the visibility of other fields.
use base qw(Bugzilla::Field::Choice);
use constant DEFAULT_CLASSIFICATION_ID => 1;
@ -37,24 +42,87 @@ use constant DEFAULT_CLASSIFICATION_ID => 1;
###############################
use constant DB_TABLE => 'products';
# Reset these back to the Bugzilla::Object defaults, instead of the
# Bugzilla::Field::Choice defaults.
use constant NAME_FIELD => 'name';
use constant LIST_ORDER => 'name';
use constant DB_COLUMNS => qw(
products.id
products.name
products.classification_id
products.description
products.milestoneurl
products.disallownew
products.votesperuser
products.maxvotesperbug
products.votestoconfirm
products.defaultmilestone
id
name
classification_id
description
milestoneurl
disallownew
votesperuser
maxvotesperbug
votestoconfirm
defaultmilestone
);
use constant REQUIRED_CREATE_FIELDS => qw(
name
description
version
);
use constant UPDATE_COLUMNS => qw(
name
description
defaultmilestone
milestoneurl
disallownew
votesperuser
maxvotesperbug
votestoconfirm
);
use constant VALIDATORS => {
classification => \&_check_classification,
name => \&_check_name,
description => \&_check_description,
version => \&_check_version,
defaultmilestone => \&_check_default_milestone,
milestoneurl => \&_check_milestone_url,
disallownew => \&Bugzilla::Object::check_boolean,
votesperuser => \&_check_votes_per_user,
maxvotesperbug => \&_check_votes_per_bug,
votestoconfirm => \&_check_votes_to_confirm,
create_series => \&Bugzilla::Object::check_boolean
};
###############################
#### Constructors #####
###############################
sub create {
my $class = shift;
my $dbh = Bugzilla->dbh;
$dbh->bz_start_transaction();
$class->check_required_create_fields(@_);
my $params = $class->run_create_validators(@_);
# Some fields do not exist in the DB as is.
$params->{classification_id} = delete $params->{classification};
my $version = delete $params->{version};
my $create_series = delete $params->{create_series};
my $product = $class->insert_create_data($params);
# Add the new version and milestone into the DB as valid values.
Bugzilla::Version::create($version, $product);
Bugzilla::Milestone->create({name => $params->{defaultmilestone}, product => $product});
# Create groups and series for the new product, if requested.
$product->_create_bug_group() if Bugzilla->params->{'makeproductgroups'};
$product->_create_series() if $create_series;
$dbh->bz_commit_transaction();
return $product;
}
# This is considerably faster than calling new_from_list three times
# for each product in the list, particularly with hundreds or thousands
# of products.
@ -78,10 +146,523 @@ sub preload {
}
}
sub update {
my $self = shift;
my $dbh = Bugzilla->dbh;
# Don't update the DB if something goes wrong below -> transaction.
$dbh->bz_start_transaction();
my ($changes, $old_self) = $self->SUPER::update(@_);
# We also have to fix votes.
my @msgs; # Will store emails to send to voters.
if ($changes->{maxvotesperbug} || $changes->{votesperuser} || $changes->{votestoconfirm}) {
# We cannot |use| these modules, due to dependency loops.
require Bugzilla::Bug;
import Bugzilla::Bug qw(RemoveVotes CheckIfVotedConfirmed);
require Bugzilla::User;
import Bugzilla::User qw(user_id_to_login);
# 1. too many votes for a single user on a single bug.
my @toomanyvotes_list = ();
if ($self->max_votes_per_bug < $self->votes_per_user) {
my $votes = $dbh->selectall_arrayref(
'SELECT votes.who, votes.bug_id
FROM votes
INNER JOIN bugs
ON bugs.bug_id = votes.bug_id
WHERE bugs.product_id = ?
AND votes.vote_count > ?',
undef, ($self->id, $self->max_votes_per_bug));
foreach my $vote (@$votes) {
my ($who, $id) = (@$vote);
# If some votes are removed, RemoveVotes() returns a list
# of messages to send to voters.
push(@msgs, RemoveVotes($id, $who, 'votes_too_many_per_bug'));
my $name = user_id_to_login($who);
push(@toomanyvotes_list, {id => $id, name => $name});
}
}
$changes->{'too_many_votes'} = \@toomanyvotes_list;
# 2. too many total votes for a single user.
# This part doesn't work in the general case because RemoveVotes
# doesn't enforce votesperuser (except per-bug when it's less
# than maxvotesperbug). See Bugzilla::Bug::RemoveVotes().
my $votes = $dbh->selectall_arrayref(
'SELECT votes.who, votes.vote_count
FROM votes
INNER JOIN bugs
ON bugs.bug_id = votes.bug_id
WHERE bugs.product_id = ?',
undef, $self->id);
my %counts;
foreach my $vote (@$votes) {
my ($who, $count) = @$vote;
if (!defined $counts{$who}) {
$counts{$who} = $count;
} else {
$counts{$who} += $count;
}
}
my @toomanytotalvotes_list = ();
foreach my $who (keys(%counts)) {
if ($counts{$who} > $self->votes_per_user) {
my $bug_ids = $dbh->selectcol_arrayref(
'SELECT votes.bug_id
FROM votes
INNER JOIN bugs
ON bugs.bug_id = votes.bug_id
WHERE bugs.product_id = ?
AND votes.who = ?',
undef, ($self->id, $who));
foreach my $bug_id (@$bug_ids) {
# RemoveVotes() returns a list of messages to send
# in case some voters had too many votes.
push(@msgs, RemoveVotes($bug_id, $who, 'votes_too_many_per_user'));
my $name = user_id_to_login($who);
push(@toomanytotalvotes_list, {id => $bug_id, name => $name});
}
}
}
$changes->{'too_many_total_votes'} = \@toomanytotalvotes_list;
# 3. enough votes to confirm
my $bug_list =
$dbh->selectcol_arrayref('SELECT bug_id FROM bugs WHERE product_id = ?
AND bug_status = ? AND votes >= ?',
undef, ($self->id, 'UNCONFIRMED', $self->votes_to_confirm));
my @updated_bugs = ();
foreach my $bug_id (@$bug_list) {
my $confirmed = CheckIfVotedConfirmed($bug_id);
push (@updated_bugs, $bug_id) if $confirmed;
}
$changes->{'confirmed_bugs'} = \@updated_bugs;
}
# Also update group settings.
if ($self->{check_group_controls}) {
require Bugzilla::Bug;
import Bugzilla::Bug qw(LogActivityEntry);
my $old_settings = $old_self->group_controls;
my $new_settings = $self->group_controls;
my $timestamp = $dbh->selectrow_array('SELECT NOW()');
foreach my $gid (keys %$new_settings) {
my $old_setting = $old_settings->{$gid} || {};
my $new_setting = $new_settings->{$gid};
# If all new settings are 0 for a given group, we delete the entry
# from group_control_map, so we have to track it here.
my $all_zero = 1;
my @fields;
my @values;
foreach my $field ('entry', 'membercontrol', 'othercontrol', 'canedit',
'editcomponents', 'editbugs', 'canconfirm')
{
my $old_value = $old_setting->{$field};
my $new_value = $new_setting->{$field};
$all_zero = 0 if $new_value;
next if (defined $old_value && $old_value == $new_value);
push(@fields, $field);
# The value has already been validated.
detaint_natural($new_value);
push(@values, $new_value);
}
# Is there anything to update?
next unless scalar @fields;
if ($all_zero) {
$dbh->do('DELETE FROM group_control_map
WHERE product_id = ? AND group_id = ?',
undef, $self->id, $gid);
}
else {
if (exists $old_setting->{group}) {
# There is already an entry in the DB.
my $set_fields = join(', ', map {"$_ = ?"} @fields);
$dbh->do("UPDATE group_control_map SET $set_fields
WHERE product_id = ? AND group_id = ?",
undef, (@values, $self->id, $gid));
}
else {
# No entry yet.
my $fields = join(', ', @fields);
# +2 because of the product and group IDs.
my $qmarks = join(',', ('?') x (scalar @fields + 2));
$dbh->do("INSERT INTO group_control_map (product_id, group_id, $fields)
VALUES ($qmarks)", undef, ($self->id, $gid, @values));
}
}
# If the group is mandatory, restrict all bugs to it.
if ($new_setting->{membercontrol} == CONTROLMAPMANDATORY) {
my $bug_ids =
$dbh->selectcol_arrayref('SELECT bugs.bug_id
FROM bugs
LEFT JOIN bug_group_map
ON bug_group_map.bug_id = bugs.bug_id
AND group_id = ?
WHERE product_id = ?
AND bug_group_map.bug_id IS NULL',
undef, $gid, $self->id);
if (scalar @$bug_ids) {
my $sth = $dbh->prepare('INSERT INTO bug_group_map (bug_id, group_id)
VALUES (?, ?)');
foreach my $bug_id (@$bug_ids) {
$sth->execute($bug_id, $gid);
# Add this change to the bug history.
LogActivityEntry($bug_id, 'bug_group', '',
$new_setting->{group}->name,
Bugzilla->user->id, $timestamp);
}
push(@{$changes->{'group_controls'}->{'now_mandatory'}},
{name => $new_setting->{group}->name,
bug_count => scalar @$bug_ids});
}
}
# If the group can no longer be used to restrict bugs, remove them.
elsif ($new_setting->{membercontrol} == CONTROLMAPNA) {
my $bug_ids =
$dbh->selectcol_arrayref('SELECT bugs.bug_id
FROM bugs
INNER JOIN bug_group_map
ON bug_group_map.bug_id = bugs.bug_id
WHERE product_id = ? AND group_id = ?',
undef, $self->id, $gid);
if (scalar @$bug_ids) {
$dbh->do('DELETE FROM bug_group_map WHERE group_id = ? AND ' .
$dbh->sql_in('bug_id', $bug_ids), undef, $gid);
# Add this change to the bug history.
foreach my $bug_id (@$bug_ids) {
LogActivityEntry($bug_id, 'bug_group',
$old_setting->{group}->name, '',
Bugzilla->user->id, $timestamp);
}
push(@{$changes->{'group_controls'}->{'now_na'}},
{name => $old_setting->{group}->name,
bug_count => scalar @$bug_ids});
}
}
}
}
$dbh->bz_commit_transaction();
# Changes have been committed.
delete $self->{check_group_controls};
# Now that changes have been committed, we can send emails to voters.
foreach my $msg (@msgs) {
MessageToMTA($msg);
}
return $changes;
}
sub remove_from_db {
my $self = shift;
my $user = Bugzilla->user;
my $dbh = Bugzilla->dbh;
$dbh->bz_start_transaction();
$self->_check_if_controller();
if ($self->bug_count) {
if (Bugzilla->params->{'allowbugdeletion'}) {
require Bugzilla::Bug;
foreach my $bug_id (@{$self->bug_ids}) {
# Note that we allow the user to delete bugs he can't see,
# which is okay, because he's deleting the whole Product.
my $bug = new Bugzilla::Bug($bug_id);
$bug->remove_from_db();
}
}
else {
ThrowUserError('product_has_bugs', { nb => $self->bug_count });
}
}
# XXX - This line can go away as soon as bug 427455 is fixed.
$dbh->do("DELETE FROM group_control_map WHERE product_id = ?", undef, $self->id);
$dbh->do("DELETE FROM products WHERE id = ?", undef, $self->id);
$dbh->bz_commit_transaction();
# We have to delete these internal variables, else we get
# the old lists of products and classifications again.
delete $user->{selectable_products};
delete $user->{selectable_classifications};
}
###############################
#### Validators ####
###############################
sub _check_classification {
my ($invocant, $classification_name) = @_;
my $classification_id = 1;
if (Bugzilla->params->{'useclassification'}) {
my $classification = Bugzilla::Classification->check($classification_name);
$classification_id = $classification->id;
}
return $classification_id;
}
sub _check_name {
my ($invocant, $name) = @_;
$name = trim($name);
$name || ThrowUserError('product_blank_name');
if (length($name) > MAX_PRODUCT_SIZE) {
ThrowUserError('product_name_too_long', {'name' => $name});
}
my $product = new Bugzilla::Product({name => $name});
if ($product && (!ref $invocant || $product->id != $invocant->id)) {
# Check for exact case sensitive match:
if ($product->name eq $name) {
ThrowUserError('product_name_already_in_use', {'product' => $product->name});
}
else {
ThrowUserError('product_name_diff_in_case', {'product' => $name,
'existing_product' => $product->name});
}
}
return $name;
}
sub _check_description {
my ($invocant, $description) = @_;
$description = trim($description);
$description || ThrowUserError('product_must_have_description');
return $description;
}
sub _check_version {
my ($invocant, $version) = @_;
$version = trim($version);
$version || ThrowUserError('product_must_have_version');
# We will check the version length when Bugzilla::Version->create will do it.
return $version;
}
sub _check_default_milestone {
my ($invocant, $milestone) = @_;
# Do nothing if target milestones are not in use.
unless (Bugzilla->params->{'usetargetmilestone'}) {
return (ref $invocant) ? $invocant->default_milestone : '---';
}
$milestone = trim($milestone);
if (ref $invocant) {
# The default milestone must be one of the existing milestones.
my $mil_obj = new Bugzilla::Milestone({name => $milestone, product => $invocant});
$mil_obj || ThrowUserError('product_must_define_defaultmilestone',
{product => $invocant->name,
milestone => $milestone});
}
else {
$milestone ||= '---';
}
return $milestone;
}
sub _check_milestone_url {
my ($invocant, $url) = @_;
# Do nothing if target milestones are not in use.
unless (Bugzilla->params->{'usetargetmilestone'}) {
return (ref $invocant) ? $invocant->milestone_url : '';
}
$url = trim($url || '');
return $url;
}
sub _check_votes_per_user {
return _check_votes(@_, 0);
}
sub _check_votes_per_bug {
return _check_votes(@_, 10000);
}
sub _check_votes_to_confirm {
return _check_votes(@_, 0);
}
# This subroutine is only used internally by other _check_votes_* validators.
sub _check_votes {
my ($invocant, $votes, $field, $default) = @_;
detaint_natural($votes);
# On product creation, if the number of votes is not a valid integer,
# we silently fall back to the given default value.
# If the product already exists and the change is illegal, we complain.
if (!defined $votes) {
if (ref $invocant) {
ThrowUserError('product_illegal_votes', {field => $field, votes => $_[1]});
}
else {
$votes = $default;
}
}
return $votes;
}
#####################################
# Implement Bugzilla::Field::Choice #
#####################################
sub field {
my $invocant = shift;
my $class = ref $invocant || $invocant;
my $cache = Bugzilla->request_cache;
$cache->{"field_$class"} ||= new Bugzilla::Field({ name => 'product' });
return $cache->{"field_$class"};
}
use constant is_default => 0;
###############################
#### Methods ####
###############################
sub _create_bug_group {
my $self = shift;
my $dbh = Bugzilla->dbh;
my $group_name = $self->name;
while (new Bugzilla::Group({name => $group_name})) {
$group_name .= '_';
}
my $group_description = get_text('bug_group_description', {product => $self});
my $group = Bugzilla::Group->create({name => $group_name,
description => $group_description,
isbuggroup => 1});
# Associate the new group and new product.
$dbh->do('INSERT INTO group_control_map
(group_id, product_id, entry, membercontrol, othercontrol, canedit)
VALUES (?, ?, ?, ?, ?, ?)',
undef, ($group->id, $self->id, Bugzilla->params->{'useentrygroupdefault'},
CONTROLMAPDEFAULT, CONTROLMAPNA, 0));
}
sub _create_series {
my $self = shift;
my @series;
# We do every status, every resolution, and an "opened" one as well.
foreach my $bug_status (@{get_legal_field_values('bug_status')}) {
push(@series, [$bug_status, "bug_status=" . url_quote($bug_status)]);
}
foreach my $resolution (@{get_legal_field_values('resolution')}) {
next if !$resolution;
push(@series, [$resolution, "resolution=" . url_quote($resolution)]);
}
my @openedstatuses = BUG_STATE_OPEN;
my $query = join("&", map { "bug_status=" . url_quote($_) } @openedstatuses);
push(@series, [get_text('series_all_open'), $query]);
foreach my $sdata (@series) {
my $series = new Bugzilla::Series(undef, $self->name,
get_text('series_subcategory'),
$sdata->[0], Bugzilla->user->id, 1,
$sdata->[1] . "&product=" . url_quote($self->name), 1);
$series->writeToDatabase();
}
}
sub set_name { $_[0]->set('name', $_[1]); }
sub set_description { $_[0]->set('description', $_[1]); }
sub set_default_milestone { $_[0]->set('defaultmilestone', $_[1]); }
sub set_milestone_url { $_[0]->set('milestoneurl', $_[1]); }
sub set_disallow_new { $_[0]->set('disallownew', $_[1]); }
sub set_votes_per_user { $_[0]->set('votesperuser', $_[1]); }
sub set_votes_per_bug { $_[0]->set('maxvotesperbug', $_[1]); }
sub set_votes_to_confirm { $_[0]->set('votestoconfirm', $_[1]); }
sub set_group_controls {
my ($self, $group, $settings) = @_;
$group->is_active_bug_group
|| ThrowUserError('product_illegal_group', {group => $group});
scalar(keys %$settings)
|| ThrowCodeError('product_empty_group_controls', {group => $group});
# We store current settings for this group.
my $gs = $self->group_controls->{$group->id};
# If there is no entry for this group yet, create a default hash.
unless (defined $gs) {
$gs = { entry => 0,
membercontrol => CONTROLMAPNA,
othercontrol => CONTROLMAPNA,
canedit => 0,
editcomponents => 0,
editbugs => 0,
canconfirm => 0,
group => $group };
}
# Both settings must be defined, or none of them can be updated.
if (defined $settings->{membercontrol} && defined $settings->{othercontrol}) {
# Legality of control combination is a function of
# membercontrol\othercontrol
# NA SH DE MA
# NA + - - -
# SH + + + +
# DE + - + +
# MA - - - +
foreach my $field ('membercontrol', 'othercontrol') {
my ($is_legal) = grep { $settings->{$field} == $_ }
(CONTROLMAPNA, CONTROLMAPSHOWN, CONTROLMAPDEFAULT, CONTROLMAPMANDATORY);
defined $is_legal || ThrowCodeError('product_illegal_group_control',
{ field => $field, value => $settings->{$field} });
}
unless ($settings->{membercontrol} == $settings->{othercontrol}
|| $settings->{membercontrol} == CONTROLMAPSHOWN
|| ($settings->{membercontrol} == CONTROLMAPDEFAULT
&& $settings->{othercontrol} != CONTROLMAPSHOWN))
{
ThrowUserError('illegal_group_control_combination', {groupname => $group->name});
}
$gs->{membercontrol} = $settings->{membercontrol};
$gs->{othercontrol} = $settings->{othercontrol};
}
foreach my $field ('entry', 'canedit', 'editcomponents', 'editbugs', 'canconfirm') {
next unless defined $settings->{$field};
$gs->{$field} = $settings->{$field} ? 1 : 0;
}
$self->{group_controls}->{$group->id} = $gs;
$self->{check_group_controls} = 1;
}
sub components {
my $self = shift;
my $dbh = Bugzilla->dbh;
@ -99,25 +680,33 @@ sub components {
}
sub group_controls {
my $self = shift;
my ($self, $full_data) = @_;
my $dbh = Bugzilla->dbh;
if (!defined $self->{group_controls}) {
my $query = qq{SELECT
groups.id,
group_control_map.entry,
group_control_map.membercontrol,
group_control_map.othercontrol,
group_control_map.canedit,
group_control_map.editcomponents,
group_control_map.editbugs,
group_control_map.canconfirm
# By default, we don't return groups which are not listed in
# group_control_map. If $full_data is true, then we also
# return groups whose settings could be set for the product.
my $where_or_and = 'WHERE';
my $and_or_where = 'AND';
if ($full_data) {
$where_or_and = 'AND';
$and_or_where = 'WHERE';
}
# If $full_data is true, we collect all the data in all cases,
# even if the cache is already populated.
# $full_data is never used except in the very special case where
# all configurable bug groups are displayed to administrators,
# so we don't care about collecting all the data again in this case.
if (!defined $self->{group_controls} || $full_data) {
# Include name to the list, to allow us sorting data more easily.
my $query = qq{SELECT id, name, entry, membercontrol, othercontrol,
canedit, editcomponents, editbugs, canconfirm
FROM groups
LEFT JOIN group_control_map
ON groups.id = group_control_map.group_id
WHERE group_control_map.product_id = ?
AND groups.isbuggroup != 0
ORDER BY groups.name};
ON id = group_id
$where_or_and product_id = ?
$and_or_where isbuggroup = 1};
$self->{group_controls} =
$dbh->selectall_hashref($query, 'id', undef, $self->id);
@ -126,6 +715,21 @@ sub group_controls {
my $groups = Bugzilla::Group->new_from_list(\@gids);
$self->{group_controls}->{$_->id}->{group} = $_ foreach @$groups;
}
# We never cache bug counts, for the same reason as above.
if ($full_data) {
my $counts =
$dbh->selectall_arrayref('SELECT group_id, COUNT(bugs.bug_id) AS bug_count
FROM bug_group_map
INNER JOIN bugs
ON bugs.bug_id = bug_group_map.bug_id
WHERE bugs.product_id = ? ' .
$dbh->sql_group_by('group_id'),
{'Slice' => {}}, $self->id);
foreach my $data (@$counts) {
$self->{group_controls}->{$data->{group_id}}->{bug_count} = $data->{bug_count};
}
}
return $self->{group_controls};
}
@ -341,7 +945,10 @@ below.
Description: Returns a hash (group id as key) with all product
group controls.
Params: none.
Params: $full_data (optional, false by default) - when true,
the number of bugs per group applicable to the product
is also returned. Moreover, bug groups which have no
special settings for the product are also returned.
Returns: A hash with group id as key and hash containing
a Bugzilla::Group object and the properties of group

View File

@ -33,7 +33,13 @@ use strict;
package Bugzilla::Search;
use base qw(Exporter);
@Bugzilla::Search::EXPORT = qw(IsValidQueryType);
@Bugzilla::Search::EXPORT = qw(
EMPTY_COLUMN
IsValidQueryType
split_order_term
translate_old_column
);
use Bugzilla::Error;
use Bugzilla::Util;
@ -47,33 +53,126 @@ use Bugzilla::Keyword;
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 => {
'bugs.target_milestone' => [ 'ms_order.sortkey','ms_order.value' ],
'bugs.bug_status' => [ 'bug_status.sortkey','bug_status.value' ],
'bugs.rep_platform' => [ 'rep_platform.sortkey','rep_platform.value' ],
'bugs.priority' => [ 'priority.sortkey','priority.value' ],
'bugs.op_sys' => [ 'op_sys.sortkey','op_sys.value' ],
'bugs.resolution' => [ 'resolution.sortkey', 'resolution.value' ],
'bugs.bug_severity' => [ 'bug_severity.sortkey','bug_severity.value' ]
'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 => {
'bugs.target_milestone' => 'LEFT JOIN milestones AS ms_order ON ms_order.value = bugs.target_milestone AND ms_order.product_id = bugs.product_id',
'bugs.bug_status' => 'LEFT JOIN bug_status ON bug_status.value = bugs.bug_status',
'bugs.rep_platform' => 'LEFT JOIN rep_platform ON rep_platform.value = bugs.rep_platform',
'bugs.priority' => 'LEFT JOIN priority ON priority.value = bugs.priority',
'bugs.op_sys' => 'LEFT JOIN op_sys ON op_sys.value = bugs.op_sys',
'bugs.resolution' => 'LEFT JOIN resolution ON resolution.value = bugs.resolution',
'bugs.bug_severity' => 'LEFT JOIN bug_severity ON bug_severity.value = bugs.bug_severity'
'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".
my $actual_time = '(SUM(ldtime.work_time)'
. ' * COUNT(DISTINCT ldtime.bug_when)/COUNT(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)",
);
# 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 {
@ -90,26 +189,18 @@ sub new {
sub init {
my $self = shift;
my $fieldsref = $self->{'fields'};
my @fields = @{ $self->{'fields'} || [] };
my $params = $self->{'params'};
$self->{'user'} ||= Bugzilla->user;
my $user = $self->{'user'};
my $orderref = $self->{'order'} || 0;
my @inputorder;
@inputorder = @$orderref if $orderref;
my @inputorder = @{ $self->{'order'} || [] };
my @orderby;
my $debug = 0;
my @debugdata;
if ($params->param('debug')) { $debug = 1; }
my @fields;
my @supptables;
my @wherepart;
my @having;
my @groupby;
@fields = @$fieldsref if $fieldsref;
my @specialchart;
my @andlist;
my %chartfields;
@ -117,55 +208,64 @@ sub init {
my %special_order = %{SPECIAL_ORDER()};
my %special_order_join = %{SPECIAL_ORDER_JOIN()};
my @select_fields = Bugzilla->get_fields({ type => FIELD_TYPE_SINGLE_SELECT,
obsolete => 0 });
my @select_fields =
Bugzilla->get_fields({ type => FIELD_TYPE_SINGLE_SELECT });
my @multi_select_fields = Bugzilla->get_fields({ type => FIELD_TYPE_MULTI_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;
$special_order{"bugs.$name"} = [ "$name.sortkey", "$name.value" ],
$special_order_join{"bugs.$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(/map_assigned_to/, @$fieldsref)) {
if (grep(/^assigned_to/, @fields)) {
push @supptables, "INNER JOIN profiles AS map_assigned_to " .
"ON bugs.assigned_to = map_assigned_to.userid";
}
if (grep(/map_reporter/, @$fieldsref)) {
if (grep(/^reporter/, @fields)) {
push @supptables, "INNER JOIN profiles AS map_reporter " .
"ON bugs.reporter = map_reporter.userid";
}
if (grep(/map_qa_contact/, @$fieldsref)) {
if (grep(/^qa_contact/, @fields)) {
push @supptables, "LEFT JOIN profiles AS map_qa_contact " .
"ON bugs.qa_contact = map_qa_contact.userid";
}
if (lsearch($fieldsref, 'map_products.name') >= 0) {
if (grep($_ eq 'product' || $_ eq 'classification', @fields))
{
push @supptables, "INNER JOIN products AS map_products " .
"ON bugs.product_id = map_products.id";
}
if (lsearch($fieldsref, 'map_classifications.name') >= 0) {
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 (lsearch($fieldsref, 'map_components.name') >= 0) {
if (grep($_ eq 'component', @fields)) {
push @supptables, "INNER JOIN components AS map_components " .
"ON bugs.component_id = map_components.id";
}
if (grep($_ =~/AS (actual_time|percentage_complete)$/, @$fieldsref)) {
if (grep($_ eq 'actual_time' || $_ eq 'percentage_complete', @fields)) {
push(@supptables, "LEFT JOIN longdescs AS ldtime " .
"ON ldtime.bug_id = bugs.bug_id");
}
@ -220,10 +320,9 @@ sub init {
}
}
my @legal_fields = ("product", "version", "rep_platform", "op_sys",
"bug_status", "resolution", "priority", "bug_severity",
"assigned_to", "reporter", "component", "classification",
"target_milestone", "bug_group");
my @legal_fields = ("product", "version", "assigned_to", "reporter",
"component", "classification", "target_milestone",
"bug_group");
# Include custom select fields.
push(@legal_fields, map { $_->name } @select_fields);
@ -253,15 +352,7 @@ sub init {
next;
}
my $type = $params->param("emailtype$id");
if ($type eq "exact") {
$type = "anyexact";
foreach my $name (split(',', $email)) {
$name = trim($name);
if ($name) {
login_to_id($name, THROW_ERROR);
}
}
}
$type = "anyexact" if ($type eq "exact");
my @clist;
foreach my $field ("assigned_to", "reporter", "cc", "qa_contact") {
@ -274,9 +365,17 @@ sub init {
}
if (@clist) {
push(@specialchart, \@clist);
} else {
ThrowUserError("missing_email_type",
{ email => $email });
}
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;
}
}
}
@ -319,8 +418,22 @@ sub init {
my $sql_chvalue = $chvalue ne '' ? $dbh->quote($chvalue) : '';
trick_taint($sql_chvalue);
if(!@chfield) {
push(@wherepart, "bugs.delta_ts >= $sql_chfrom") if ($sql_chfrom);
push(@wherepart, "bugs.delta_ts <= $sql_chto") if ($sql_chto);
if ($sql_chfrom) {
my $term = "bugs.delta_ts >= $sql_chfrom";
push(@wherepart, $term);
$self->search_description({
field => 'delta_ts', type => 'greaterthaneq',
value => $chfieldfrom, term => $term,
});
}
if ($sql_chto) {
my $term = "bugs.delta_ts <= $sql_chto";
push(@wherepart, $term);
$self->search_description({
field => 'delta_ts', type => 'lessthaneq',
value => $chfieldto, term => $term,
});
}
} else {
my $bug_creation_clause;
my @list;
@ -330,8 +443,22 @@ sub init {
# Treat [Bug creation] differently because we need to look
# at bugs.creation_ts rather than the bugs_activity table.
my @l;
push(@l, "bugs.creation_ts >= $sql_chfrom") if($sql_chfrom);
push(@l, "bugs.creation_ts <= $sql_chto") if($sql_chto);
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) . ")";
} else {
push(@actlist, get_field_id($f));
@ -343,18 +470,39 @@ sub init {
if(@actlist) {
my $extra = " actcheck.bug_id = bugs.bug_id";
push(@list, "(actcheck.bug_when IS NOT NULL)");
if($sql_chfrom) {
$extra .= " AND actcheck.bug_when >= $sql_chfrom";
}
if($sql_chto) {
$extra .= " AND actcheck.bug_when <= $sql_chto";
}
if($sql_chvalue) {
$extra .= " AND actcheck.added = $sql_chvalue";
}
my $from_term = " AND actcheck.bug_when >= $sql_chfrom";
$extra .= $from_term if $sql_chfrom;
my $to_term = " AND actcheck.bug_when <= $sql_chto";
$extra .= $to_term if $sql_chto;
my $value_term = " AND actcheck.added = $sql_chvalue";
$extra .= $value_term if $sql_chvalue;
push(@supptables, "LEFT JOIN bugs_activity AS actcheck " .
"ON $extra AND "
. $dbh->sql_in('actcheck.fieldid', \@actlist));
foreach my $field (@chfield) {
next if $field eq "[Bug creation]";
if ($sql_chvalue) {
$self->search_description({
field => $field, type => 'changedto',
value => $chvalue, term => $value_term,
});
}
if ($sql_chfrom) {
$self->search_description({
field => $field, type => 'changedafter',
value => $chfieldfrom, term => $from_term,
});
}
if ($sql_chvalue) {
$self->search_description({
field => $field, type => 'changedbefore',
value => $chfieldto, term => $to_term,
});
}
}
}
# Now that we're done using @list to determine if there are any
@ -369,7 +517,7 @@ sub init {
my $sql_deadlinefrom;
my $sql_deadlineto;
if ($user->in_group(Bugzilla->params->{'timetrackinggroup'})) {
if ($user->is_timetracker) {
my $deadlinefrom;
my $deadlineto;
@ -380,7 +528,12 @@ sub init {
format => 'YYYY-MM-DD'});
$sql_deadlinefrom = $dbh->quote($deadlinefrom);
trick_taint($sql_deadlinefrom);
push(@wherepart, "bugs.deadline >= $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')){
@ -390,11 +543,16 @@ sub init {
format => 'YYYY-MM-DD'});
$sql_deadlineto = $dbh->quote($deadlineto);
trick_taint($sql_deadlineto);
push(@wherepart, "bugs.deadline <= $sql_deadlineto");
my $term = "bugs.deadline <= $sql_deadlineto";
push(@wherepart, $term);
$self->search_description({
field => 'deadline', type => 'lessthaneq',
value => $deadlineto, term => $term,
});
}
}
foreach my $f ("short_desc", "long_desc", "bug_file_loc",
foreach my $f ("short_desc", "longdesc", "bug_file_loc",
"status_whiteboard") {
if (defined $params->param($f)) {
my $s = trim($params->param($f));
@ -416,8 +574,6 @@ sub init {
my $chartid;
my $sequence = 0;
# $type_id is used by the code that queries for attachment flags.
my $type_id = 0;
my $f;
my $ff;
my $t;
@ -460,6 +616,7 @@ sub init {
"^(?: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,
@ -541,12 +698,6 @@ sub init {
$params->param("field$chart-$row-$col", shift(@$ref));
$params->param("type$chart-$row-$col", shift(@$ref));
$params->param("value$chart-$row-$col", shift(@$ref));
if ($debug) {
push(@debugdata, "$row-$col = " .
$params->param("field$chart-$row-$col") . ' | ' .
$params->param("type$chart-$row-$col") . ' | ' .
$params->param("value$chart-$row-$col") . ' *');
}
$col++;
}
@ -654,6 +805,7 @@ sub init {
$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;
@ -680,24 +832,21 @@ sub init {
foreach my $key (@funcnames) {
if ("$f,$t,$rhs" =~ m/$key/) {
my $ref = $funcsbykey{$key};
if ($debug) {
push(@debugdata, "$key ($f / $t / $rhs) =>");
}
$ff = $f;
if ($f !~ /\./) {
$ff = "bugs.$f";
}
$self->$ref(%func_args);
if ($debug) {
push(@debugdata, "$f / $t / $v / " .
($term || "undef") . " *");
}
if ($term) {
last;
}
}
}
if ($term) {
$self->search_description({
field => $original_f, type => $t, value => $v,
term => $term,
});
push(@orlist, $term);
}
else {
@ -726,13 +875,6 @@ sub init {
# to other parts of the query, so we want to create it before we
# write the FROM clause.
foreach my $orderitem (@inputorder) {
# Some fields have 'AS' aliases. The aliases go in the ORDER BY,
# not the whole fields.
# XXX - Ideally, we would get just the aliases in @inputorder,
# and we'd never have to deal with this.
if ($orderitem =~ /\s+AS\s+(.+)$/i) {
$orderitem = $1;
}
BuildOrderBy(\%special_order, $orderitem, \@orderby);
}
# Now JOIN the correct tables in the FROM clause.
@ -740,9 +882,9 @@ sub init {
# cleaner to do it this way.
foreach my $orderitem (@inputorder) {
# Grab the part without ASC or DESC.
my @splitfield = split(/\s+/, $orderitem);
if ($special_order_join{$splitfield[0]}) {
push(@supptables, $special_order_join{$splitfield[0]});
my $column_name = split_order_term($orderitem);
if ($special_order_join{$column_name}) {
push(@supptables, $special_order_join{$column_name});
}
}
@ -771,14 +913,17 @@ sub init {
# Make sure we create a legal SQL query.
@andlist = ("1 = 1") if !@andlist;
my $query = "SELECT " . join(', ', @fields) .
my @sql_fields = map { $_ eq EMPTY_COLUMN ? EMPTY_COLUMN
: COLUMNS->{$_}->{name} . ' AS ' . $_ } @fields;
my $query = "SELECT " . join(', ', @sql_fields) .
" FROM $suppstring" .
" LEFT JOIN bug_group_map " .
" ON bug_group_map.bug_id = bugs.bug_id ";
if ($user->id) {
if (%{$user->groups}) {
$query .= " AND bug_group_map.group_id NOT IN (" . join(',', values(%{$user->groups})) . ") ";
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;
@ -797,17 +942,21 @@ sub init {
}
}
foreach my $field (@fields, @orderby) {
next if ($field =~ /(AVG|SUM|COUNT|MAX|MIN|VARIANCE)\s*\(/i ||
$field =~ /^\d+$/ || $field eq "bugs.bug_id" ||
$field =~ /^(relevance|actual_time|percentage_complete)/);
# The structure of fields is of the form:
# [foo AS] {bar | bar.baz} [ASC | DESC]
# Only the mandatory part bar OR bar.baz is of interest.
# But for Oracle, it needs the real name part instead.
my $regexp = $dbh->GROUPBY_REGEXP;
if ($field =~ /$regexp/i) {
push(@groupby, $1) if !grep($_ eq $1, @groupby);
# 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)));
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));
@ -822,7 +971,6 @@ sub init {
}
$self->{'sql'} = $query;
$self->{'debugdata'} = \@debugdata;
}
###############################################################################
@ -926,9 +1074,13 @@ sub getSQL {
return $self->{'sql'};
}
sub getDebugData {
my $self = shift;
return $self->{'debugdata'};
sub search_description {
my ($self, $params) = @_;
my $desc = $self->{'search_description'} ||= [];
if ($params) {
push(@$desc, $params);
}
return $self->{'search_description'};
}
sub pronoun {
@ -988,9 +1140,7 @@ sub IsValidQueryType
sub BuildOrderBy {
my ($special_order, $orderitem, $stringlist, $reverseorder) = (@_);
my @twopart = split(/\s+/, $orderitem);
my $orderfield = $twopart[0];
my $orderdirection = $twopart[1] || "";
my ($orderfield, $orderdirection) = split_order_term($orderitem);
if ($reverseorder) {
# If orderdirection is empty or ASC...
@ -1017,6 +1167,40 @@ sub BuildOrderBy {
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));
$direction ||= "";
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
#####################################################################
@ -1032,7 +1216,7 @@ sub _contact_exact_group {
my $group = $1;
my $groupid = Bugzilla::Group::ValidateGroupName( $group, ($user));
$groupid || ThrowUserError('invalid_group_name',{name => $group});
my @childgroups = @{$user->flatten_group_membership($groupid)};
my @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 " .
@ -1104,7 +1288,7 @@ sub _cc_exact_group {
my $group = $1;
my $groupid = Bugzilla::Group::ValidateGroupName( $group, ($user));
$groupid || ThrowUserError('invalid_group_name',{name => $group});
my @childgroups = @{$user->flatten_group_membership($groupid)};
my @childgroups = @{Bugzilla::Group->flatten_group_membership($groupid)};
my $chartseq = $$chartid;
if ($$chartid eq "") {
$chartseq = "CC$$sequence";
@ -1237,7 +1421,7 @@ sub _content_matches
my $l = FULLTEXT_BUGLIST_LIMIT;
my $table = "bugs_fulltext_$$chartid";
my $comments_col = "comments";
$comments_col = "comments_noprivate" unless $self->{user}->is_insider;
$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);
@ -1424,8 +1608,8 @@ sub _percentage_complete {
}
if ($oper ne "noop") {
my $table = "longdescs_$$chartid";
if(lsearch($fields, "bugs.remaining_time") == -1) {
push(@$fields, "bugs.remaining_time");
if (!grep($_ eq 'remaining_time', @$fields)) {
push(@$fields, "remaining_time");
}
push(@$supptables, "LEFT JOIN longdescs AS $table " .
"ON $table.bug_id = bugs.bug_id");
@ -2159,7 +2343,7 @@ sub LookupNamedQuery
"SELECT group_id FROM namedquery_group_map WHERE namedquery_id = ?",
undef, $id
);
if (!grep { $_ == $group } values %{$user->groups})
if (!grep { $_->id == $group } @{ $user->groups })
{
ThrowUserError("missing_query", {
queryname => $name,

View File

@ -33,7 +33,6 @@ package Bugzilla::Series;
use Bugzilla::Error;
use Bugzilla::Util;
use Bugzilla::User;
sub new {
my $invocant = shift;

View File

@ -22,35 +22,99 @@ use strict;
package Bugzilla::Status;
use base qw(Bugzilla::Object Exporter);
@Bugzilla::Status::EXPORT = qw(BUG_STATE_OPEN is_open_state closed_bug_statuses);
use Bugzilla::Error;
use base qw(Bugzilla::Field::Choice Exporter);
@Bugzilla::Status::EXPORT = qw(
BUG_STATE_OPEN
SPECIAL_STATUS_WORKFLOW_ACTIONS
is_open_state
closed_bug_statuses
);
################################
##### Initialization #####
################################
use constant DB_TABLE => 'bug_status';
use constant DB_COLUMNS => qw(
id
value
sortkey
isactive
is_open
use constant SPECIAL_STATUS_WORKFLOW_ACTIONS => qw(
none
duplicate
change_resolution
clearresolution
);
use constant NAME_FIELD => 'value';
use constant LIST_ORDER => 'sortkey, value';
use constant DB_TABLE => 'bug_status';
# This has all the standard Bugzilla::Field::Choice columns plus "is_open"
sub DB_COLUMNS {
return ($_[0]->SUPER::DB_COLUMNS, 'is_open');
}
sub VALIDATORS {
my $invocant = shift;
my $validators = $invocant->SUPER::VALIDATORS;
$validators->{is_open} = \&Bugzilla::Object::check_boolean;
$validators->{value} = \&_check_value;
return $validators;
}
#########################
# Database Manipulation #
#########################
sub create {
my $class = shift;
my $self = $class->SUPER::create(@_);
add_missing_bug_status_transitions();
return $self;
}
sub remove_from_db {
my $self = shift;
my $dbh = Bugzilla->dbh;
my $id = $self->id;
$dbh->bz_start_transaction();
$self->SUPER::remove_from_db();
$dbh->do('DELETE FROM status_workflow
WHERE old_status = ? OR new_status = ?',
undef, $id, $id);
$dbh->bz_commit_transaction();
}
###############################
##### Accessors ####
###############################
sub name { return $_[0]->{'value'}; }
sub sortkey { return $_[0]->{'sortkey'}; }
sub is_active { return $_[0]->{'isactive'}; }
sub is_open { return $_[0]->{'is_open'}; }
sub is_static {
my $self = shift;
if ($self->name eq 'UNCONFIRMED'
|| $self->name eq Bugzilla->params->{'duplicate_or_move_bug_status'})
{
return 1;
}
return 0;
}
##############
# Validators #
##############
sub _check_value {
my $invocant = shift;
my $value = $invocant->SUPER::_check_value(@_);
if (grep { lc($value) eq lc($_) } SPECIAL_STATUS_WORKFLOW_ACTIONS) {
ThrowUserError('fieldvalue_reserved_word',
{ field => $invocant->field, value => $value });
}
return $value;
}
###############################
##### Methods ####
###############################

View File

@ -48,7 +48,6 @@ use Cwd qw(abs_path);
use MIME::Base64;
use MIME::QuotedPrint qw(encode_qp);
use Encode qw(encode);
# for time2str - replace by TT Date plugin??
use Date::Format ();
use File::Basename qw(dirname);
use File::Find;
@ -179,7 +178,7 @@ sub template_exists
# If you want to modify this routine, read the comments carefully
sub quoteUrls {
my ($text, $curr_bugid) = (@_);
my ($text, $curr_bugid, $already_wrapped) = (@_);
return $text unless $text;
# We use /g for speed, but uris can have other things inside them
@ -194,6 +193,10 @@ sub quoteUrls {
my $chr1 = chr(1);
$text =~ s/\0/$chr1\0/g;
# If the comment is already wrapped, we should ignore newlines when
# looking for matching regexps. Else we should take them into account.
my $s = $already_wrapped ? qr/\s/ : qr/[[:blank:]]/;
# However, note that adding the title (for buglinks) can affect things
# In particular, attachment matches go before bug titles, so that titles
# with 'attachment 1' don't double match.
@ -212,7 +215,7 @@ sub quoteUrls {
map { qr/$_/ } grep($_, Bugzilla->params->{'urlbase'},
Bugzilla->params->{'sslbase'})) . ')';
$text =~ s~\b(${urlbase_re}\Qshow_bug.cgi?id=\E([0-9]+)(\#c([0-9]+))?)\b
~($things[$count++] = get_bug_link($3, $1, $5)) &&
~($things[$count++] = get_bug_link($3, $1, { comment_num => $5 })) &&
("\0\0" . ($count-1) . "\0\0")
~egox;
@ -259,7 +262,7 @@ sub quoteUrls {
("\0\0" . ($count-1) . "\0\0")
~egmx;
$text =~ s~\b(attachment\s*\#?\s*(\d+))
$text =~ s~\b(attachment$s*\#?$s*(\d+))
~($things[$count++] = get_attachment_link($2, $1)) &&
("\0\0" . ($count-1) . "\0\0")
~egmxi;
@ -272,12 +275,12 @@ sub quoteUrls {
# Also, we can't use $bug_re?$comment_re? because that will match the
# empty string
my $bug_word = get_text('term', { term => 'bug' });
my $bug_re = qr/\Q$bug_word\E\s*\#?\s*(\d+)/i;
my $comment_re = qr/comment\s*\#?\s*(\d+)/i;
$text =~ s~\b($bug_re(?:\s*,?\s*$comment_re)?|$comment_re)
my $bug_re = qr/\Q$bug_word\E$s*\#?$s*(\d+)/i;
my $comment_re = qr/comment$s*\#?$s*(\d+)/i;
$text =~ s~\b($bug_re(?:$s*,?$s*$comment_re)?|$comment_re)
~ # We have several choices. $1 here is the link, and $2-4 are set
# depending on which part matched
(defined($2) ? get_bug_link($2,$1,$3) :
(defined($2) ? get_bug_link($2, $1, { comment_num => $3 }) :
"<a href=\"$current_bugurl#c$4\">$1</a>")
~egox;
@ -342,7 +345,7 @@ sub get_attachment_link {
# comment in the bug
sub get_bug_link {
my ($bug_num, $link_text, $comment_num) = @_;
my ($bug_num, $link_text, $options) = @_;
my $dbh = Bugzilla->dbh;
if (!defined($bug_num) || ($bug_num eq "")) {
@ -351,11 +354,15 @@ sub get_bug_link {
my $quote_bug_num = html_quote($bug_num);
detaint_natural($bug_num) || return "&lt;invalid bug number: $quote_bug_num&gt;";
my ($bug_state, $bug_res, $bug_desc) =
$dbh->selectrow_array('SELECT bugs.bug_status, resolution, short_desc
my ($bug_alias, $bug_state, $bug_res, $bug_desc) =
$dbh->selectrow_array('SELECT bugs.alias, bugs.bug_status, bugs.resolution, bugs.short_desc
FROM bugs WHERE bugs.bug_id = ?',
undef, $bug_num);
if ($options->{use_alias} && $link_text =~ /^\d+$/ && $bug_alias) {
$link_text = $bug_alias;
}
if ($bug_state) {
# Initialize these variables to be "" so that we don't get warnings
# if we don't change them below (which is highly likely).
@ -378,8 +385,8 @@ sub get_bug_link {
$title = html_quote(clean_text($title));
my $linkval = "show_bug.cgi?id=$bug_num";
if (defined $comment_num) {
$linkval .= "#c$comment_num";
if ($options->{comment_num}) {
$linkval .= "#c" . $options->{comment_num};
}
return qq{$pre<a href="$linkval" title="$title">$link_text</a>$post};
}
@ -588,20 +595,20 @@ sub create {
css_class_quote => \&Bugzilla::Util::css_class_quote ,
quoteUrls => [ sub {
my ($context, $bug) = @_;
my ($context, $bug, $already_wrapped) = @_;
return sub {
my $text = shift;
return quoteUrls($text, $bug);
return quoteUrls($text, $bug, $already_wrapped);
};
},
1
],
bug_link => [ sub {
my ($context, $bug) = @_;
my ($context, $bug, $options) = @_;
return sub {
my $text = shift;
return get_bug_link($bug, $text);
return get_bug_link($bug, $text, $options);
};
},
1
@ -665,7 +672,15 @@ sub create {
},
# Format a time for display (more info in Bugzilla::Util)
time => \&Bugzilla::Util::format_time,
time => [ sub {
my ($context, $format, $timezone) = @_;
return sub {
my $time = shift;
return format_time($time, $format, $timezone);
};
},
1
],
# Bug 120030: Override html filter to obscure the '@' in user
# visible strings.
@ -703,6 +718,8 @@ sub create {
html_light => \&Bugzilla::Util::html_light_quote,
email => \&Bugzilla::Util::email_filter,
# iCalendar contentline filter
ics => [ sub {
my ($context, @args) = @_;
@ -741,6 +758,10 @@ sub create {
$var =~ s/\&gt;/>/g;
$var =~ s/\&quot;/\"/g;
$var =~ s/\&amp;/\&/g;
# Now remove extra whitespace, and wrap it to 72 characters.
my $collapse_filter = $Template::Filters::FILTERS->{collapse};
$var = $collapse_filter->($var);
$var = wrap_comment($var, 72);
return $var;
},

View File

@ -46,9 +46,9 @@ sub user_visible_products {
$query .= "AND group_control_map.membercontrol = " .
CONTROLMAPMANDATORY . " ";
}
if (%{Bugzilla->user->groups}) {
if (@{Bugzilla->user->groups}) {
$query .= "AND group_id NOT IN(" .
join(',', values(%{Bugzilla->user->groups})) . ") ";
join(',', map { $_->id } @{Bugzilla->user->groups}) . ") ";
}
$query .= "WHERE group_id IS NULL AND products.classification_id= ? ORDER BY products.name";

View File

@ -234,9 +234,9 @@ sub get_env_product_list {
$query .=
"AND group_control_map.membercontrol = " . CONTROLMAPMANDATORY . " ";
}
if ( %{ Bugzilla->user->groups } ) {
if ( @{ Bugzilla->user->groups } ) {
$query .= "AND group_id NOT IN("
. join( ',', values( %{ Bugzilla->user->groups } ) ) . ") ";
. join( ',', map { $_->id } @{ Bugzilla->user->groups } ) . ") ";
}
$query .= "LEFT OUTER JOIN test_environment_category AS tec

View File

@ -824,9 +824,9 @@ sub get_distinct_builds {
$query .= "AND group_control_map.membercontrol = " .
CONTROLMAPMANDATORY . " ";
}
if (%{Bugzilla->user->groups}) {
if (@{Bugzilla->user->groups}) {
$query .= "AND group_id NOT IN(" .
join(',', values(%{Bugzilla->user->groups})) . ") ";
join(',', map { $_->id } @{Bugzilla->user->groups}) . ") ";
}
$query .= "WHERE group_id IS NULL AND build.isactive = 1 ORDER BY build.name";

View File

@ -76,7 +76,7 @@ sub issue_new_user_account_token {
my ($token, $token_ts) = _create_token(undef, 'account', $login_name);
$vars->{'email'} = $login_name . Bugzilla->params->{'emailsuffix'};
$vars->{'token_ts'} = $token_ts;
$vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400);
$vars->{'token'} = $token;
my $message;
@ -105,10 +105,7 @@ sub IssueEmailChangeToken {
$vars->{'oldemailaddress'} = $old_email . $email_suffix;
$vars->{'newemailaddress'} = $new_email . $email_suffix;
$vars->{'max_token_age'} = MAX_TOKEN_AGE;
$vars->{'token_ts'} = $token_ts;
$vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400);
$vars->{'token'} = $token;
$vars->{'emailaddress'} = $old_email . $email_suffix;
@ -153,8 +150,10 @@ sub IssuePasswordToken {
$vars->{'token'} = $token;
$vars->{'emailaddress'} = $user->email;
$vars->{'max_token_age'} = MAX_TOKEN_AGE;
$vars->{'token_ts'} = $token_ts;
$vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400);
# The user is not logged in (else he wouldn't request a new password).
# So we have to pass this information to the template.
$vars->{'timezone'} = $user->timezone;
my $message = "";
$template->process("account/password/forgotten-password.txt.tmpl",
@ -288,6 +287,9 @@ sub Cancel {
$vars->{'token'} = $token;
$vars->{'tokentype'} = $tokentype;
$vars->{'issuedate'} = $issuedate;
# The user is probably not logged in.
# So we have to pass this information to the template.
$vars->{'timezone'} = $user->timezone;
$vars->{'eventdata'} = $eventdata;
$vars->{'cancelaction'} = $cancelaction;

View File

@ -48,6 +48,10 @@ use Bugzilla::User::Setting;
use Bugzilla::Product;
use Bugzilla::Classification;
use Bugzilla::Field;
use Bugzilla::Group;
use Scalar::Util qw(blessed);
use DateTime::TimeZone;
use base qw(Bugzilla::Object Exporter);
@Bugzilla::User::EXPORT = qw(is_available_username
@ -362,6 +366,22 @@ sub settings {
return $self->{'settings'};
}
sub timezone {
my $self = shift;
if (!defined $self->{timezone}) {
my $tz = $self->settings->{timezone}->{value};
if ($tz eq 'local') {
# The user wants the local timezone of the server.
$self->{timezone} = Bugzilla->local_timezone;
}
else {
$self->{timezone} = DateTime::TimeZone->new(name => $tz);
}
}
return $self->{timezone};
}
sub flush_queries_cache {
my $self = shift;
@ -374,75 +394,62 @@ sub groups {
my $self = shift;
return $self->{groups} if defined $self->{groups};
return {} unless $self->id;
return [] unless $self->id;
my $dbh = Bugzilla->dbh;
my $groups = $dbh->selectcol_arrayref(q{SELECT DISTINCT groups.name, group_id
FROM groups, user_group_map
WHERE groups.id=user_group_map.group_id
AND user_id=?
AND isbless=0},
{ Columns=>[1,2] },
$self->id);
my $groups_to_check = $dbh->selectcol_arrayref(
q{SELECT DISTINCT group_id
FROM user_group_map
WHERE user_id = ? AND isbless = 0}, undef, $self->id);
# The above gives us an arrayref [name, id, name, id, ...]
# Convert that into a hashref
my %groups = @$groups;
my @groupidstocheck = values(%groups);
my %groupidschecked = ();
my $rows = $dbh->selectall_arrayref(
"SELECT DISTINCT groups.name, groups.id, member_id
"SELECT DISTINCT grantor_id, member_id
FROM group_group_map
INNER JOIN groups
ON groups.id = grantor_id
WHERE grant_type = " . GROUP_MEMBERSHIP);
my %group_names = ();
my %group_membership = ();
foreach my $row (@$rows) {
my ($member_name, $grantor_id, $member_id) = @$row;
# Just save the group names
$group_names{$grantor_id} = $member_name;
# And group membership
push (@{$group_membership{$member_id}}, $grantor_id);
my %group_membership;
foreach my $row (@$rows) {
my ($grantor_id, $member_id) = @$row;
push (@{ $group_membership{$member_id} }, $grantor_id);
}
# Let's walk the groups hierarchy tree (using FIFO)
# On the first iteration it's pre-filled with direct groups
# membership. Later on, each group can add its own members into the
# FIFO. Circular dependencies are eliminated by checking
# $groupidschecked{$member_id} hash values.
# $checked_groups{$member_id} hash values.
# As a result, %groups will have all the groups we are the member of.
while ($#groupidstocheck >= 0) {
my %checked_groups;
my %groups;
while (scalar(@$groups_to_check) > 0) {
# Pop the head group from FIFO
my $member_id = shift @groupidstocheck;
my $member_id = shift @$groups_to_check;
# Skip the group if we have already checked it
if (!$groupidschecked{$member_id}) {
if (!$checked_groups{$member_id}) {
# Mark group as checked
$groupidschecked{$member_id} = 1;
$checked_groups{$member_id} = 1;
# Add all its members to the FIFO check list
# %group_membership contains arrays of group members
# for all groups. Accessible by group number.
foreach my $newgroupid (@{$group_membership{$member_id}}) {
push @groupidstocheck, $newgroupid
if (!$groupidschecked{$newgroupid});
}
# Note on if clause: we could have group in %groups from 1st
# query and do not have it in second one
$groups{$group_names{$member_id}} = $member_id
if $group_names{$member_id} && $member_id;
my $members = $group_membership{$member_id};
my @new_to_check = grep(!$checked_groups{$_}, @$members);
push(@$groups_to_check, @new_to_check);
$groups{$member_id} = 1;
}
}
$self->{groups} = \%groups;
$self->{groups} = Bugzilla::Group->new_from_list([keys %groups]);
return $self->{groups};
}
sub groups_as_string {
my $self = shift;
return (join(',',values(%{$self->groups})) || '-1');
my @ids = map { $_->id } @{ $self->groups };
return scalar(@ids) ? join(',', @ids) : '-1';
}
sub bless_groups {
@ -451,54 +458,45 @@ sub bless_groups {
return $self->{'bless_groups'} if defined $self->{'bless_groups'};
return [] unless $self->id;
my $dbh = Bugzilla->dbh;
my $query;
my $connector;
my @bindValues;
if ($self->in_group('editusers')) {
# Users having editusers permissions may bless all groups.
$query = 'SELECT DISTINCT id, name, description FROM groups';
$connector = 'WHERE';
$self->{'bless_groups'} = [Bugzilla::Group->get_all];
return $self->{'bless_groups'};
}
else {
my $dbh = Bugzilla->dbh;
# Get all groups for the user where:
# + They have direct bless privileges
# + They are a member of a group that inherits bless privs.
$query = q{
SELECT DISTINCT groups.id, groups.name, groups.description
my @group_ids = map {$_->id} @{ $self->groups };
@group_ids = (-1) if !@group_ids;
my $query =
'SELECT DISTINCT groups.id
FROM groups, user_group_map, group_group_map AS ggm
WHERE user_group_map.user_id = ?
AND ((user_group_map.isbless = 1
AND ( (user_group_map.isbless = 1
AND groups.id=user_group_map.group_id)
OR (groups.id = ggm.grantor_id
AND ggm.grant_type = ?
AND ggm.member_id IN(} .
$self->groups_as_string .
q{)))};
$connector = 'AND';
@bindValues = ($self->id, GROUP_BLESS);
}
AND ggm.grant_type = ' . GROUP_BLESS . '
AND ' . $dbh->sql_in('ggm.member_id', \@group_ids)
. ') )';
# If visibilitygroups are used, restrict the set of groups.
if (!$self->in_group('editusers')
&& Bugzilla->params->{'usevisibilitygroups'})
{
if (Bugzilla->params->{'usevisibilitygroups'}) {
return [] if !$self->visible_groups_as_string;
# Users need to see a group in order to bless it.
my $visibleGroups = join(', ', @{$self->visible_groups_direct()})
|| return $self->{'bless_groups'} = [];
$query .= " $connector id in ($visibleGroups)";
$query .= " AND "
. $dbh->sql_in('groups.id', $self->visible_groups_inherited);
}
$query .= ' ORDER BY name';
return $self->{'bless_groups'} =
$dbh->selectall_arrayref($query, {'Slice' => {}}, @bindValues);
my $ids = $dbh->selectcol_arrayref($query, undef, $self->id);
return $self->{'bless_groups'} = Bugzilla::Group->new_from_list($ids);
}
sub in_group {
my ($self, $group, $product_id) = @_;
if (exists $self->groups->{$group}) {
if (scalar grep($_->name eq $group, @{ $self->groups })) {
return 1;
}
elsif ($product_id && detaint_natural($product_id)) {
@ -527,8 +525,7 @@ sub in_group {
sub in_group_id {
my ($self, $id) = @_;
my %j = reverse(%{$self->groups});
return exists $j{$id} ? 1 : 0;
return grep($_->id == $id, @{ $self->groups }) ? 1 : 0;
}
sub get_products_by_permission {
@ -590,44 +587,72 @@ sub can_edit_product {
}
sub can_see_bug {
my ($self, $bugid) = @_;
my ($self, $bug_id) = @_;
return @{ $self->visible_bugs([$bug_id]) } ? 1 : 0;
}
sub visible_bugs {
my ($self, $bugs) = @_;
# Allow users to pass in Bug objects and bug ids both.
my @bug_ids = map { blessed $_ ? $_->id : $_ } @$bugs;
# We only check the visibility of bugs that we haven't
# checked yet.
# Bugzilla::Bug->update automatically removes updated bugs
# from the cache to force them to be checked again.
my $visible_cache = $self->{_visible_bugs_cache} ||= {};
my @check_ids = grep(!exists $visible_cache->{$_}, @bug_ids);
if (@check_ids) {
my $dbh = Bugzilla->dbh;
my $sth = $self->{sthCanSeeBug};
my $userid = $self->id;
# Get fields from bug, presence of user on cclist, and determine if
# the user is missing any groups required by the bug. The prepared query
# is cached because this may be called for every row in buglists or
# every bug in a dependency list.
unless ($sth) {
$sth = $dbh->prepare("SELECT 1, reporter, assigned_to, qa_contact,
reporter_accessible, cclist_accessible,
COUNT(cc.who), COUNT(bug_group_map.bug_id)
my $user_id = $self->id;
my $sth;
# Speed up the can_see_bug case.
if (scalar(@check_ids) == 1) {
$sth = $self->{_sth_one_visible_bug};
}
$sth ||= $dbh->prepare(
# This checks for groups that the bug is in that the user
# *isn't* in. Then, in the Perl code below, we check if
# the user can otherwise access the bug (for example, by being
# the assignee or QA Contact).
#
# The DISTINCT exists because the bug could be in *several*
# groups that the user isn't in, but they will all return the
# same result for bug_group_map.bug_id (so DISTINCT filters
# out duplicate rows).
"SELECT DISTINCT bugs.bug_id, reporter, assigned_to, qa_contact,
reporter_accessible, cclist_accessible, cc.who,
bug_group_map.bug_id
FROM bugs
LEFT JOIN cc
ON cc.bug_id = bugs.bug_id
AND cc.who = $userid
AND cc.who = $user_id
LEFT JOIN bug_group_map
ON bugs.bug_id = bug_group_map.bug_id
AND bug_group_map.group_ID NOT IN(" .
$self->groups_as_string .
") WHERE bugs.bug_id = ?
AND creation_ts IS NOT NULL " .
$dbh->sql_group_by('bugs.bug_id', 'reporter, ' .
'assigned_to, qa_contact, reporter_accessible, ' .
'cclist_accessible'));
}
$sth->execute($bugid);
my ($ready, $reporter, $owner, $qacontact, $reporter_access, $cclist_access,
$isoncclist, $missinggroup) = $sth->fetchrow_array();
$sth->finish;
$self->{sthCanSeeBug} = $sth;
return ($ready
&& ((($reporter == $userid) && $reporter_access)
AND bug_group_map.group_id NOT IN ("
. $self->groups_as_string . ')
WHERE bugs.bug_id IN (' . join(',', ('?') x @check_ids) . ')
AND creation_ts IS NOT NULL ');
if (scalar(@check_ids) == 1) {
$self->{_sth_one_visible_bug} = $sth;
}
$sth->execute(@check_ids);
while (my $row = $sth->fetchrow_arrayref) {
my ($bug_id, $reporter, $owner, $qacontact, $reporter_access,
$cclist_access, $isoncclist, $missinggroup) = @$row;
$visible_cache->{$bug_id} ||=
((($reporter == $user_id) && $reporter_access)
|| (Bugzilla->params->{'useqacontact'}
&& $qacontact && ($qacontact == $userid))
|| ($owner == $userid)
&& $qacontact && ($qacontact == $user_id))
|| ($owner == $user_id)
|| ($isoncclist && $cclist_access)
|| (!$missinggroup)));
|| !$missinggroup) ? 1 : 0;
}
}
return [grep { $visible_cache->{blessed $_ ? $_->id : $_} } @$bugs];
}
sub can_see_product {
@ -677,20 +702,12 @@ sub get_selectable_products {
sub get_selectable_classifications {
my ($self) = @_;
if (defined $self->{selectable_classifications}) {
return $self->{selectable_classifications};
}
if (!defined $self->{selectable_classifications}) {
my $products = $self->get_selectable_products;
my %class_ids = map { $_->classification_id => 1 } @$products;
my $class;
foreach my $product (@$products) {
$class->{$product->classification_id} ||=
new Bugzilla::Classification($product->classification_id);
$self->{selectable_classifications} = Bugzilla::Classification->new_from_list([keys %class_ids]);
}
my @sorted_class = sort {$a->sortkey <=> $b->sortkey
|| lc($a->name) cmp lc($b->name)} (values %$class);
$self->{selectable_classifications} = \@sorted_class;
return $self->{selectable_classifications};
}
@ -839,7 +856,7 @@ sub visible_groups_inherited {
return $self->{visible_groups_inherited} if defined $self->{visible_groups_inherited};
return [] unless $self->id;
my @visgroups = @{$self->visible_groups_direct};
@visgroups = @{$self->flatten_group_membership(@visgroups)};
@visgroups = @{Bugzilla::Group->flatten_group_membership(@visgroups)};
$self->{visible_groups_inherited} = \@visgroups;
return $self->{visible_groups_inherited};
}
@ -856,7 +873,7 @@ sub visible_groups_direct {
my $sth;
if (Bugzilla->params->{'usevisibilitygroups'}) {
my $glist = join(',',(-1,values(%{$self->groups})));
my $glist = $self->groups_as_string;
$sth = $dbh->prepare("SELECT DISTINCT grantor_id
FROM group_group_map
WHERE member_id IN($glist)
@ -901,7 +918,7 @@ sub queryshare_groups {
}
}
else {
@queryshare_groups = values(%{$self->groups});
@queryshare_groups = map { $_->id } @{ $self->groups };
}
}
@ -1020,36 +1037,12 @@ sub can_bless {
if (!scalar(@_)) {
# If we're called without an argument, just return
# whether or not we can bless at all.
return scalar(@{$self->bless_groups}) ? 1 : 0;
return scalar(@{ $self->bless_groups }) ? 1 : 0;
}
# Otherwise, we're checking a specific group
my $group_id = shift;
return (grep {$$_{'id'} eq $group_id} (@{$self->bless_groups})) ? 1 : 0;
}
sub flatten_group_membership {
my ($self, @groups) = @_;
my $dbh = Bugzilla->dbh;
my $sth;
my @groupidstocheck = @groups;
my %groupidschecked = ();
$sth = $dbh->prepare("SELECT member_id FROM group_group_map
WHERE grantor_id = ?
AND grant_type = " . GROUP_MEMBERSHIP);
while (my $node = shift @groupidstocheck) {
$sth->execute($node);
my $member;
while (($member) = $sth->fetchrow_array) {
if (!$groupidschecked{$member}) {
$groupidschecked{$member} = 1;
push @groupidstocheck, $member;
push @groups, $member unless grep $_ == $member, @groups;
}
}
}
return \@groups;
return grep($_->id == $group_id, @{ $self->bless_groups }) ? 1 : 0;
}
sub match {
@ -1121,7 +1114,6 @@ sub match {
&& (Bugzilla->params->{'usermatchmode'} eq 'search')
&& (length($str) >= 3))
{
$str = lc($str);
trick_taint($str);
my $query = "SELECT DISTINCT login_name FROM profiles ";
@ -1130,8 +1122,8 @@ sub match {
ON user_group_map.user_id = profiles.userid ";
}
$query .= " WHERE (" .
$dbh->sql_position('?', 'LOWER(login_name)') . " > 0" . " OR " .
$dbh->sql_position('?', 'LOWER(realname)') . " > 0) ";
$dbh->sql_iposition('?', 'login_name') . " > 0" . " OR " .
$dbh->sql_iposition('?', 'realname') . " > 0) ";
if (Bugzilla->params->{'usevisibilitygroups'}) {
$query .= " AND isbless = 0" .
" AND group_id IN(" .
@ -1429,8 +1421,9 @@ our %names_to_events = (
# Note: the "+" signs before the constants suppress bareword quoting.
sub wants_bug_mail {
my $self = shift;
my ($bug_id, $relationship, $fieldDiffs, $commentField, $dependencyText,
my ($bug_id, $relationship, $fieldDiffs, $comments, $dependencyText,
$changer, $bug_is_new) = @_;
my $comments_concatenated = join("\n", map { $_->{body} } (@$comments));
# Make a list of the events which have happened during this bug change,
# from the point of view of this user.
@ -1465,20 +1458,24 @@ sub wants_bug_mail {
}
}
if ($bug_is_new) {
# Notify about new bugs.
$events{+EVT_BUG_CREATED} = 1;
# You role is new if the bug itself is.
# Only makes sense for the assignee, QA contact and the CC list.
if ($bug_is_new
&& ($relationship == REL_ASSIGNEE
if ($relationship == REL_ASSIGNEE
|| $relationship == REL_QA
|| $relationship == REL_CC))
|| $relationship == REL_CC)
{
$events{+EVT_ADDED_REMOVED} = 1;
}
}
if ($commentField =~ /Created an attachment \(/) {
if ($comments_concatenated =~ /Created an attachment \(/) {
$events{+EVT_ATTACHMENT} = 1;
}
elsif ($commentField ne '') {
elsif (defined($$comments[0])) {
$events{+EVT_COMMENT} = 1;
}
@ -1587,6 +1584,17 @@ sub is_global_watcher {
return $self->{'is_global_watcher'};
}
sub is_timetracker {
my $self = shift;
if (!defined $self->{'is_timetracker'}) {
my $tt_group = Bugzilla->params->{'timetrackinggroup'};
$self->{'is_timetracker'} =
($tt_group && $self->in_group($tt_group)) ? 1 : 0;
}
return $self->{'is_timetracker'};
}
sub get_userlist {
my $self = shift;
@ -1925,12 +1933,15 @@ value - the value of this setting for this user. Will be the same
is_default - a boolean to indicate whether the user has chosen to make
a preference for themself or use the site default.
=item C<timezone>
Returns the timezone used to display dates and times to the user,
as a DateTime::TimeZone object.
=item C<groups>
Returns a hashref of group names for groups the user is a member of. The keys
are the names of the groups, whilst the values are the respective group ids.
(This is so that a set of all groupids for groups the user is in can be
obtained by C<values(%{$user-E<gt>groups})>.)
Returns an arrayref of L<Bugzilla::Group> objects representing
groups that this user is a member of.
=item C<groups_as_string>
@ -1950,12 +1961,11 @@ Determines whether or not a user is in the given group by id.
=item C<bless_groups>
Returns an arrayref of hashes of C<groups> entries, where the keys of each hash
are the names of C<id>, C<name> and C<description> columns of the C<groups>
table.
Returns an arrayref of L<Bugzilla::Group> objects.
The arrayref consists of the groups the user can bless, taking into account
that having editusers permissions means that you can bless all groups, and
that you need to be aware of a group in order to bless a group.
that you need to be able to see a group in order to bless it.
=item C<get_products_by_permission($group)>
@ -2083,14 +2093,6 @@ Returns a reference to an array of users. The array is populated with hashrefs
containing the login, identity and visibility. Users that are not visible to this
user will have 'visible' set to zero.
=item C<flatten_group_membership>
Accepts a list of groups and returns a list of all the groups whose members
inherit membership in any group on the list. So, we can determine if a user
is in any of the groups input to flatten_group_membership by querying the
user_group_map for any user with DIRECT or REGEXP membership IN() the list
of groups returned.
=item C<direct_group_membership>
Returns a reference to an array of group objects. Groups the user belong to

View File

@ -0,0 +1,71 @@
# -*- 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 Frédéric Buclin.
# Portions created by Frédéric Buclin are Copyright (c) 2008 Frédéric Buclin.
# All rights reserved.
#
# Contributor(s): Frédéric Buclin <LpSolit@gmail.com>
package Bugzilla::User::Setting::Timezone;
use strict;
use DateTime::TimeZone;
use base qw(Bugzilla::User::Setting);
use Bugzilla::Constants;
sub legal_values {
my ($self) = @_;
return $self->{'legal_values'} if defined $self->{'legal_values'};
my @timezones = DateTime::TimeZone->all_names;
# Remove old formats, such as CST6CDT, EST, EST5EDT.
@timezones = grep { $_ =~ m#.+/.+#} @timezones;
# Append 'local' to the list, which will use the timezone
# given by the server.
push(@timezones, 'local');
return $self->{'legal_values'} = \@timezones;
}
1;
__END__
=head1 NAME
Bugzilla::User::Setting::Timezone - Object for a user preference setting for desired timezone
=head1 DESCRIPTION
Timezone.pm extends Bugzilla::User::Setting and implements a class specialized for
setting the desired timezone.
=head1 METHODS
=over
=item C<legal_values()>
Description: Returns all legal timezones
Params: none
Returns: A reference to an array containing the names of all legal timezones
=back

View File

@ -31,13 +31,13 @@ package Bugzilla::Util;
use strict;
use base qw(Exporter);
@Bugzilla::Util::EXPORT = qw(is_tainted trick_taint detaint_natural
@Bugzilla::Util::EXPORT = qw(trick_taint detaint_natural
detaint_signed
html_quote url_quote xml_quote
css_class_quote html_light_quote url_decode
i_am_cgi get_netaddr correct_urlbase
lsearch ssl_require_redirect use_attachbase
diff_arrays diff_strings
diff_arrays
trim wrap_hard wrap_comment find_wrap_point
format_time format_time_decimal validate_date
validate_time
@ -50,19 +50,14 @@ use Bugzilla::Constants;
use Date::Parse;
use Date::Format;
use DateTime;
use DateTime::TimeZone;
use Digest;
use Email::Address;
use Scalar::Util qw(tainted);
use Text::Wrap;
use Lingua::Stem::RuUTF8;
# This is from the perlsec page, slightly modified to remove a warning
# From that page:
# This function makes use of the fact that the presence of
# tainted data anywhere within an expression renders the
# entire expression tainted.
# Don't ask me how it works...
sub is_tainted {
return not eval { my $foo = join('',@_), kill 0; 1; };
}
sub trick_taint {
require Carp;
Carp::confess("Undef to trick_taint") unless defined $_[0];
@ -177,6 +172,20 @@ sub html_light_quote {
}
}
sub email_filter {
my ($toencode) = @_;
if (!Bugzilla->user->id) {
my @emails = Email::Address->parse($toencode);
if (scalar @emails) {
my @hosts = map { quotemeta($_->host) } @emails;
my $hosts_re = join('|', @hosts);
$toencode =~ s/\@(?:$hosts_re)//g;
return $toencode;
}
}
return $toencode;
}
# This originally came from CGI.pm, by Lincoln D. Stein
sub url_quote {
my ($toencode) = (@_);
@ -337,22 +346,6 @@ sub trim {
return $str;
}
sub diff_strings {
my ($oldstr, $newstr) = @_;
# Split the old and new strings into arrays containing their values.
s/[\s,]+/ /gso for $oldstr, $newstr;
my @old = split(" ", $oldstr);
my @new = split(" ", $newstr);
my ($rem, $add) = diff_arrays(\@old, \@new);
my $removed = join (", ", @$rem);
my $added = join (", ", @$add);
return ($removed, $added);
}
sub wrap_comment {
my ($comment, $cols) = @_;
my $wrappedcomment = "";
@ -414,38 +407,55 @@ sub wrap_hard {
}
sub format_time {
my ($date, $format) = @_;
my ($date, $format, $timezone) = @_;
# If $format is undefined, try to guess the correct date format.
my $show_timezone;
if (!defined($format)) {
if ($date =~ m/^(\d{4})[-\.](\d{2})[-\.](\d{2}) (\d{2}):(\d{2})(:(\d{2}))?$/) {
my $sec = $7;
if (defined $sec) {
$format = "%Y-%m-%d %T";
$format = "%Y-%m-%d %T %Z";
} else {
$format = "%Y-%m-%d %R";
$format = "%Y-%m-%d %R %Z";
}
} else {
# Default date format. See Date::Format for other formats available.
$format = "%Y-%m-%d %R";
# Default date format. See DateTime for other formats available.
$format = "%Y-%m-%d %R %Z";
}
# By default, we want the timezone to be displayed.
$show_timezone = 1;
}
else {
# Search for %Z or %z, meaning we want the timezone to be displayed.
# Till bug 182238 gets fixed, we assume Bugzilla->params->{'timezone'}
# is used.
$show_timezone = ($format =~ s/\s?%Z$//i);
}
# str2time($date) is undefined if $date has an invalid date format.
my $time = str2time($date);
# strptime($date) returns an empty array if $date has an invalid date format.
my @time = strptime($date);
if (defined $time) {
$date = time2str($format, $time);
$date .= " " . Bugzilla->params->{'timezone'} if $show_timezone;
unless (scalar @time) {
# If an unknown timezone is passed (such as MSK, for Moskow), strptime() is
# unable to parse the date. We try again, but we first remove the timezone.
$date =~ s/\s+\S+$//;
@time = strptime($date);
}
if (scalar @time) {
# Fix a bug in strptime() where seconds can be undefined in some cases.
$time[0] ||= 0;
# strptime() counts years from 1900, and months from 0 (January).
# We have to fix both values.
my $dt = DateTime->new({year => 1900 + $time[5],
month => ++$time[4],
day => $time[3],
hour => $time[2],
minute => $time[1],
# DateTime doesn't like fractional seconds.
second => int($time[0]),
# If importing, use the specified timezone, otherwise
# use the timezone specified by the server.
time_zone => Bugzilla->local_timezone->offset_as_string($time[6])
|| Bugzilla->local_timezone});
# Now display the date using the given timezone,
# or the user's timezone if none is given.
$dt->set_time_zone($timezone || Bugzilla->user->timezone);
$date = $dt->strftime($format);
}
else {
# Don't let invalid (time) strings to be passed to templates!
@ -475,33 +485,56 @@ sub file_mod_time {
}
sub bz_crypt {
my ($password) = @_;
my ($password, $salt) = @_;
# The list of characters that can appear in a salt. Salts and hashes
# are both encoded as a sequence of characters from a set containing
# 64 characters, each one of which represents 6 bits of the salt/hash.
# The encoding is similar to BASE64, the difference being that the
# BASE64 plus sign (+) is replaced with a forward slash (/).
my @saltchars = (0..9, 'A'..'Z', 'a'..'z', '.', '/');
# Generate the salt. We use an 8 character (48 bit) salt for maximum
# security on systems whose crypt uses MD5. Systems with older
# versions of crypt will just use the first two characters of the salt.
my $salt = '';
for ( my $i=0 ; $i < 8 ; ++$i ) {
$salt .= $saltchars[rand(64)];
my $algorithm;
if (!defined $salt) {
# If you don't use a salt, then people can create tables of
# hashes that map to particular passwords, and then break your
# hashing very easily if they have a large-enough table of common
# (or even uncommon) passwords. So we generate a unique salt for
# each password in the database, and then just prepend it to
# the hash.
$salt = generate_random_password(PASSWORD_SALT_LENGTH);
$algorithm = PASSWORD_DIGEST_ALGORITHM;
}
# We append the algorithm used to the string. This is good because then
# we can change the algorithm being used, in the future, without
# disrupting the validation of existing passwords. Also, this tells
# us if a password is using the old "crypt" method of hashing passwords,
# because the algorithm will be missing from the string.
if ($salt =~ /{([^}]+)}$/) {
$algorithm = $1;
}
my $crypted_password;
if (!$algorithm) {
# Wide characters cause crypt to die
if (Bugzilla->params->{'utf8'}) {
utf8::encode($password) if utf8::is_utf8($password);
}
# Crypt the password.
my $cryptedpassword = crypt($password, $salt);
$crypted_password = crypt($password, $salt);
# HACK: Perl has bug where returned crypted password is considered
# tainted. See http://rt.perl.org/rt3/Public/Bug/Display.html?id=59998
unless(tainted($password) || tainted($salt)) {
trick_taint($crypted_password);
}
}
else {
my $hasher = Digest->new($algorithm);
# We only want to use the first characters of the salt, no
# matter how long of a salt we may have been passed.
$salt = substr($salt, 0, PASSWORD_SALT_LENGTH);
$hasher->add($password, $salt);
$crypted_password = $salt . $hasher->b64digest . "{$algorithm}";
}
# Return the crypted password.
return $cryptedpassword;
return $crypted_password;
}
sub generate_random_password {
@ -637,7 +670,6 @@ Bugzilla::Util - Generic utility functions for bugzilla
use Bugzilla::Util;
# Functions for dealing with variable tainting
$rv = is_tainted($var);
trick_taint($var);
detaint_natural($var);
detaint_signed($var);
@ -646,6 +678,7 @@ Bugzilla::Util - Generic utility functions for bugzilla
html_quote($var);
url_quote($var);
xml_quote($var);
email_filter($var);
# Functions for decoding
$rv = url_decode($var);
@ -663,7 +696,6 @@ Bugzilla::Util - Generic utility functions for bugzilla
# Functions for manipulating strings
$val = trim(" abc ");
($removed, $added) = diff_strings($old, $new);
$wrapped = wrap_comment($comment);
# Functions for formatting time
@ -701,10 +733,6 @@ with care> to avoid security holes.
=over 4
=item C<is_tainted>
Determines whether a particular variable is tainted
=item C<trick_taint($val)>
Tricks perl into untainting a particular variable.
@ -767,6 +795,12 @@ is kept separate from html_quote partly for compatibility with previous code
Converts the %xx encoding from the given URL back to its original form.
=item C<email_filter>
Removes the hostname from email addresses in the string, if the user
currently viewing Bugzilla is logged out. If the user is logged-in,
this filter just returns the input string.
=back
=head2 Environment and Location
@ -842,14 +876,6 @@ If the item is not in the list, returns -1.
Removes any leading or trailing whitespace from a string. This routine does not
modify the existing string.
=item C<diff_strings($oldstr, $newstr)>
Takes two strings containing a list of comma- or space-separated items
and returns what items were removed from or added to the new one,
compared to the old one. Returns a list, where the first entry is a scalar
containing removed items, and the second entry is a scalar containing added
items.
=item C<wrap_hard($string, $size)>
Wraps a string, so that a line is I<never> longer than C<$size>.
@ -920,15 +946,13 @@ A string.
=item C<format_time($time)>
Takes a time, converts it to the desired format and appends the timezone
as defined in editparams.cgi, if desired. This routine will be expanded
in the future to adjust for user preferences regarding what timezone to
display times in.
Takes a time and converts it to the desired format and timezone.
If no format is given, the routine guesses the correct one and returns
an empty array if it cannot. If no timezone is given, the user's timezone
is used, as defined in his preferences.
This routine is mainly called from templates to filter dates, see
"FILTER time" in Templates.pm. In this case, $format is undefined and
the routine has to "guess" the date format that was passed to $dbh->sql_date_format().
"FILTER time" in L<Bugzilla::Template>.
=item C<format_time_decimal($time)>
@ -953,12 +977,14 @@ of the "mtime" parameter of the perl "stat" function.
=over 4
=item C<bz_crypt($password)>
=item C<bz_crypt($password, $salt)>
Takes a string and returns a C<crypt>ed value for it, using a random salt.
Takes a string and returns a hashed (encrypted) value for it, using a
random salt. An optional salt string may also be passed in.
Please always use this function instead of the built-in perl "crypt"
when initially encrypting a password.
Please always use this function instead of the built-in perl C<crypt>
function, when checking or setting a password. Bugzilla does not use
C<crypt>.
=begin undocumented

View File

@ -14,21 +14,16 @@
#
# Contributor(s): Marc Schumann <wurblzap@gmail.com>
# Max Kanat-Alexander <mkanat@bugzilla.org>
# Rosie Clarkson <rosie.clarkson@planningportal.gov.uk>
#
# Portions © Crown copyright 2009 - Rosie Clarkson (development@planningportal.gov.uk) for the Planning Portal
# This is the base class for $self in WebService method calls. For the
# actual RPC server, see Bugzilla::WebService::Server and its subclasses.
package Bugzilla::WebService;
use strict;
use Bugzilla::WebService::Constants;
use Bugzilla::Util;
use Date::Parse;
sub fail_unimplemented {
my $this = shift;
die SOAP::Fault
->faultcode(ERROR_UNIMPLEMENTED)
->faultstring('Service Unimplemented');
}
use XMLRPC::Lite;
sub datetime_format {
my ($self, $date_string) = @_;
@ -42,32 +37,6 @@ sub datetime_format {
return $iso_datetime;
}
sub handle_login {
my ($classes, $action, $uri, $method) = @_;
my $class = $classes->{$uri};
eval "require $class";
return if $class->login_exempt($method);
Bugzilla->login();
# Even though we check for the need to redirect in
# Bugzilla->login() we check here again since Bugzilla->login()
# does not know what the current XMLRPC method is. Therefore
# ssl_require_redirect in Bugzilla->login() will have returned
# false if system was configured to redirect for authenticated
# sessions and the user was not yet logged in.
# So here we pass in the method name to ssl_require_redirect so
# it can then check for the extra case where the method equals
# User.login, which we would then need to redirect if not
# over a secure connection.
my $full_method = $uri . "." . $method;
Bugzilla->cgi->require_https(Bugzilla->params->{'sslbase'})
if ssl_require_redirect($full_method);
return;
}
# For some methods, we shouldn't call Bugzilla->login before we call them
use constant LOGIN_EXEMPT => { };
@ -77,64 +46,12 @@ sub login_exempt {
return $class->LOGIN_EXEMPT->{$method};
}
1;
package Bugzilla::WebService::XMLRPC::Transport::HTTP::CGI;
use strict;
eval { require XMLRPC::Transport::HTTP; };
our @ISA = qw(XMLRPC::Transport::HTTP::CGI);
sub initialize {
my $self = shift;
my %retval = $self->SUPER::initialize(@_);
$retval{'serializer'} = Bugzilla::WebService::XMLRPC::Serializer->new;
return %retval;
}
sub make_response {
my $self = shift;
$self->SUPER::make_response(@_);
# XMLRPC::Transport::HTTP::CGI doesn't know about Bugzilla carrying around
# its cookies in Bugzilla::CGI, so we need to copy them over.
foreach (@{Bugzilla->cgi->{'Bugzilla_cookie_list'}}) {
$self->response->headers->push_header('Set-Cookie', $_);
sub type {
my ($self, $type, $value) = @_;
if ($type eq 'dateTime') {
$value = $self->datetime_format($value);
}
}
1;
# This package exists to fix a UTF-8 bug in SOAP::Lite.
# See http://rt.cpan.org/Public/Bug/Display.html?id=32952.
package Bugzilla::WebService::XMLRPC::Serializer;
use strict;
# We can't use "use base" because XMLRPC::Serializer doesn't return
# a true value.
eval { require XMLRPC::Lite; };
our @ISA = qw(XMLRPC::Serializer);
sub new {
my $class = shift;
my $self = $class->SUPER::new(@_);
# This fixes UTF-8.
$self->{'_typelookup'}->{'base64'} =
[10, sub { !utf8::is_utf8($_[0]) && $_[0] =~ /[^\x09\x0a\x0d\x20-\x7f]/},
'as_base64'];
# This makes arrays work right even though we're a subclass.
# (See http://rt.cpan.org//Ticket/Display.html?id=34514)
$self->{'_encodingStyle'} = '';
return $self;
}
sub as_string {
my $self = shift;
my ($value) = @_;
# Something weird happens with XML::Parser when we have upper-ASCII
# characters encoded as UTF-8, and this fixes it.
utf8::encode($value) if utf8::is_utf8($value)
&& $value =~ /^[\x00-\xff]+$/;
return $self->SUPER::as_string($value);
return XMLRPC::Data->type($type)->value($value);
}
1;
@ -285,3 +202,92 @@ an error 302, there won't be an error -302.
Sometimes a function will throw an error that doesn't have a specific
error code. In this case, the code will be C<-32000> if it's a "fatal"
error, and C<32000> if it's a "transient" error.
=head1 COMMON PARAMETERS
Many Webservice methods take similar arguments. Instead of re-writing
the documentation for each method, we document the parameters here, once,
and then refer back to this documentation from the individual methods
where these parameters are used.
=head2 Limiting What Fields Are Returned
Many WebService methods return an array of structs with various
fields in the structs. (For example, L<Bugzilla::WebService::Bug/get>
returns a list of C<bugs> that have fields like C<id>, C<summary>,
C<creation_time>, etc.)
These parameters allow you to limit what fields are present in
the structs, to possibly improve performance or save some bandwidth.
=over
=item C<include_fields> (array)
An array of strings, representing the (case-sensitive) names of fields.
Only the fields specified in this hash will be returned, the rest will
not be included.
If you specify an empty array, then this function will return empty
hashes.
Invalid field names are ignored.
Example:
User.get( ids => [1], include_fields => ['id', 'name'] )
would return something like:
{ users => [{ id => 1, name => 'user@domain.com' }] }
=item C<exclude_fields> (array)
An array of strings, representing the (case-sensitive) names of fields.
The fields specified will not be included in the returned hashes.
If you specify all the fields, then this function will return empty
hashes.
Invalid field names are ignored.
Specifying fields here overrides C<include_fields>, so if you specify a
field in both, it will be excluded, not included.
Example:
User.get( ids => [1], exclude_fields => ['name'] )
would return something like:
{ users => [{ id => 1, real_name => 'John Smith' }] }
=back
=head1 EXTENSIONS TO THE XML-RPC STANDARD
=head2 Undefined Values
Normally, XML-RPC does not allow empty values for C<int>, C<double>, or
C<dateTime.iso8601> fields. Bugzilla does--it treats empty values as
C<undef> (called C<NULL> or C<None> in some programming languages).
Bugzilla also accepts an element called C<< <nil> >>, as specified by
the XML-RPC extension here: L<http://ontosys.com/xml-rpc/extensions.php>,
which is always considered to be C<undef>, no matter what it contains.
Bugzilla does not use C<< <nil> >> values in returned data, because currently
most clients do not support C<< <nil> >>. Instead, any fields with C<undef>
values will be stripped from the response completely. Therefore
B<the client must handle the fact that some expected fields may not be
returned>.
=begin private
nil is implemented by XMLRPC::Lite, in XMLRPC::Deserializer::decode_value
in the CPAN SVN since 14th Dec 2008
L<http://rt.cpan.org/Public/Bug/Display.html?id=20569> and in Fedora's
perl-SOAP-Lite package in versions 0.68-1 and above.
=end private

File diff suppressed because it is too large Load Diff

View File

@ -22,9 +22,8 @@ use strict;
use base qw(Bugzilla::WebService);
use Bugzilla::Constants;
use Bugzilla::Hook;
import SOAP::Data qw(type);
use Time::Zone;
use DateTime;
# Basic info that is needed before logins
use constant LOGIN_EXEMPT => {
@ -33,26 +32,52 @@ use constant LOGIN_EXEMPT => {
};
sub version {
return { version => type('string')->value(BUGZILLA_VERSION) };
my $self = shift;
return { version => $self->type('string', BUGZILLA_VERSION) };
}
sub extensions {
my $self = shift;
my $extensions = Bugzilla::Hook::enabled_plugins();
foreach my $name (keys %$extensions) {
my $info = $extensions->{$name};
foreach my $data (keys %$info)
{
$extensions->{$name}->{$data} = type('string')->value($info->{$data});
foreach my $data (keys %$info) {
$extensions->{$name}->{$data} =
$self->type('string', $info->{$data});
}
}
return { extensions => $extensions };
}
sub timezone {
my $offset = tz_offset();
my $self = shift;
my $offset = Bugzilla->local_timezone->offset_for_datetime(DateTime->now());
$offset = (($offset / 60) / 60) * 100;
$offset = sprintf('%+05d', $offset);
return { timezone => type('string')->value($offset) };
return { timezone => $self->type('string', $offset) };
}
sub time {
my ($self) = @_;
my $dbh = Bugzilla->dbh;
my $db_time = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
my $now_utc = DateTime->now();
my $tz = Bugzilla->local_timezone;
my $now_local = $now_utc->clone->set_time_zone($tz);
my $tz_offset = $tz->offset_for_datetime($now_local);
return {
db_time => $self->type('dateTime', $db_time),
web_time => $self->type('dateTime', $now_local),
web_time_utc => $self->type('dateTime', $now_utc),
tz_name => $self->type('string', $tz->name),
tz_offset => $self->type('string',
$tz->offset_as_string($tz_offset)),
tz_short_name => $self->type('string',
$now_local->time_zone_short_name),
};
}
1;
@ -127,7 +152,8 @@ extension
=item C<timezone>
B<STABLE>
B<DEPRECATED> This method may be removed in a future version of Bugzilla.
Use L</time> instead.
=over
@ -146,4 +172,80 @@ string in (+/-)XXXX (RFC 2822) format.
=back
=item C<time>
B<UNSTABLE>
=over
=item B<Description>
Gets information about what time the Bugzilla server thinks it is, and
what timezone it's running in.
=item B<Params> (none)
=item B<Returns>
A struct with the following items:
=over
=item C<db_time>
C<dateTime> The current time in Bugzilla's B<local time zone>, according
to the Bugzilla I<database server>.
Note that Bugzilla assumes that the database and the webserver are running
in the same time zone. However, if the web server and the database server
aren't synchronized for some reason, I<this> is the time that you should
rely on for doing searches and other input to the WebService.
=item C<web_time>
C<dateTime> This is the current time in Bugzilla's B<local time zone>,
according to Bugzilla's I<web server>.
This might be different by a second from C<db_time> since this comes from
a different source. If it's any more different than a second, then there is
likely some problem with this Bugzilla instance. In this case you should
rely on the C<db_time>, not the C<web_time>.
=item C<web_time_utc>
The same as C<web_time>, but in the B<UTC> time zone instead of the local
time zone.
=item C<tz_name>
C<string> The long name of the time zone that the Bugzilla web server is
in. Will usually look something like: C<America/Los Angeles>
=item C<tz_short_name>
C<string> The "short name" of the time zone that the Bugzilla web server
is in. This should only be used for display, and not relied on for your
programs, because different time zones can have the same short name.
(For example, there are two C<EST>s.)
This will look something like: C<PST>.
=item C<tz_offset>
C<string> The timezone offset as a string in (+/-)XXXX (RFC 2822) format.
=back
=item B<History>
=over
=item Added in Bugzilla B<3.4>.
=back
=back
=back

View File

@ -20,13 +20,13 @@ package Bugzilla::WebService::Constants;
use strict;
use base qw(Exporter);
@Bugzilla::WebService::Constants::EXPORT = qw(
our @EXPORT = qw(
WS_ERROR_CODE
ERROR_UNKNOWN_FATAL
ERROR_UNKNOWN_TRANSIENT
ERROR_AUTH_NODATA
ERROR_UNIMPLEMENTED
WS_DISPATCH
);
# This maps the error names in global/*-error.html.tmpl to numbers.
@ -48,10 +48,12 @@ use base qw(Exporter);
# comment that it was retired. Also, if an error changes its name, you'll
# have to fix it here.
use constant WS_ERROR_CODE => {
# Generic Bugzilla::Object errors are 50-99.
object_name_not_specified => 50,
# Generic errors (Bugzilla::Object and others) are 50-99.
object_not_specified => 50,
param_required => 50,
params_required => 50,
object_does_not_exist => 51,
xmlrpc_invalid_value => 52,
# Bug errors usually occupy the 100-200 range.
improper_bug_id_field_value => 100,
bug_id_does_not_exist => 101,
@ -79,12 +81,21 @@ use constant WS_ERROR_CODE => {
invalid_field_name => 108,
# Not authorized to edit the bug
product_edit_denied => 109,
# Comment-related errors
comment_is_private => 110,
comment_id_invalid => 111,
# See Also errors
bug_url_invalid => 112,
bug_url_too_long => 112,
# Insidergroup Errors
user_not_insider => 113,
# Authentication errors are usually 300-400.
invalid_username_or_password => 300,
account_disabled => 301,
auth_invalid_email => 302,
extern_id_conflict => -303,
auth_failure => 304,
# User errors are 500-600.
account_exists => 500,
@ -97,6 +108,8 @@ use constant WS_ERROR_CODE => {
# This is from strict_isolation, but it also basically means
# "invalid user."
invalid_user_group => 504,
user_access_by_id_denied => 505,
user_access_by_match_denied => 505,
};
# These are the fallback defaults for errors not in ERROR_CODE.
@ -104,7 +117,23 @@ use constant ERROR_UNKNOWN_FATAL => -32000;
use constant ERROR_UNKNOWN_TRANSIENT => 32000;
use constant ERROR_AUTH_NODATA => 410;
use constant ERROR_UNIMPLEMENTED => 910;
use constant ERROR_GENERAL => 999;
sub WS_DISPATCH {
# We "require" here instead of "use" above to avoid a dependency loop.
require Bugzilla::Hook;
my %hook_dispatch;
Bugzilla::Hook::process('webservice', { dispatch => \%hook_dispatch });
my $dispatch = {
'Bugzilla' => 'Bugzilla::WebService::Bugzilla',
'Bug' => 'Bugzilla::WebService::Bug',
'User' => 'Bugzilla::WebService::User',
'Product' => 'Bugzilla::WebService::Product',
%hook_dispatch
};
return $dispatch;
};
1;

View File

@ -21,7 +21,7 @@ use strict;
use base qw(Bugzilla::WebService);
use Bugzilla::Product;
use Bugzilla::User;
import SOAP::Data qw(type);
use Bugzilla::WebService::Util qw(validate);
##################################################
# Add aliases here for method name compatibility #
@ -46,7 +46,7 @@ sub get_accessible_products {
# Get a list of actual products, based on list of ids
sub get {
my ($self, $params) = @_;
my ($self, $params) = validate(@_, 'ids');
# Only products that are in the users accessible products,
# can be allowed to be returned
@ -63,9 +63,9 @@ sub get {
my @products =
map {{
internals => $_,
id => type('int')->value($_->id),
name => type('string')->value($_->name),
description => type('string')->value($_->description),
id => $self->type('int', $_->id),
name => $self->type('string', $_->name),
description => $self->type('string', $_->description),
}
} @requested_accessible;

View File

@ -0,0 +1,42 @@
# -*- 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.
#
# Contributor(s): Marc Schumann <wurblzap@gmail.com>
# Max Kanat-Alexander <mkanat@bugzilla.org>
package Bugzilla::WebService::Server;
use strict;
use Bugzilla::Util qw(ssl_require_redirect);
sub handle_login {
my ($self, $class, $method, $full_method) = @_;
eval "require $class";
return if $class->login_exempt($method);
Bugzilla->login();
# Even though we check for the need to redirect in
# Bugzilla->login() we check here again since Bugzilla->login()
# does not know what the current XMLRPC method is. Therefore
# ssl_require_redirect in Bugzilla->login() will have returned
# false if system was configured to redirect for authenticated
# sessions and the user was not yet logged in.
# So here we pass in the method name to ssl_require_redirect so
# it can then check for the extra case where the method equals
# User.login, which we would then need to redirect if not
# over a secure connection.
Bugzilla->cgi->require_https(Bugzilla->params->{'sslbase'})
if ssl_require_redirect($full_method);
}
1;

View File

@ -0,0 +1,254 @@
# -*- 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.
#
# Contributor(s): Marc Schumann <wurblzap@gmail.com>
# Max Kanat-Alexander <mkanat@bugzilla.org>
# Rosie Clarkson <rosie.clarkson@planningportal.gov.uk>
#
# Portions © Crown copyright 2009 - Rosie Clarkson (development@planningportal.gov.uk) for the Planning Portal
package Bugzilla::WebService::Server::XMLRPC;
use strict;
use XMLRPC::Transport::HTTP;
use Bugzilla::WebService::Server;
our @ISA = qw(XMLRPC::Transport::HTTP::CGI Bugzilla::WebService::Server);
use Bugzilla::WebService::Constants;
sub initialize {
my $self = shift;
my %retval = $self->SUPER::initialize(@_);
$retval{'serializer'} = Bugzilla::XMLRPC::Serializer->new;
$retval{'deserializer'} = Bugzilla::XMLRPC::Deserializer->new;
$retval{'dispatch_with'} = WS_DISPATCH;
return %retval;
}
sub make_response {
my $self = shift;
$self->SUPER::make_response(@_);
# XMLRPC::Transport::HTTP::CGI doesn't know about Bugzilla carrying around
# its cookies in Bugzilla::CGI, so we need to copy them over.
foreach (@{Bugzilla->cgi->{'Bugzilla_cookie_list'}}) {
$self->response->headers->push_header('Set-Cookie', $_);
}
}
sub datetime_format {
my ($self, $date_string) = @_;
my $time = str2time($date_string);
my ($sec, $min, $hour, $mday, $mon, $year) = localtime $time;
# This format string was stolen from SOAP::Utils->format_datetime,
# which doesn't work but which has almost the right format string.
my $iso_datetime = sprintf('%d%02d%02dT%02d:%02d:%02d',
$year + 1900, $mon + 1, $mday, $hour, $min, $sec);
return $iso_datetime;
}
sub handle_login {
my ($self, $classes, $action, $uri, $method) = @_;
my $class = $classes->{$uri};
my $full_method = $uri . "." . $method;
$self->SUPER::handle_login($class, $method, $full_method);
return;
}
1;
# This exists to validate input parameters (which XMLRPC::Lite doesn't do)
# and also, in some cases, to more-usefully decode them.
package Bugzilla::XMLRPC::Deserializer;
use strict;
# We can't use "use base" because XMLRPC::Serializer doesn't return
# a true value.
eval { require XMLRPC::Lite; };
our @ISA = qw(XMLRPC::Deserializer);
use Bugzilla::Error;
# Some method arguments need to be converted in some way, when they are input.
sub decode_value {
my $self = shift;
my ($type) = @{ $_[0] };
my $value = $self->SUPER::decode_value(@_);
# We only validate/convert certain types here.
return $value if $type !~ /^(?:int|i4|boolean|double|dateTime\.iso8601)$/;
# Though the XML-RPC standard doesn't allow an empty <int>,
# <double>,or <dateTime.iso8601>, we do, and we just say
# "that's undef".
if (grep($type eq $_, qw(int double dateTime))) {
return undef if $value eq '';
}
my $validator = $self->_validation_subs->{$type};
if (!$validator->($value)) {
ThrowUserError('xmlrpc_invalid_value',
{ type => $type, value => $value });
}
# We convert dateTimes to a DB-friendly date format.
if ($type eq 'dateTime.iso8601') {
# We leave off the $ from the end of this regex to allow for possible
# extensions to the XML-RPC date standard.
$value =~ /^(\d{4})(\d{2})(\d{2})T(\d{2}):(\d{2}):(\d{2})/;
$value = "$1-$2-$3 $4:$5:$6";
}
return $value;
}
sub _validation_subs {
my $self = shift;
return $self->{_validation_subs} if $self->{_validation_subs};
# The only place that XMLRPC::Lite stores any sort of validation
# regex is in XMLRPC::Serializer. We want to re-use those regexes here.
my $lookup = Bugzilla::XMLRPC::Serializer->new->typelookup;
# $lookup is a hash whose values are arrayrefs, and whose keys are the
# names of types. The second item of each arrayref is a subroutine
# that will do our validation for us.
my %validators = map { $_ => $lookup->{$_}->[1] } (keys %$lookup);
# Add a boolean validator
$validators{'boolean'} = sub {$_[0] =~ /^[01]$/};
# Some types have multiple names, or have a different name in
# XMLRPC::Serializer than their standard XML-RPC name.
$validators{'dateTime.iso8601'} = $validators{'dateTime'};
$validators{'i4'} = $validators{'int'};
$self->{_validation_subs} = \%validators;
return \%validators;
}
1;
# This package exists to fix a UTF-8 bug in SOAP::Lite.
# See http://rt.cpan.org/Public/Bug/Display.html?id=32952.
package Bugzilla::XMLRPC::Serializer;
use Scalar::Util qw(blessed);
use strict;
# We can't use "use base" because XMLRPC::Serializer doesn't return
# a true value.
eval { require XMLRPC::Lite; };
our @ISA = qw(XMLRPC::Serializer);
sub new {
my $class = shift;
my $self = $class->SUPER::new(@_);
# This fixes UTF-8.
$self->{'_typelookup'}->{'base64'} =
[10, sub { !utf8::is_utf8($_[0]) && $_[0] =~ /[^\x09\x0a\x0d\x20-\x7f]/},
'as_base64'];
# This makes arrays work right even though we're a subclass.
# (See http://rt.cpan.org//Ticket/Display.html?id=34514)
$self->{'_encodingStyle'} = '';
return $self;
}
sub as_string {
my $self = shift;
my ($value) = @_;
# Something weird happens with XML::Parser when we have upper-ASCII
# characters encoded as UTF-8, and this fixes it.
utf8::encode($value) if utf8::is_utf8($value)
&& $value =~ /^[\x00-\xff]+$/;
return $self->SUPER::as_string($value);
}
# Here the XMLRPC::Serializer is extended to use the XMLRPC nil extension.
sub encode_object {
my $self = shift;
my @encoded = $self->SUPER::encode_object(@_);
return $encoded[0]->[0] eq 'nil'
? ['value', {}, [@encoded]]
: @encoded;
}
# Removes undefined values so they do not produce invalid XMLRPC.
sub envelope {
my $self = shift;
my ($type, $method, $data) = @_;
# If the type isn't a successful response we don't want to change the values.
if ($type eq 'response'){
$data = _strip_undefs($data);
}
return $self->SUPER::envelope($type, $method, $data);
}
# In an XMLRPC response we have to handle hashes of arrays, hashes, scalars,
# Bugzilla objects (reftype = 'HASH') and XMLRPC::Data objects.
# The whole XMLRPC::Data object must be removed if its value key is undefined
# so it cannot be recursed like the other hash type objects.
sub _strip_undefs {
my ($initial) = @_;
if (ref $initial eq "HASH" || (blessed $initial && $initial->isa("HASH"))) {
while (my ($key, $value) = each(%$initial)) {
if ( !defined $value
|| (blessed $value && $value->isa('XMLRPC::Data') && !defined $value->value) )
{
# If the value is undefined remove it from the hash.
delete $initial->{$key};
}
else {
$initial->{$key} = _strip_undefs($value);
}
}
}
if (ref $initial eq "ARRAY" || (blessed $initial && $initial->isa("ARRAY"))) {
for (my $count = 0; $count < scalar @{$initial}; $count++) {
my $value = $initial->[$count];
if ( !defined $value
|| (blessed $value && $value->isa('XMLRPC::Data') && !defined $value->value) )
{
# If the value is undefined remove it from the array.
splice(@$initial, $count, 1);
$count--;
}
else {
$initial->[$count] = _strip_undefs($value);
}
}
}
return $initial;
}
sub BEGIN {
no strict 'refs';
for my $type (qw(double i4 int dateTime)) {
my $method = 'as_' . $type;
*$method = sub {
my ($self, $value) = @_;
if (!defined($value)) {
return as_nil();
}
else {
my $super_method = "SUPER::$method";
return $self->$super_method($value);
}
}
}
}
sub as_nil {
return ['nil', {}];
}
1;

View File

@ -15,20 +15,20 @@
# Contributor(s): Marc Schumann <wurblzap@gmail.com>
# Max Kanat-Alexander <mkanat@bugzilla.org>
# Mads Bondo Dydensborg <mbd@dbc.dk>
# Noura Elhawary <nelhawar@redhat.com>
package Bugzilla::WebService::User;
use strict;
use base qw(Bugzilla::WebService);
import SOAP::Data qw(type);
use Bugzilla;
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::User;
use Bugzilla::Util qw(trim);
use Bugzilla::Token;
use Bugzilla::WebService::Util qw(filter validate);
# Don't need auth to login
use constant LOGIN_EXEMPT => {
@ -67,7 +67,7 @@ sub login {
$cgi->param('Bugzilla_remember', $remember);
Bugzilla->login;
return { id => type('int')->value(Bugzilla->user->id) };
return { id => $self->type('int', Bugzilla->user->id) };
}
sub logout {
@ -122,7 +122,105 @@ sub create {
cryptpassword => $password
});
return { id => type('int')->value($user->id) };
return { id => $self->type('int', $user->id) };
}
# function to return user information by passing either user ids or
# login names or both together:
# $call = $rpc->call( 'User.get', { ids => [1,2,3],
# names => ['testusera@redhat.com', 'testuserb@redhat.com'] });
sub get {
my ($self, $params) = validate(@_, 'names', 'ids');
my @user_objects;
@user_objects = map { Bugzilla::User->check($_) } @{ $params->{names} }
if $params->{names};
# start filtering to remove duplicate user ids
my %unique_users = map { $_->id => $_ } @user_objects;
@user_objects = values %unique_users;
my @users;
# If the user is not logged in: Return an error if they passed any user ids.
# Otherwise, return a limited amount of information based on login names.
if (!Bugzilla->user->id){
if ($params->{ids}){
ThrowUserError("user_access_by_id_denied");
}
if ($params->{match}) {
ThrowUserError('user_access_by_match_denied');
}
@users = map {filter $params, {
id => $self->type('int', $_->id),
real_name => $self->type('string', $_->name),
name => $self->type('string', $_->login),
}} @user_objects;
return { users => \@users };
}
my $obj_by_ids;
$obj_by_ids = Bugzilla::User->new_from_list($params->{ids}) if $params->{ids};
# obj_by_ids are only visible to the user if he can see
# the otheruser, for non visible otheruser throw an error
foreach my $obj (@$obj_by_ids) {
if (Bugzilla->user->can_see_user($obj)){
if (!$unique_users{$obj->id}) {
push (@user_objects, $obj);
$unique_users{$obj->id} = $obj;
}
}
else {
ThrowUserError('auth_failure', {reason => "not_visible",
action => "access",
object => "user",
userid => $obj->id});
}
}
# User Matching
my $limit;
if ($params->{'maxusermatches'}) {
$limit = $params->{'maxusermatches'} + 1;
}
foreach my $match_string (@{ $params->{'match'} || [] }) {
my $matched = Bugzilla::User::match($match_string, $limit);
foreach my $user (@$matched) {
if (!$unique_users{$user->id}) {
push(@user_objects, $user);
$unique_users{$user->id} = $user;
}
}
}
if (Bugzilla->user->in_group('editusers')) {
@users =
map {filter $params, {
id => $self->type('int', $_->id),
real_name => $self->type('string', $_->name),
name => $self->type('string', $_->login),
email => $self->type('string', $_->email),
can_login => $self->type('boolean', $_->is_disabled ? 0 : 1),
email_enabled => $self->type('boolean', $_->email_enabled),
login_denied_text => $self->type('string', $_->disabledtext),
}} @user_objects;
}
else {
@users =
map {filter $params, {
id => $self->type('int', $_->id),
real_name => $self->type('string', $_->name),
name => $self->type('string', $_->login),
email => $self->type('string', $_->email),
can_login => $self->type('boolean', $_->is_disabled ? 0 : 1),
}} @user_objects;
}
return { users => \@users };
}
#################
@ -354,6 +452,135 @@ password is over ten characters.)
=back
=back
=back
=head2 User Info
=over
=item C<get>
B<UNSTABLE>
=over
=item B<Description>
Gets information about user accounts in Bugzilla.
=item B<Params>
B<Note>: At least one of C<ids>, C<names>, or C<match> must be specified.
B<Note>: Users will not be returned more than once, so even if a user
is matched by more than one argument, only one user will be returned.
In addition to the parameters below, this method also accepts the
standard L<include_fields|Bugzilla::WebService/include_fields> and
L<exclude_fields|Bugzilla::WebService/exclude_fields> arguments.
=over
=item C<ids> (array)
An array of integers, representing user ids.
Logged-out users cannot pass this parameter to this function. If they try,
they will get an error. Logged-in users will get an error if they specify
the id of a user they cannot see.
=item C<names> (array) - An array of login names (strings).
=item C<match> (array)
An array of strings. This works just like "user matching" in
Bugzilla itself. Users will be returned whose real name or login name
contains any one of the specified strings. Users that you cannot see will
not be included in the returned list.
Some Bugzilla installations have user-matching turned off, in which
case you will only be returned exact matches.
Most installations have a limit on how many matches are returned for
each string, which defaults to 1000 but can be changed by the Bugzilla
administrator.
Logged-out users cannot use this argument, and an error will be thrown
if they try. (This is to make it harder for spammers to harvest email
addresses from Bugzilla, and also to enforce the user visibility
restrictions that are implemented on some Bugzillas.)
=back
=item B<Returns>
A hash containing one item, C<users>, that is an array of
hashes. Each hash describes a user, and has the following items:
=over
=item id
C<int> The unique integer ID that Bugzilla uses to represent this user.
Even if the user's login name changes, this will not change.
=item real_name
C<string> The actual name of the user. May be blank.
=item email
C<string> The email address of the user.
=item name
C<string> The login name of the user. Note that in some situations this is
different than their email.
=item can_login
C<boolean> A boolean value to indicate if the user can login into bugzilla.
=item email_enabled
C<boolean> A boolean value to indicate if bug-related mail will be sent
to the user or not.
=item login_denied_text
C<string> A text field that holds the reason for disabling a user from logging
into bugzilla, if empty then the user account is enabled. Otherwise it is
disabled/closed.
B<Note>: If you are not logged in to Bugzilla when you call this function, you
will only be returned the C<id>, C<name>, and C<real_name> items. If you are
logged in and not in editusers group, you will only be returned the C<id>, C<name>,
C<real_name>, C<email>, and C<can_login> items.
=back
=item B<Errors>
=over
=item 51 (Bad Login Name)
You passed an invalid login name in the "names" array.
=item 304 (Authorization Required)
You are logged in, but you are not authorized to see one of the users you
wanted to get information about by user id.
=item 505 (User Access By Id or User-Matching Denied)
Logged-out users cannot use the "ids" or "match" arguments to this
function.
=back
=item B<History>
=over

101
Bugzilla/WebService/Util.pm Normal file
View File

@ -0,0 +1,101 @@
# -*- 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 Everything Solved, Inc.
# Portions created by the Initial Developer are Copyright (C) 2008
# the Initial Developer. All Rights Reserved.
#
# Contributor(s):
# Max Kanat-Alexander <mkanat@bugzilla.org>
package Bugzilla::WebService::Util;
use strict;
use base qw(Exporter);
our @EXPORT_OK = qw(filter validate);
sub filter ($$) {
my ($params, $hash) = @_;
my %newhash = %$hash;
my %include = map { $_ => 1 } @{ $params->{'include_fields'} || [] };
my %exclude = map { $_ => 1 } @{ $params->{'exclude_fields'} || [] };
foreach my $key (keys %$hash) {
if (defined $params->{include_fields}) {
delete $newhash{$key} if !$include{$key};
}
if (defined $params->{exclude_fields}) {
delete $newhash{$key} if $exclude{$key};
}
}
return \%newhash;
}
sub validate {
my ($self, $params, @keys) = @_;
# If @keys is not empty then we convert any named
# parameters that have scalar values to arrayrefs
# that match.
foreach my $key (@keys) {
if (exists $params->{$key}) {
$params->{$key} = ref $params->{$key}
? $params->{$key}
: [ $params->{$key} ];
}
}
return ($self, $params);
}
__END__
=head1 NAME
Bugzilla::WebService::Util - Utility functions used inside of the WebService
code. These are B<not> functions that can be called via the WebService.
=head1 DESCRIPTION
This is somewhat like L<Bugzilla::Util>, but these functions are only used
internally in the WebService code.
=head1 SYNOPSIS
filter({ include_fields => ['id', 'name'],
exclude_fields => ['name'] }, $hash);
validate(@_, 'ids');
=head1 METHODS
=over
=item C<filter_fields>
This helps implement the C<include_fields> and C<exclude_fields> arguments
of WebService methods. Given a hash (the second argument to this subroutine),
this will remove any keys that are I<not> in C<include_fields> and then remove
any keys that I<are> in C<exclude_fields>.
=item C<validate>
This helps in the validation of parameters passed into the WebSerice
methods. Currently it converts listed parameters into an array reference
if the client only passed a single scalar value. It modifies the parameters
hash in place so other parameters should be unaltered.
=back

View File

@ -175,7 +175,7 @@ sub validateID {
|| ThrowUserError("invalid_attach_id", { attach_id => $cgi->param($param) });
# Make sure the attachment exists in the database.
my $attachment = Bugzilla::Attachment->get($attach_id)
my $attachment = new Bugzilla::Attachment($attach_id)
|| ThrowUserError("invalid_attach_id", { attach_id => $attach_id });
return $attachment if ($dont_validate_access || check_can_access($attachment));
@ -187,8 +187,10 @@ sub check_can_access {
my $user = Bugzilla->user;
# Make sure the user is authorized to access this attachment's bug.
ValidateBugID($attachment->bug_id);
if ($attachment->isprivate && $user->id != $attachment->attacher->id && !$user->is_insider) {
Bugzilla::Bug->check($attachment->bug_id);
if ($attachment->isprivate && $user->id != $attachment->attacher->id
&& !$user->is_insider)
{
ThrowUserError('auth_failure', {action => 'access',
object => 'attachment'});
}
@ -377,9 +379,8 @@ sub diff {
# HTML page.
sub viewall {
# Retrieve and validate parameters
my $bugid = $cgi->param('bugid');
ValidateBugID($bugid);
my $bug = new Bugzilla::Bug($bugid);
my $bug = Bugzilla::Bug->check(scalar $cgi->param('bugid'));
my $bugid = $bug->id;
my $attachments = Bugzilla::Attachment->get_attachments_by_bug($bugid);
@ -397,13 +398,12 @@ sub viewall {
# Display a form for entering a new attachment.
sub enter {
# Retrieve and validate parameters
my $bugid = $cgi->param('bugid');
ValidateBugID($bugid);
my $bug = Bugzilla::Bug->check(scalar $cgi->param('bugid'));
my $bugid = $bug->id;
validateCanChangeBug($bugid);
my $dbh = Bugzilla->dbh;
my $user = Bugzilla->user;
my $bug = new Bugzilla::Bug($bugid, $user->id);
# Retrieve the attachments the user can edit from the database and write
# them into an array of hashes where each hash represents one attachment.
my $canEdit = "";
@ -416,7 +416,7 @@ sub enter {
# Define the variables and functions that will be passed to the UI template.
$vars->{'bug'} = $bug;
$vars->{'attachments'} = Bugzilla::Attachment->get_list($attach_ids);
$vars->{'attachments'} = Bugzilla::Attachment->new_from_list($attach_ids);
my $flag_types = Bugzilla::FlagType::match({'target_type' => 'attachment',
'product_id' => $bug->product_id,
@ -440,8 +440,8 @@ sub insert {
$dbh->bz_start_transaction;
# Retrieve and validate parameters
my $bugid = $cgi->param('bugid');
ValidateBugID($bugid);
my $bug = Bugzilla::Bug->check(scalar $cgi->param('bugid'));
my $bugid = $bug->id;
validateCanChangeBug($bugid);
my ($timestamp) = Bugzilla->dbh->selectrow_array("SELECT NOW()");
@ -469,9 +469,8 @@ sub insert {
}
}
my $bug = new Bugzilla::Bug($bugid);
my $attachment = Bugzilla::Attachment->insert_attachment_for_bug(
THROW_ERROR, $bug, $user, $timestamp, $vars);
my $attachment =
Bugzilla::Attachment->create(THROW_ERROR, $bug, $user, $timestamp, $vars);
# Insert a comment about the new attachment into the database.
my $comment =
@ -560,32 +559,14 @@ sub insert {
# Validations are done later when the user submits changes.
sub edit {
my $attachment = validateID();
my $dbh = Bugzilla->dbh;
# Retrieve a list of attachments for this bug as well as a summary of the bug
# to use in a navigation bar across the top of the screen.
my $bugattachments =
Bugzilla::Attachment->get_attachments_by_bug($attachment->bug_id);
# We only want attachment IDs.
@$bugattachments = map { $_->id } @$bugattachments;
my ($bugsummary, $product_id, $component_id) =
$dbh->selectrow_array('SELECT short_desc, product_id, component_id
FROM bugs
WHERE bug_id = ?', undef, $attachment->bug_id);
# Get a list of flag types that can be set for this attachment.
my $flag_types = Bugzilla::FlagType::match({ 'target_type' => 'attachment' ,
'product_id' => $product_id ,
'component_id' => $component_id });
foreach my $flag_type (@$flag_types) {
$flag_type->{'flags'} = Bugzilla::Flag->match({ 'type_id' => $flag_type->id,
'attach_id' => $attachment->id });
}
$vars->{'flag_types'} = $flag_types;
$vars->{'any_flags_requesteeble'} = grep($_->is_requesteeble, @$flag_types);
$vars->{'any_flags_requesteeble'} = grep($_->is_requesteeble, @{$attachment->flag_types});
$vars->{'attachment'} = $attachment;
$vars->{'bugsummary'} = $bugsummary;
$vars->{'attachments'} = $bugattachments;
print $cgi->header();
@ -710,41 +691,54 @@ sub update {
$cgi->param('ispatch'), $cgi->param('isobsolete'),
$cgi->param('isprivate'), $timestamp, $attachment->id));
my $updated_attachment = Bugzilla::Attachment->get($attachment->id);
my $updated_attachment = new Bugzilla::Attachment($attachment->id);
# Record changes in the activity table.
my $sth = $dbh->prepare('INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when,
fieldid, removed, added)
VALUES (?, ?, ?, ?, ?, ?, ?)');
# Flag for updating Last-Modified timestamp if record changed
my $updated = 0;
if ($attachment->description ne $updated_attachment->description) {
my $fieldid = get_field_id('attachments.description');
$sth->execute($bug->id, $attachment->id, $user->id, $timestamp, $fieldid,
$attachment->description, $updated_attachment->description);
$updated = 1;
}
if ($attachment->contenttype ne $updated_attachment->contenttype) {
my $fieldid = get_field_id('attachments.mimetype');
$sth->execute($bug->id, $attachment->id, $user->id, $timestamp, $fieldid,
$attachment->contenttype, $updated_attachment->contenttype);
$updated = 1;
}
if ($attachment->filename ne $updated_attachment->filename) {
my $fieldid = get_field_id('attachments.filename');
$sth->execute($bug->id, $attachment->id, $user->id, $timestamp, $fieldid,
$attachment->filename, $updated_attachment->filename);
$updated = 1;
}
if ($attachment->ispatch != $updated_attachment->ispatch) {
my $fieldid = get_field_id('attachments.ispatch');
$sth->execute($bug->id, $attachment->id, $user->id, $timestamp, $fieldid,
$attachment->ispatch, $updated_attachment->ispatch);
$updated = 1;
}
if ($attachment->isobsolete != $updated_attachment->isobsolete) {
my $fieldid = get_field_id('attachments.isobsolete');
$sth->execute($bug->id, $attachment->id, $user->id, $timestamp, $fieldid,
$attachment->isobsolete, $updated_attachment->isobsolete);
$updated = 1;
}
if ($attachment->isprivate != $updated_attachment->isprivate) {
my $fieldid = get_field_id('attachments.isprivate');
$sth->execute($bug->id, $attachment->id, $user->id, $timestamp, $fieldid,
$attachment->isprivate, $updated_attachment->isprivate);
$updated = 1;
}
if ($updated) {
$dbh->do("UPDATE bugs SET delta_ts = ? WHERE bug_id = ?", undef,
$timestamp, $bug->id);
}
# Commit the transaction now that we are finished updating the database.

View File

@ -67,6 +67,17 @@ if (length($buffer) == 0) {
ThrowUserError("buglist_parameters_required");
}
#
# If query was POSTed, clean the URL from empty parameters and redirect back to
# itself. This will make advanced search URLs more tolerable.
#
if ($cgi->request_method() eq 'POST') {
$cgi->clean_search_url();
print $cgi->redirect(-url => $cgi->self_url());
exit;
}
# Determine whether this is a quicksearch query.
my $searchstring = $cgi->param('quicksearch');
if (defined($searchstring)) {
@ -202,18 +213,17 @@ foreach my $chart (@charts) {
# Utilities
################################################################################
local our @weekday= qw( Sun Mon Tue Wed Thu Fri Sat );
sub DiffDate {
my ($datestr) = @_;
my $date = str2time($datestr);
my $age = time() - $date;
my ($s,$m,$h,$d,$mo,$y,$wd)= localtime $date;
if( $age < 18*60*60 ) {
$date = sprintf "%02d:%02d:%02d", $h,$m,$s;
$date = format_time($datestr, '%H:%M:%S');
} elsif( $age < 6*24*60*60 ) {
$date = sprintf "%s %02d:%02d", $weekday[$wd],$h,$m;
$date = format_time($datestr, '%a %H:%M');
} else {
$date = sprintf "%04d-%02d-%02d", 1900+$y,$mo+1,$d;
$date = format_time($datestr, '%Y-%m-%d');
}
return $date;
}
@ -336,14 +346,14 @@ sub _close_standby_message {
# Command Execution
################################################################################
$cgi->param('cmdtype', "") if !defined $cgi->param('cmdtype');
$cgi->param('remaction', "") if !defined $cgi->param('remaction');
my $cmdtype = $cgi->param('cmdtype') || '';
my $remaction = $cgi->param('remaction') || '';
# Backwards-compatibility - the old interface had cmdtype="runnamed" to run
# a named command, and we can't break this because it's in bookmarks.
if ($cgi->param('cmdtype') eq "runnamed") {
$cgi->param('cmdtype', "dorem");
$cgi->param('remaction', "run");
if ($cmdtype eq "runnamed") {
$cmdtype = "dorem";
$remaction = "run";
}
# Now we're going to be running, so ensure that the params object is set up,
@ -361,7 +371,7 @@ $params ||= new Bugzilla::CGI($cgi);
my @time = localtime(time());
my $date = sprintf "%04d-%02d-%02d", 1900+$time[5],$time[4]+1,$time[3];
my $filename = "bugs-$date.$format->{extension}";
if ($cgi->param('cmdtype') eq "dorem" && $cgi->param('remaction') =~ /^run/) {
if ($cmdtype eq "dorem" && $remaction =~ /^run/) {
$filename = $cgi->param('namedcmd') . "-$date.$format->{extension}";
# Remove white-space from the filename so the user cannot tamper
# with the HTTP headers.
@ -371,8 +381,8 @@ $filename =~ s/\\/\\\\/g; # escape backslashes
$filename =~ s/"/\\"/g; # escape quotes
# Take appropriate action based on user's request.
if ($cgi->param('cmdtype') eq "dorem") {
if ($cgi->param('remaction') eq "run") {
if ($cmdtype eq "dorem") {
if ($remaction eq "run") {
my $query_id;
($buffer, $query_id) = Bugzilla::Search::LookupNamedQuery(
scalar $cgi->param("namedcmd"), scalar $cgi->param('sharer_id')
@ -389,14 +399,14 @@ if ($cgi->param('cmdtype') eq "dorem") {
$order = $params->param('order') || $order;
}
elsif ($cgi->param('remaction') eq "runseries") {
elsif ($remaction eq "runseries") {
$buffer = LookupSeries(scalar $cgi->param("series_id"));
$vars->{'searchname'} = $cgi->param('namedcmd');
$vars->{'searchtype'} = "series";
$params = new Bugzilla::CGI($buffer);
$order = $params->param('order') || $order;
}
elsif ($cgi->param('remaction') eq "forget") {
elsif ($remaction eq "forget") {
my $user = Bugzilla->login(LOGIN_REQUIRED);
# Copy the name into a variable, so that we can trick_taint it for
# the DB. We know it's safe, because we're using placeholders in
@ -460,7 +470,7 @@ if ($cgi->param('cmdtype') eq "dorem") {
exit;
}
}
elsif (($cgi->param('cmdtype') eq "doit") && defined $cgi->param('remtype')) {
elsif (($cmdtype eq "doit") && defined $cgi->param('remtype')) {
if ($cgi->param('remtype') eq "asdefault") {
my $user = Bugzilla->login(LOGIN_REQUIRED);
InsertNamedQuery(DEFAULT_QUERY_NAME, $buffer);
@ -506,8 +516,10 @@ elsif (($cgi->param('cmdtype') eq "doit") && defined $cgi->param('remtype')) {
# exists, add/remove bugs to it, else create it. But if we are
# considering an existing tag, then it has to exist and we throw
# an error if it doesn't (hence the usage of !$is_new_name).
if (my $old_query = Bugzilla::Search::LookupNamedQuery(
$query_name, undef, LIST_OF_BUGS, !$is_new_name))
my ($old_query, $query_id) =
Bugzilla::Search::LookupNamedQuery($query_name, undef, LIST_OF_BUGS, !$is_new_name);
if ($old_query)
{
# We get the encoded query. We need to decode it.
my $old_cgi = new Bugzilla::CGI($old_query);
@ -521,8 +533,8 @@ elsif (($cgi->param('cmdtype') eq "doit") && defined $cgi->param('remtype')) {
my $changes = 0;
foreach my $bug_id (split(/[\s,]+/, $cgi->param('bug_ids'))) {
next unless $bug_id;
ValidateBugID($bug_id);
$bug_ids{$bug_id} = $keep_bug;
my $bug = Bugzilla::Bug->check($bug_id);
$bug_ids{$bug->id} = $keep_bug;
$changes = 1;
}
ThrowUserError('no_bug_ids',
@ -533,9 +545,10 @@ elsif (($cgi->param('cmdtype') eq "doit") && defined $cgi->param('remtype')) {
# Only keep bug IDs we want to add/keep. Disregard deleted ones.
my @bug_ids = grep { $bug_ids{$_} == 1 } keys %bug_ids;
# If the list is now empty, we could as well delete it completely.
ThrowUserError('no_bugs_in_list', {'tag' => $query_name})
unless scalar(@bug_ids);
if (!scalar @bug_ids) {
ThrowUserError('no_bugs_in_list', {name => $query_name,
query_id => $query_id});
}
$new_query = "bug_id=" . join(',', sort {$a <=> $b} @bug_ids);
$query_type = LIST_OF_BUGS;
}
@ -574,83 +587,7 @@ if (!$params->param('query_format')) {
# Column Definition
################################################################################
# Define 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.
# Note: For column names using aliasing (SQL "<field> AS <alias>"), the column
# ID needs to be identical to the field ID for list ordering to work.
local our $columns = {};
sub DefineColumn {
my ($id, $name, $title) = @_;
$columns->{$id} = { 'name' => $name , 'title' => $title };
}
# Column: ID Name Title
DefineColumn("bug_id" , "bugs.bug_id" , "ID" );
DefineColumn("alias" , "bugs.alias" , "Alias" );
DefineColumn("opendate" , "bugs.creation_ts" , "Opened" );
DefineColumn("changeddate" , "bugs.delta_ts" , "Changed" );
DefineColumn("bug_severity" , "bugs.bug_severity" , "Severity" );
DefineColumn("priority" , "bugs.priority" , "Priority" );
DefineColumn("rep_platform" , "bugs.rep_platform" , "Hardware" );
DefineColumn("assigned_to" , "map_assigned_to.login_name" , "Assignee" );
DefineColumn("reporter" , "map_reporter.login_name" , "Reporter" );
DefineColumn("qa_contact" , "map_qa_contact.login_name" , "QA Contact" );
if ($format->{'extension'} eq 'html') {
DefineColumn("assigned_to_realname", "CASE WHEN map_assigned_to.realname = '' THEN map_assigned_to.login_name ELSE map_assigned_to.realname END AS assigned_to_realname", "Assignee" );
DefineColumn("reporter_realname" , "CASE WHEN map_reporter.realname = '' THEN map_reporter.login_name ELSE map_reporter.realname END AS reporter_realname" , "Reporter" );
DefineColumn("qa_contact_realname" , "CASE WHEN map_qa_contact.realname = '' THEN map_qa_contact.login_name ELSE map_qa_contact.realname END AS qa_contact_realname" , "QA Contact");
} else {
DefineColumn("assigned_to_realname", "map_assigned_to.realname AS assigned_to_realname", "Assignee" );
DefineColumn("reporter_realname" , "map_reporter.realname AS reporter_realname" , "Reporter" );
DefineColumn("qa_contact_realname" , "map_qa_contact.realname AS qa_contact_realname" , "QA Contact");
}
DefineColumn("bug_status" , "bugs.bug_status" , "Status" );
DefineColumn("resolution" , "bugs.resolution" , "Resolution" );
DefineColumn("short_short_desc" , "bugs.short_desc" , "Summary" );
DefineColumn("short_desc" , "bugs.short_desc" , "Summary" );
DefineColumn("status_whiteboard" , "bugs.status_whiteboard" , "Whiteboard" );
DefineColumn("component" , "map_components.name" , "Component" );
DefineColumn("product" , "map_products.name" , "Product" );
DefineColumn("classification" , "map_classifications.name" , "Classification" );
DefineColumn("version" , "bugs.version" , "Version" );
DefineColumn("op_sys" , "bugs.op_sys" , "OS" );
DefineColumn("target_milestone" , "bugs.target_milestone" , "Target Milestone" );
DefineColumn("votes" , "bugs.votes" , "Votes" );
DefineColumn("keywords" , "bugs.keywords" , "Keywords" );
DefineColumn("estimated_time" , "bugs.estimated_time" , "Estimated Hours" );
DefineColumn("remaining_time" , "bugs.remaining_time" , "Remaining Hours" );
DefineColumn("actual_time" , "(SUM(ldtime.work_time)*COUNT(DISTINCT ldtime.bug_when)/COUNT(bugs.bug_id)) AS actual_time", "Actual Hours");
DefineColumn("percentage_complete",
"(CASE WHEN (SUM(ldtime.work_time)*COUNT(DISTINCT ldtime.bug_when)/COUNT(bugs.bug_id)) " .
" + bugs.remaining_time = 0.0 " .
"THEN 0.0 " .
"ELSE 100*((SUM(ldtime.work_time)*COUNT(DISTINCT ldtime.bug_when)/COUNT(bugs.bug_id)) " .
" /((SUM(ldtime.work_time)*COUNT(DISTINCT ldtime.bug_when)/COUNT(bugs.bug_id)) + bugs.remaining_time)) " .
"END) AS percentage_complete" , "% Complete");
DefineColumn("relevance" , "relevance" , "Relevance" );
DefineColumn("deadline" , $dbh->sql_date_format('bugs.deadline', '%Y-%m-%d') . " AS deadline", "Deadline");
foreach my $field (Bugzilla->active_custom_fields) {
# Multi-select fields are not (yet) supported in buglists.
next if $field->type == FIELD_TYPE_MULTI_SELECT;
DefineColumn($field->name, 'bugs.' . $field->name, $field->description);
}
Bugzilla::Hook::process("buglist-columns", {'columns' => $columns} );
my $columns = Bugzilla::Search::COLUMNS;
################################################################################
# Display Column Determination
@ -748,6 +685,18 @@ if (lsearch(\@displaycolumns, "percentage_complete") >= 0) {
push (@selectcolumns, "actual_time");
}
# Make sure that the login_name version of a field is always also
# requested if the realname version is requested, so that we can
# display the login name when the realname is empty.
my @realname_fields = grep(/_realname$/, @displaycolumns);
foreach my $item (@realname_fields) {
my $login_field = $item;
$login_field =~ s/_realname$//;
if (!grep($_ eq $login_field, @selectcolumns)) {
push(@selectcolumns, $login_field);
}
}
# Display columns are selected because otherwise we could not display them.
push (@selectcolumns, @displaycolumns);
@ -788,17 +737,6 @@ if ($format->{'extension'} eq 'atom') {
}
}
################################################################################
# Query Generation
################################################################################
# Convert the list of columns being selected into a list of column names.
my @selectnames = map($columns->{$_}->{'name'}, @selectcolumns);
# Remove columns with no names, such as percentage_complete
# (or a removed *_time column due to permissions)
@selectnames = grep($_ ne '', @selectnames);
################################################################################
# Sort Order Determination
################################################################################
@ -822,39 +760,47 @@ if (!$order || $order =~ /^reuse/i) {
}
}
my $db_order = ""; # Modified version of $order for use with SQL query
if ($order) {
# Convert the value of the "order" form field into a list of columns
# by which to sort the results.
ORDER: for ($order) {
/^Bug Number$/ && do {
$order = "bugs.bug_id";
$order = "bug_id";
last ORDER;
};
/^Importance$/ && do {
$order = "bugs.priority, bugs.bug_severity";
$order = "priority,bug_severity";
last ORDER;
};
/^Assignee$/ && do {
$order = "map_assigned_to.login_name, bugs.bug_status, bugs.priority, bugs.bug_id";
$order = "assigned_to,bug_status,priority,bug_id";
last ORDER;
};
/^Last Changed$/ && do {
$order = "bugs.delta_ts, bugs.bug_status, bugs.priority, map_assigned_to.login_name, bugs.bug_id";
$order = "changeddate,bug_status,priority,assigned_to,bug_id";
last ORDER;
};
do {
my @order;
my @columnnames = map($columns->{lc($_)}->{'name'}, keys(%$columns));
# A custom list of columns. Make sure each column is valid.
foreach my $fragment (split(/,/, $order)) {
$fragment = trim($fragment);
next unless $fragment;
# Accept an order fragment matching a column name, with
# asc|desc optionally following (to specify the direction)
if (grep($fragment =~ /^\Q$_\E(\s+(asc|desc))?$/, @columnnames, keys(%$columns))) {
next if $fragment =~ /\brelevance\b/ && !$fulltext;
push(@order, $fragment);
my ($column_name, $direction) = split_order_term($fragment);
$column_name = translate_old_column($column_name);
# Special handlings for certain columns
next if $column_name eq 'relevance' && !$fulltext;
# If we are sorting by votes, sort in descending order if
# no explicit sort order was given.
if ($column_name eq 'votes' && !$direction) {
$direction = "DESC";
}
if (exists $columns->{$column_name}) {
$direction = " $direction" if $direction;
push(@order, "$column_name$direction");
}
else {
my $vars = { fragment => $fragment };
@ -877,61 +823,17 @@ if ($order) {
if (!$order) {
# DEFAULT
$order = "bugs.bug_status, bugs.priority, map_assigned_to.login_name, bugs.bug_id";
$order = "bug_status,priority,assigned_to,bug_id";
}
# Make sure ORDER BY columns are included in the field list.
foreach my $fragment (split(/,/, $order)) {
$fragment = trim($fragment);
if (!grep($fragment =~ /^\Q$_\E(\s+(asc|desc))?$/, @selectnames)) {
# Add order columns to selectnames
# The fragment has already been validated
$fragment =~ s/\s+(asc|desc)$//;
# While newer fragments contain IDs for aliased columns, older
# LASTORDER cookies (or bookmarks) may contain full names.
# Convert them to an ID here.
if ($fragment =~ / AS (\w+)/) {
$fragment = $1;
}
$fragment =~ tr/a-zA-Z\.0-9\-_//cd;
# If the order fragment is an ID, we need its corresponding name
# to be in the field list.
if (exists($columns->{$fragment})) {
$fragment = $columns->{$fragment}->{'name'};
}
push @selectnames, $fragment;
}
}
$db_order = $order; # Copy $order into $db_order for use with SQL query
# If we are sorting by votes, sort in descending order if no explicit
# sort order was given
$db_order =~ s/bugs.votes\s*(,|$)/bugs.votes desc$1/i;
# the 'actual_time' field is defined as an aggregate function, but
# for order we just need the column name 'actual_time'
my $aggregate_search = quotemeta($columns->{'actual_time'}->{'name'});
$db_order =~ s/$aggregate_search/actual_time/g;
# the 'percentage_complete' field is defined as an aggregate too
$aggregate_search = quotemeta($columns->{'percentage_complete'}->{'name'});
$db_order =~ s/$aggregate_search/percentage_complete/g;
# Now put $db_order into a format that Bugzilla::Search can use.
# (We create $db_order as a string first because that's the way
# we did it before Bugzilla::Search took an "order" argument.)
my @orderstrings = split(/,\s*/, $db_order);
my @orderstrings = split(/,\s*/, $order);
# Generate the basic SQL query that will be used to generate the bug list.
my $search = new Bugzilla::Search('fields' => \@selectnames,
my $search = new Bugzilla::Search('fields' => \@selectcolumns,
'params' => $params,
'order' => \@orderstrings);
my $query = $search->getSQL();
$vars->{'search_description'} = $search->search_description;
if (defined $cgi->param('limit')) {
my $limit = $cgi->param('limit');
@ -952,7 +854,13 @@ elsif ($fulltext) {
if ($cgi->param('debug')) {
$vars->{'debug'} = 1;
$vars->{'query'} = $query;
$vars->{'debugdata'} = $search->getDebugData();
# Explains are limited to admins because you could use them to figure
# out how many hidden bugs are in a particular product (by doing
# searches and looking at the number of rows the explain says it's
# examining).
if (Bugzilla->user->in_group('admin')) {
$vars->{'query_explain'} = $dbh->bz_explain($query);
}
}
# Time to use server push to display an interim message to the user until
@ -998,6 +906,22 @@ $buglist_sth->execute();
# Retrieve the query results one row at a time and write the data into a list
# of Perl records.
# If we're doing time tracking, then keep totals for all bugs.
my $percentage_complete = lsearch(\@displaycolumns, 'percentage_complete') >= 0;
my $estimated_time = lsearch(\@displaycolumns, 'estimated_time') >= 0;
my $remaining_time = ((lsearch(\@displaycolumns, 'remaining_time') >= 0)
|| $percentage_complete);
my $actual_time = ((lsearch(\@displaycolumns, 'actual_time') >= 0)
|| $percentage_complete);
my $time_info = { 'estimated_time' => 0,
'remaining_time' => 0,
'actual_time' => 0,
'percentage_complete' => 0,
'time_present' => ($estimated_time || $remaining_time ||
$actual_time || $percentage_complete),
};
my $bugowners = {};
my $bugproducts = {};
my $bugstatuses = {};
@ -1020,16 +944,12 @@ while (my @row = $buglist_sth->fetchrow_array()) {
$bug->{'changeddate'} =~
s/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/$1-$2-$3 $4:$5:$6/;
# Put in the change date as a time, so that the template date plugin
# can format the date in any way needed by the template. ICS and Atom
# have specific, and different, date and time formatting.
$bug->{'changedtime'} = str2time($bug->{'changeddate'}, Bugzilla->params->{'timezone'});
$bug->{'changedtime'} = $bug->{'changeddate'}; # for iCalendar and Atom
$bug->{'changeddate'} = DiffDate($bug->{'changeddate'});
}
if ($bug->{'opendate'}) {
# Put in the open date as a time for the template date plugin.
$bug->{'opentime'} = str2time($bug->{'opendate'}, Bugzilla->params->{'timezone'});
$bug->{'opentime'} = $bug->{'opendate'}; # for iCalendar
$bug->{'opendate'} = DiffDate($bug->{'opendate'});
}
@ -1045,6 +965,11 @@ while (my @row = $buglist_sth->fetchrow_array()) {
# Add id to list for checking for bug privacy later
push(@bugidlist, $bug->{'bug_id'});
# Compute time tracking info.
$time_info->{'estimated_time'} += $bug->{'estimated_time'} if ($estimated_time);
$time_info->{'remaining_time'} += $bug->{'remaining_time'} if ($remaining_time);
$time_info->{'actual_time'} += $bug->{'actual_time'} if ($actual_time);
}
# Check for bug privacy and set $bug->{'secure_mode'} to 'implied' or 'manual'
@ -1077,6 +1002,15 @@ if (@bugidlist) {
}
}
# Compute percentage complete without rounding.
my $sum = $time_info->{'actual_time'}+$time_info->{'remaining_time'};
if ($sum > 0) {
$time_info->{'percentage_complete'} = 100*$time_info->{'actual_time'}/$sum;
}
else { # remaining_time <= 0
$time_info->{'percentage_complete'} = 0
}
################################################################################
# Template Variable Definition
################################################################################
@ -1102,6 +1036,7 @@ $vars->{'urlquerypart'} = $params->canonicalise_query('order',
'query_based_on');
$vars->{'order'} = $order;
$vars->{'caneditbugs'} = 1;
$vars->{'time_info'} = $time_info;
if (!Bugzilla->user->in_group('editbugs')) {
foreach my $product (keys %$bugproducts) {
@ -1126,7 +1061,7 @@ if (scalar(@bugowners) > 1 && Bugzilla->user->in_group('editbugs')) {
$vars->{'splitheader'} = $cgi->cookie('SPLITHEADER') ? 1 : 0;
$vars->{'quip'} = GetQuip();
$vars->{'currenttime'} = time();
$vars->{'currenttime'} = localtime(time());
# The following variables are used when the user is making changes to multiple bugs.
if ($dotweak && scalar @bugs) {
@ -1151,8 +1086,6 @@ if ($dotweak && scalar @bugs) {
$vars->{'severities'} = get_legal_field_values('bug_severity');
$vars->{'resolutions'} = get_legal_field_values('resolution');
$vars->{'unconfirmedstate'} = 'UNCONFIRMED';
# Convert bug statuses to their ID.
my @bug_statuses = map {$dbh->quote($_)} keys %$bugstatuses;
my $bug_status_ids =

View File

@ -294,7 +294,7 @@ sub wrap {
# We create a Chart object so we can validate the parameters
my $chart = new Bugzilla::Chart($cgi);
$vars->{'time'} = time();
$vars->{'time'} = localtime(time());
$vars->{'imagebase'} = $cgi->canonicalise_query(
"action", "action-wrap", "ctype", "format", "width", "height");

View File

@ -95,10 +95,7 @@ exit if $switch{'check-modules'};
# then instead of our nice normal checksetup message, the user would
# get a cryptic perl error about the missing module.
# We need $::ENV{'PATH'} to remain defined.
my $env = $::ENV{'PATH'};
require Bugzilla;
$::ENV{'PATH'} = $env;
require Bugzilla::Config;
import Bugzilla::Config qw(:admin);
@ -115,7 +112,6 @@ require Bugzilla::Template;
require Bugzilla::Field;
require Bugzilla::Install;
Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
Bugzilla->installation_mode(INSTALLATION_MODE_NON_INTERACTIVE) if $answers_file;
Bugzilla->installation_answers($answers_file);

View File

@ -21,6 +21,7 @@
# Contributor(s): Terry Weissman <terry@mozilla.org>
# Gervase Markham <gerv@gerv.net>
# Max Kanat-Alexander <mkanat@bugzilla.org>
# Pascal Held <paheld@gmail.com>
use strict;
@ -95,17 +96,15 @@ if (defined $cgi->param('rememberedquery')) {
if (defined $cgi->param('resetit')) {
@collist = DEFAULT_COLUMN_LIST;
} else {
foreach my $i (@masterlist) {
if (defined $cgi->param("column_$i")) {
push @collist, $i;
}
if (defined $cgi->param("selected_columns")) {
my %legal_list = map { $_ => 1 } @masterlist;
@collist = grep { exists $legal_list{$_} } $cgi->param("selected_columns");
}
if (defined $cgi->param('splitheader')) {
$splitheader = $cgi->param('splitheader')? 1: 0;
}
}
my $list = join(" ", @collist);
my $urlbase = Bugzilla->params->{"urlbase"};
if ($list) {
# Only set the cookie if this is not a saved search.
@ -142,13 +141,11 @@ if (defined $cgi->param('rememberedquery')) {
$params->param('columnlist', join(",", @collist));
$search->set_url($params->query_string());
$search->update();
$vars->{'redirect_url'} = "buglist.cgi?".$cgi->param('rememberedquery');
}
else {
my $params = new Bugzilla::CGI($cgi->param('rememberedquery'));
$params->param('columnlist', join(",", @collist));
$vars->{'redirect_url'} = "buglist.cgi?".$params->query_string();
}
# If we're running on Microsoft IIS, using cgi->redirect discards
@ -169,7 +166,9 @@ if (defined $cgi->param('rememberedquery')) {
exit;
}
if (defined $cgi->cookie('COLUMNLIST')) {
if (defined $cgi->param('columnlist')) {
@collist = split(/[ ,]+/, $cgi->param('columnlist'));
} elsif (defined $cgi->cookie('COLUMNLIST')) {
@collist = split(/ /, $cgi->cookie('COLUMNLIST'));
} else {
@collist = DEFAULT_COLUMN_LIST;
@ -185,16 +184,8 @@ if (defined $cgi->param('query_based_on')) {
my $searches = Bugzilla->user->queries;
my ($search) = grep($_->name eq $cgi->param('query_based_on'), @$searches);
# Only allow users to edit their own queries.
if ($search && $search->user->id == Bugzilla->user->id) {
if ($search) {
$vars->{'saved_search'} = $search;
$vars->{'buffer'} = "cmdtype=runnamed&namedcmd=". url_quote($search->name);
my $params = new Bugzilla::CGI($search->url);
if ($params->param('columnlist')) {
my @collist = split(',', $params->param('columnlist'));
$vars->{'collist'} = \@collist if scalar (@collist);
}
}
}

View File

@ -59,9 +59,6 @@ if (chdir("graphs")) {
chdir($cwd);
}
# This is a pure command line script.
Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
my $dbh = Bugzilla->switch_to_shadow_db();
@ -134,32 +131,6 @@ my $tend = time;
CollectSeriesData();
{
local $ENV{'GATEWAY_INTERFACE'} = 'cmdline';
local $ENV{'REQUEST_METHOD'} = 'GET';
local $ENV{'QUERY_STRING'} = 'ctype=rdf';
my $perl = $^X;
trick_taint($perl);
# Generate a static RDF file containing the default view of the duplicates data.
open(CGI, "$perl -T duplicates.cgi |")
|| die "can't fork duplicates.cgi: $!";
open(RDF, ">$datadir/duplicates.tmp")
|| die "can't write to $datadir/duplicates.tmp: $!";
my $headers_done = 0;
while (<CGI>) {
print RDF if $headers_done;
$headers_done = 1 if $_ eq "\r\n";
}
close CGI;
close RDF;
}
if (-s "$datadir/duplicates.tmp") {
rename("$datadir/duplicates.rdf", "$datadir/duplicates-old.rdf");
rename("$datadir/duplicates.tmp", "$datadir/duplicates.rdf");
}
sub check_data_dir {
my $dir = shift;
@ -590,7 +561,7 @@ sub CollectSeriesData {
# login name or a renamed product or component, etc.
eval {
my $search = new Bugzilla::Search('params' => $cgi,
'fields' => ["bugs.bug_id"],
'fields' => ["bug_id"],
'user' => $user);
my $sql = $search->getSQL();
$data = $shadow_dbh->selectall_arrayref($sql);

View File

@ -46,6 +46,9 @@ if (Bugzilla->params->{'requirelogin'} && !$user->id) {
display_data();
}
# Get data from the shadow DB as they don't change very often.
Bugzilla->switch_to_shadow_db;
# Pass a bunch of Bugzilla configuration to the templates.
my $vars = {};
$vars->{'priority'} = get_legal_field_values('priority');
@ -56,8 +59,7 @@ $vars->{'keyword'} = [map($_->name, Bugzilla::Keyword->get_all)];
$vars->{'resolution'} = get_legal_field_values('resolution');
$vars->{'status'} = get_legal_field_values('bug_status');
$vars->{'custom_fields'} =
[ grep {$_->type == FIELD_TYPE_SINGLE_SELECT || $_->type == FIELD_TYPE_MULTI_SELECT}
Bugzilla->active_custom_fields ];
[ grep {$_->is_select} Bugzilla->active_custom_fields ];
# Include a list of product objects.
if ($cgi->param('product')) {

View File

@ -48,8 +48,6 @@ use constant TARGET_DB_HOST => 'localhost';
# MAIN SCRIPT
#####################################################################
Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
print "Connecting to the '" . SOURCE_DB_NAME . "' source database on "
. SOURCE_DB_TYPE . "...\n";
my $source_db = Bugzilla::DB::_connect(SOURCE_DB_TYPE, SOURCE_DB_HOST,

View File

@ -32,14 +32,15 @@ use Bugzilla::Error;
use Bugzilla::Product;
my $user = Bugzilla->login();
my $cgi = Bugzilla->cgi;
my $dbh = Bugzilla->dbh;
my $template = Bugzilla->template;
my $vars = {};
print $cgi->header();
# This script does nothing but displaying mostly static data.
Bugzilla->switch_to_shadow_db;
my $product_name = trim($cgi->param('product') || '');
my $product = new Bugzilla::Product({'name' => $product_name});

View File

@ -35,6 +35,9 @@ my $cgi = Bugzilla->cgi;
my $template = Bugzilla->template;
my $vars = {};
# Run queries against the shadow DB.
Bugzilla->switch_to_shadow_db;
$vars->{'keywords'} = Bugzilla::Keyword->get_all_with_bug_count();
$vars->{'caneditkeywords'} = Bugzilla->user->in_group("editkeywords");

View File

@ -2,14 +2,18 @@
<!-- Module Versions -->
<!ENTITY min-cgi-ver "3.21">
<!ENTITY min-digest-sha-ver "any">
<!ENTITY min-date-format-ver "2.21">
<!ENTITY min-datetime-ver "0.28">
<!ENTITY min-datetime-timezone-ver "0.71">
<!ENTITY min-file-spec-ver "0.84">
<!ENTITY min-dbi-ver "1.41">
<!ENTITY min-template-ver "2.15">
<!ENTITY min-template-ver "2.22">
<!ENTITY min-email-send-ver "2.00">
<!ENTITY min-email-mime-ver "1.861">
<!ENTITY min-email-mime-encodings-ver "1.313">
<!ENTITY min-email-mime-modifier-ver "1.442">
<!ENTITY min-uri-ver "any">
<!ENTITY min-gd-ver "1.20">
<!ENTITY min-chart-base-ver "1.0">
<!ENTITY min-template-plugin-gd-image-ver "any">
@ -28,8 +32,9 @@
<!ENTITY min-html-scrubber-ver "any">
<!ENTITY min-email-mime-attachment-stripper-ver "any">
<!ENTITY min-email-reply-ver "any">
<!ENTITY min-theschwartz-ver "any">
<!ENTITY min-daemon-generic-ver "any">
<!ENTITY min-mod_perl2-ver "1.999022">
<!ENTITY min-mp-cgi-ver "3.21">
<!-- Database Versions -->
<!ENTITY min-dbd-pg-ver "1.45">

View File

@ -2,7 +2,7 @@
<HTML
><HEAD
><TITLE
>The Bugzilla Guide - 3.2.4
>The Bugzilla Guide - 3.4
Release</TITLE
><META
NAME="GENERATOR"
@ -43,7 +43,7 @@ CLASS="TITLEPAGE"
CLASS="title"
><A
NAME="AEN2"
>The Bugzilla Guide - 3.2.4
>The Bugzilla Guide - 3.4
Release</A
></H1
><H3
@ -51,7 +51,7 @@ CLASS="corpauthor"
>The Bugzilla Team</H3
><P
CLASS="pubdate"
>2009-07-08<BR></P
>2009-07-28<BR></P
><DIV
><DIV
CLASS="abstract"
@ -714,7 +714,7 @@ NAME="newversions"
>1.3. New Versions</A
></H2
><P
>&#13; This is the 3.2.4 version of The Bugzilla Guide. It is so named
>&#13; This is the 3.4 version of The Bugzilla Guide. It is so named
to match the current version of Bugzilla.
</P
><P
@ -1980,7 +1980,7 @@ HREF="#install-modules-dbd-mysql"
HREF="#install-modules-template"
>Template</A
>
(2.15)
(2.22)
</P
></LI
><LI
@ -2128,12 +2128,6 @@ HREF="#install-modules-soap-lite"
(1.999022) for mod_perl
</P
></LI
><LI
><P
>&#13; CGI
(3.21) for mod_perl
</P
></LI
></OL
>
</P
@ -2165,7 +2159,7 @@ CLASS="section"
CLASS="section"
><A
NAME="install-modules-template"
>2.1.5.2. Template Toolkit (2.15)</A
>2.1.5.2. Template Toolkit (2.22)</A
></H4
><P
>When you install Template Toolkit, you'll get asked various
@ -2461,10 +2455,6 @@ TARGET="_top"
>http://perl.apache.org</A
> - Bugzilla requires
version 1.999022 (AKA 2.0.0-RC5) to be installed.</P
><P
>Bugzilla also requires a more up-to-date version of the CGI
perl module to be installed, version 3.21 as opposed to 3.21
</P
></DIV
></DIV
><DIV
@ -2794,7 +2784,7 @@ CLASS="section"
><HR><H5
CLASS="section"
><A
NAME="AEN482"
NAME="AEN479"
>2.2.2.2.2. Allow small words in full-text indexes</A
></H5
><P
@ -2935,7 +2925,7 @@ CLASS="section"
><HR><H5
CLASS="section"
><A
NAME="AEN509"
NAME="AEN506"
>2.2.2.2.4. Permit attachments table to grow beyond 4GB</A
></H5
><P
@ -3035,7 +3025,7 @@ CLASS="section"
><H5
CLASS="section"
><A
NAME="AEN525"
NAME="AEN522"
>2.2.2.3.1. Add a User to PostgreSQL</A
></H5
><P
@ -3120,7 +3110,7 @@ CLASS="section"
><HR><H5
CLASS="section"
><A
NAME="AEN541"
NAME="AEN538"
>2.2.2.3.2. Configure PostgreSQL</A
></H5
><P
@ -3181,7 +3171,7 @@ CLASS="section"
><H5
CLASS="section"
><A
NAME="AEN557"
NAME="AEN554"
>2.2.2.4.1. Create a New Tablespace</A
></H5
><P
@ -3233,7 +3223,7 @@ CLASS="section"
><HR><H5
CLASS="section"
><A
NAME="AEN565"
NAME="AEN562"
>2.2.2.4.2. Add a User to Oracle</A
></H5
><P
@ -3289,7 +3279,7 @@ CLASS="section"
><HR><H5
CLASS="section"
><A
NAME="AEN573"
NAME="AEN570"
>2.2.2.4.3. Configure the Web Server</A
></H5
><P
@ -3327,7 +3317,7 @@ CLASS="section"
><HR><H3
CLASS="section"
><A
NAME="AEN579"
NAME="AEN576"
>2.2.3. checksetup.pl</A
></H3
><P
@ -4147,7 +4137,7 @@ CLASS="section"
><HR><H3
CLASS="section"
><A
NAME="AEN729"
NAME="AEN726"
>2.3.1. Bug Graphs</A
></H3
><P
@ -5264,7 +5254,7 @@ CLASS="section"
><H3
CLASS="section"
><A
NAME="AEN896"
NAME="AEN893"
>2.6.1. Introduction</A
></H3
><P
@ -5284,7 +5274,7 @@ CLASS="section"
><HR><H3
CLASS="section"
><A
NAME="AEN900"
NAME="AEN897"
>2.6.2. MySQL</A
></H3
><P
@ -5340,7 +5330,7 @@ CLASS="section"
><HR><H4
CLASS="section"
><A
NAME="AEN908"
NAME="AEN905"
>2.6.2.1. Running MySQL as Non-Root</A
></H4
><DIV
@ -5348,7 +5338,7 @@ CLASS="section"
><H5
CLASS="section"
><A
NAME="AEN910"
NAME="AEN907"
>2.6.2.1.1. The Custom Configuration Method</A
></H5
><P
@ -5392,7 +5382,7 @@ CLASS="section"
><HR><H5
CLASS="section"
><A
NAME="AEN914"
NAME="AEN911"
>2.6.2.1.2. The Custom Built Method</A
></H5
><P
@ -5415,7 +5405,7 @@ CLASS="section"
><HR><H5
CLASS="section"
><A
NAME="AEN919"
NAME="AEN916"
>2.6.2.1.3. Starting the Server</A
></H5
><P
@ -5543,7 +5533,7 @@ CLASS="section"
><HR><H3
CLASS="section"
><A
NAME="AEN935"
NAME="AEN932"
>2.6.3. Perl</A
></H3
><P
@ -5647,7 +5637,7 @@ CLASS="section"
><HR><H3
CLASS="section"
><A
NAME="AEN957"
NAME="AEN954"
>2.6.5. HTTP Server</A
></H3
><P
@ -5661,7 +5651,7 @@ CLASS="section"
><HR><H4
CLASS="section"
><A
NAME="AEN960"
NAME="AEN957"
>2.6.5.1. Running Apache as Non-Root</A
></H4
><P
@ -5743,7 +5733,7 @@ CLASS="section"
><HR><H3
CLASS="section"
><A
NAME="AEN969"
NAME="AEN966"
>2.6.6. Bugzilla</A
></H3
><P
@ -6846,14 +6836,6 @@ CLASS="filename"
</P
></DD
><DT
>timezone</DT
><DD
><P
>&#13; Timezone of server. The timezone is displayed with timestamps. If
this parameter is left blank, the timezone is not displayed.
</P
></DD
><DT
>utf8</DT
><DD
><P
@ -7016,33 +6998,9 @@ NAME="param-admin-policies"
></H3
><P
>&#13; This page contains parameters for basic administrative functions.
Options include whether to allow the deletion of bugs and users, whether
to allow users to change their email address, and whether to allow
user watching (one user receiving all notifications of a selected
other user).
Options include whether to allow the deletion of bugs and users,
and whether to allow users to change their email address.
</P
><P
></P
><DIV
CLASS="variablelist"
><DL
><DT
>supportwatchers</DT
><DD
><P
>&#13; Turning on this option allows users to ask to receive copies
of bug mail sent to another user. Watching a user with
different group permissions is not a way to 'get around' the
system; copied emails are still subject to the normal groupset
permissions of a bug, and <SPAN
CLASS="QUOTE"
>"watchers"</SPAN
> will only be
copied on emails from bugs they would normally be allowed to view.
</P
></DD
></DL
></DIV
></DIV
><DIV
CLASS="section"
@ -11191,7 +11149,7 @@ CLASS="section"
><HR><H3
CLASS="section"
><A
NAME="AEN2181"
NAME="AEN2168"
>3.15.4. Assigning Group Controls to Products</A
></H3
><P
@ -11799,15 +11757,6 @@ COMPACT="COMPACT"
><P
>Block everything</P
></LI
><LI
><P
>But allow:
<TT
CLASS="filename"
>duplicates.rdf</TT
>
</P
></LI
></UL
></LI
><LI
@ -12075,9 +12024,9 @@ NAME="myaccount"
Bugzilla for the URL you should use to access it. If you're
test-driving Bugzilla, use this URL:
<A
HREF="http://landfill.bugzilla.org/bugzilla-3.2-branch/"
HREF="http://landfill.bugzilla.org/bugzilla-3.4-branch/"
TARGET="_top"
>http://landfill.bugzilla.org/bugzilla-3.2-branch/</A
>http://landfill.bugzilla.org/bugzilla-3.4-branch/</A
>.
</P
><P
@ -12227,7 +12176,7 @@ NAME="bug_page"
>The core of Bugzilla is the screen which displays a particular
bug. It's a good place to explain some Bugzilla concepts.
<A
HREF="http://landfill.bugzilla.org/bugzilla-3.2-branch/show_bug.cgi?id=1"
HREF="http://landfill.bugzilla.org/bugzilla-3.4-branch/show_bug.cgi?id=1"
TARGET="_top"
>&#13; Bug 1 on Landfill</A
>
@ -12645,9 +12594,9 @@ NAME="query"
any bug report, comment, or patch currently in the Bugzilla system. You
can play with it here:
<A
HREF="http://landfill.bugzilla.org/bugzilla-3.2-branch/query.cgi"
HREF="http://landfill.bugzilla.org/bugzilla-3.4-branch/query.cgi"
TARGET="_top"
>http://landfill.bugzilla.org/bugzilla-3.2-branch/query.cgi</A
>http://landfill.bugzilla.org/bugzilla-3.4-branch/query.cgi</A
>.</P
><P
>The Search page has controls for selecting different possible
@ -12769,7 +12718,7 @@ NAME="negation"
>&#13; At first glance, negation seems redundant. Rather than
searching for
<A
NAME="AEN2558"
NAME="AEN2540"
></A
><BLOCKQUOTE
CLASS="BLOCKQUOTE"
@ -12780,7 +12729,7 @@ CLASS="BLOCKQUOTE"
>
one could search for
<A
NAME="AEN2560"
NAME="AEN2542"
></A
><BLOCKQUOTE
CLASS="BLOCKQUOTE"
@ -12791,7 +12740,7 @@ CLASS="BLOCKQUOTE"
>
However, the search
<A
NAME="AEN2562"
NAME="AEN2544"
></A
><BLOCKQUOTE
CLASS="BLOCKQUOTE"
@ -12803,7 +12752,7 @@ CLASS="BLOCKQUOTE"
would find every bug where anyone on the CC list did not contain
"@mozilla.org" while
<A
NAME="AEN2564"
NAME="AEN2546"
></A
><BLOCKQUOTE
CLASS="BLOCKQUOTE"
@ -12817,7 +12766,7 @@ CLASS="BLOCKQUOTE"
complex expressions to be built using terms OR'd together and then
negated. Negation permits queries such as
<A
NAME="AEN2566"
NAME="AEN2548"
></A
><BLOCKQUOTE
CLASS="BLOCKQUOTE"
@ -12830,7 +12779,7 @@ CLASS="BLOCKQUOTE"
to find bugs that are neither
in the update product or in the documentation component or
<A
NAME="AEN2568"
NAME="AEN2550"
></A
><BLOCKQUOTE
CLASS="BLOCKQUOTE"
@ -12858,7 +12807,7 @@ NAME="multiplecharts"
a bug that has two different people cc'd on it, then you need
to use two boolean charts. A search for
<A
NAME="AEN2573"
NAME="AEN2555"
></A
><BLOCKQUOTE
CLASS="BLOCKQUOTE"
@ -12873,7 +12822,7 @@ CLASS="BLOCKQUOTE"
containing "foo@" and someone else containing "@mozilla.org",
then you would need two boolean charts.
<A
NAME="AEN2575"
NAME="AEN2557"
></A
><BLOCKQUOTE
CLASS="BLOCKQUOTE"
@ -13140,7 +13089,7 @@ NAME="fillingbugs"
>Years of bug writing experience has been distilled for your
reading pleasure into the
<A
HREF="http://landfill.bugzilla.org/bugzilla-3.2-branch/page.cgi?id=bug-writing.html"
HREF="http://landfill.bugzilla.org/bugzilla-3.4-branch/page.cgi?id=bug-writing.html"
TARGET="_top"
>&#13; Bug Writing Guidelines</A
>.
@ -13192,7 +13141,7 @@ VALIGN="TOP"
>&#13; If you want to file a test bug to see how Bugzilla works,
you can do it on one of our test installations on
<A
HREF="http://landfill.bugzilla.org/bugzilla-3.2-branch/"
HREF="http://landfill.bugzilla.org/bugzilla-3.4-branch/"
TARGET="_top"
>Landfill</A
>.
@ -13579,7 +13528,7 @@ CLASS="section"
><HR><H3
CLASS="section"
><A
NAME="AEN2711"
NAME="AEN2693"
>5.8.1. Autolinkification</A
></H3
><P
@ -14454,7 +14403,7 @@ CLASS="section"
><HR><H4
CLASS="section"
><A
NAME="AEN2908"
NAME="AEN2890"
>5.11.2.1. Creating Charts</A
></H4
><P
@ -14923,7 +14872,7 @@ CLASS="section"
><HR><H3
CLASS="section"
><A
NAME="AEN2968"
NAME="AEN2950"
>5.13.4. Saving Your Changes</A
></H3
><P
@ -16040,7 +15989,7 @@ COLOR="#000000"
CLASS="programlisting"
>...
[% ', &#60;a href="editkeywords.cgi"&#62;keywords&#60;/a&#62;'
IF user.groups.editkeywords %]
IF user.in_group('editkeywords') %]
[% Hook.process("edit") %]
...</PRE
></FONT
@ -16065,7 +16014,7 @@ WIDTH="100%"
COLOR="#000000"
><PRE
CLASS="programlisting"
>...[% ', &#60;a href="edit-projects.cgi"&#62;projects&#60;/a&#62;' IF user.groups.projman_admins %]</PRE
>...[% ', &#60;a href="edit-projects.cgi"&#62;projects&#60;/a&#62;' IF user.in_group('projman_admins') %]</PRE
></FONT
></TD
></TR
@ -16330,7 +16279,7 @@ COLOR="#000000"
><PRE
CLASS="programlisting"
> if ($field eq "qacontact") {
if (Bugzilla-&#62;user-&#62;groups("quality_assurance")) {
if (Bugzilla-&#62;user-&#62;in_group("quality_assurance")) {
return 1;
}
else {
@ -16932,7 +16881,7 @@ NAME="trbl-relogin-everyone-share"
>Example A-1. Examples of urlbase/cookiepath pairs for sharing login cookies</B
></P
><A
NAME="AEN3303"
NAME="AEN3285"
></A
><BLOCKQUOTE
CLASS="BLOCKQUOTE"
@ -16973,7 +16922,7 @@ NAME="trbl-relogin-everyone-restrict"
>Example A-2. Examples of urlbase/cookiepath pairs to restrict the login cookie</B
></P
><A
NAME="AEN3310"
NAME="AEN3292"
></A
><BLOCKQUOTE
CLASS="BLOCKQUOTE"
@ -17811,7 +17760,7 @@ NAME="gfdl"
><P
>Version 1.1, March 2000</P
><A
NAME="AEN3483"
NAME="AEN3465"
></A
><BLOCKQUOTE
CLASS="BLOCKQUOTE"
@ -18274,7 +18223,7 @@ NAME="gfdl-howto"
of the License in the document and put the following copyright and
license notices just after the title page:</P
><A
NAME="AEN3573"
NAME="AEN3555"
></A
><BLOCKQUOTE
CLASS="BLOCKQUOTE"
@ -18311,7 +18260,7 @@ CLASS="glossdiv"
><H1
CLASS="glossdiv"
><A
NAME="AEN3578"
NAME="AEN3560"
>0-9, high ascii</A
></H1
><DL
@ -19221,7 +19170,7 @@ NAME="gloss-zarro"
Terry had the following to say:
</P
><A
NAME="AEN3823"
NAME="AEN3805"
></A
><TABLE
BORDER="0"

View File

@ -7,11 +7,11 @@
NAME="GENERATOR"
CONTENT="Modular DocBook HTML Stylesheet Version 1.79"><LINK
REL="HOME"
TITLE="The Bugzilla Guide - 3.2.4
TITLE="The Bugzilla Guide - 3.4
Release"
HREF="index.html"><LINK
REL="PREVIOUS"
TITLE="The Bugzilla Guide - 3.2.4
TITLE="The Bugzilla Guide - 3.4
Release"
HREF="index.html"><LINK
REL="NEXT"
@ -36,7 +36,7 @@ CELLSPACING="0"
><TH
COLSPAN="3"
ALIGN="center"
>The Bugzilla Guide - 3.2.4
>The Bugzilla Guide - 3.4
Release</TH
></TR
><TR
@ -154,7 +154,7 @@ ACCESSKEY="N"
WIDTH="33%"
ALIGN="left"
VALIGN="top"
>The Bugzilla Guide - 3.2.4
>The Bugzilla Guide - 3.4
Release</TD
><TD
WIDTH="34%"

View File

@ -7,7 +7,7 @@
NAME="GENERATOR"
CONTENT="Modular DocBook HTML Stylesheet Version 1.79"><LINK
REL="HOME"
TITLE="The Bugzilla Guide - 3.2.4
TITLE="The Bugzilla Guide - 3.4
Release"
HREF="index.html"><LINK
REL="PREVIOUS"
@ -35,7 +35,7 @@ CELLSPACING="0"
><TH
COLSPAN="3"
ALIGN="center"
>The Bugzilla Guide - 3.2.4
>The Bugzilla Guide - 3.4
Release</TH
></TR
><TR
@ -359,7 +359,7 @@ HREF="groups.html#users-and-groups"
></DT
><DT
>3.15.4. <A
HREF="groups.html#AEN2181"
HREF="groups.html#AEN2168"
>Assigning Group Controls to Products</A
></DT
></DL

View File

@ -115,6 +115,13 @@ name="METHODS"
>Bugzilla::Auth</a>, and <a href="./Bugzilla/User.html" class="podlinkpod"
>Bugzilla::User</a>.</p>
<dt><a name="page_requires_login"
><code class="code">page_requires_login</code></a></dt>
<dd>
<p>If the current page always requires the user to log in (for example, <code class="code">enter_bug.cgi</code> or any page called with <code class="code">?GoAheadAndLogIn=1</code>) then this will return something true. Otherwise it will return false. (This is set when you call <a href="#login" class="podlinkpod"
>&#34;login&#34;</a>.)</p>
<dt><a name="logout($option)"
><code class="code">logout($option)</code></a></dt>
@ -210,6 +217,19 @@ name="METHODS"
<dd>
<p>If you are running inside a code hook (see <a href="./Bugzilla/Hook.html" class="podlinkpod"
>Bugzilla::Hook</a>) this is how you get the arguments passed to the hook.</p>
<dt><a name="local_timezone"
><code class="code">local_timezone</code></a></dt>
<dd>
<p>Returns the local timezone of the Bugzilla installation, as a DateTime::TimeZone object. This detection is very time consuming, so we cache this information for future references.</p>
<dt><a name="job_queue"
><code class="code">job_queue</code></a></dt>
<dd>
<p>Returns a <a href="./Bugzilla/JobQueue.html" class="podlinkpod"
>Bugzilla::JobQueue</a> that you can use for queueing jobs. Will throw an error if job queueing is not correctly configured on this Bugzilla installation.</p>
</dd>
</dl>
<p class="backlinkbottom"><b><a name="___bottom" href="index.html" title="All Documents">&lt;&lt;</a></b></p>

View File

@ -26,7 +26,7 @@ Bugzilla::Attachment</title>
name="NAME"
>NAME</a></h1>
<p>Bugzilla::Attachment - a file related to a bug that a user has uploaded to the Bugzilla server</p>
<p>Bugzilla::Attachment - Bugzilla attachment class.</p>
<h1><a class='u' href='#___top' title='click to go to top of document'
name="SYNOPSIS"
@ -35,30 +35,25 @@ name="SYNOPSIS"
<pre class="code"> use Bugzilla::Attachment;
# Get the attachment with the given ID.
my $attachment = Bugzilla::Attachment-&#62;get($attach_id);
my $attachment = new Bugzilla::Attachment($attach_id);
# Get the attachments with the given IDs.
my $attachments = Bugzilla::Attachment-&#62;get_list($attach_ids);</pre>
my $attachments = Bugzilla::Attachment-&#62;new_from_list($attach_ids);</pre>
<h1><a class='u' href='#___top' title='click to go to top of document'
name="DESCRIPTION"
>DESCRIPTION</a></h1>
<p>This module defines attachment objects, which represent files related to bugs that users upload to the Bugzilla server.</p>
<p>Attachment.pm represents an attachment object. It is an implementation of <a href="../Bugzilla/Object.html" class="podlinkpod"
>Bugzilla::Object</a>, and thus provides all methods that <a href="../Bugzilla/Object.html" class="podlinkpod"
>Bugzilla::Object</a> provides.</p>
<p>The methods that are specific to <code class="code">Bugzilla::Attachment</code> are listed below.</p>
<h2><a class='u' href='#___top' title='click to go to top of document'
name="Instance_Properties"
>Instance Properties</a></h2>
<dl>
<dt><a name="id"
><code class="code">id</code></a></dt>
<dd>
<p>the unique identifier for the attachment</p>
</dd>
</dl>
<dl>
<dt><a name="bug_id"
><code class="code">bug_id</code></a></dt>
@ -68,6 +63,15 @@ name="Instance_Properties"
</dd>
</dl>
<dl>
<dt><a name="bug"
><code class="code">bug</code></a></dt>
<dd>
<p>the bug object to which the attachment is attached</p>
</dd>
</dl>
<dl>
<dt><a name="description"
><code class="code">description</code></a></dt>
@ -194,6 +198,15 @@ name="Instance_Properties"
</dd>
</dl>
<dl>
<dt><a name="flag_types"
><code class="code">flag_types</code></a></dt>
<dd>
<p>Return all flag types available for this attachment as well as flags already set, grouped by flag type.</p>
</dd>
</dl>
<h2><a class='u' href='#___top' title='click to go to top of document'
name="Class_Methods"
>Class Methods</a></h2>
@ -253,8 +266,8 @@ name="Class_Methods"
<p>Returns: 1 on success. Else an error is thrown.</p>
<dt><a name="insert_attachment_for_bug($throw_error,_$bug,_$user,_$timestamp,_$hr_vars)"
><code class="code">insert_attachment_for_bug($throw_error, $bug, $user, $timestamp, $hr_vars)</code></a></dt>
<dt><a name="create($throw_error,_$bug,_$user,_$timestamp,_$hr_vars)"
><code class="code">create($throw_error, $bug, $user, $timestamp, $hr_vars)</code></a></dt>
<dd>
<p>Description: inserts an attachment from CGI input for the given bug.</p>

View File

@ -16,7 +16,6 @@ Bugzilla::Classification</title>
<li class='indexItem indexItem1'><a href='#SYNOPSIS'>SYNOPSIS</a>
<li class='indexItem indexItem1'><a href='#DESCRIPTION'>DESCRIPTION</a>
<li class='indexItem indexItem1'><a href='#METHODS'>METHODS</a>
<li class='indexItem indexItem1'><a href='#SUBROUTINES'>SUBROUTINES</a>
</ul>
</div>
@ -38,20 +37,19 @@ name="SYNOPSIS"
my $id = $classification-&#62;id;
my $name = $classification-&#62;name;
my $description = $classification-&#62;description;
my $sortkey = $classification-&#62;sortkey;
my $product_count = $classification-&#62;product_count;
my $products = $classification-&#62;products;
my $hash_ref = Bugzilla::Classification::get_all_classifications();
my $classification = $hash_ref-&#62;{1};
my $classification =
Bugzilla::Classification::check_classification(&#39;AcmeClass&#39;);</pre>
my $products = $classification-&#62;products;</pre>
<h1><a class='u' href='#___top' title='click to go to top of document'
name="DESCRIPTION"
>DESCRIPTION</a></h1>
<p>Classification.pm represents a Classification object.</p>
<p>Classification.pm represents a classification object. It is an implementation of <a href="../Bugzilla/Object.html" class="podlinkpod"
>Bugzilla::Object</a>, and thus provides all methods that <a href="../Bugzilla/Object.html" class="podlinkpod"
>Bugzilla::Object</a> provides.</p>
<p>The methods that are specific to <code class="code">Bugzilla::Classification</code> are listed below.</p>
<p>A Classification is a higher-level grouping of Products.</p>
@ -60,22 +58,6 @@ name="METHODS"
>METHODS</a></h1>
<dl>
<dt><a name="new($param)"
><code class="code">new($param)</code></a></dt>
<dd>
<pre class="code"> Description: The constructor is used to load an existing
classification by passing a classification
id or classification name using a hash.
Params: $param - If you pass an integer, the integer is the
classification_id from the database that we
want to read in. If you pass in a hash with
&#39;name&#39; key, then the value of the name key
is the name of a classification from the DB.
Returns: A Bugzilla::Classification object.</pre>
<dt><a name="product_count()"
><code class="code">product_count()</code></a></dt>
@ -98,34 +80,6 @@ name="METHODS"
Returns: A reference to an array of Bugzilla::Product objects.</pre>
</dd>
</dl>
<h1><a class='u' href='#___top' title='click to go to top of document'
name="SUBROUTINES"
>SUBROUTINES</a></h1>
<dl>
<dt><a name="get_all_classifications()"
><code class="code">get_all_classifications()</code></a></dt>
<dd>
<pre class="code"> Description: Returns all classifications.
Params: none.
Returns: Bugzilla::Classification object list.</pre>
<dt><a name="check_classification($classification_name)"
><code class="code">check_classification($classification_name)</code></a></dt>
<dd>
<pre class="code"> Description: Checks if the classification name passed in is a
valid classification.
Params: $classification_name - String with a classification name.
Returns: Bugzilla::Classification object.</pre>
</dd>
</dl>
<p class="backlinkbottom"><b><a name="___bottom" href="../index.html" title="All Documents">&lt;&lt;</a></b></p>
<!-- end doc -->

View File

@ -354,7 +354,11 @@ name="SQL_Generation"
<dt><a name="$expr_-_SQL_expression_for_the_text_to_be_searched_(scalar)"
><code class="code">$expr</code> - SQL expression for the text to be searched (scalar)
<dt><a name="$pattern_-_the_regular_expression_to_search_for_(scalar)"
><code class="code">$pattern</code> - the regular expression to search for (scalar)</a></dt>
><code class="code">$pattern</code> - the regular expression to search for (scalar)
<dt><a name="$nocheck_-_true_if_the_pattern_should_not_be_tested;_false_otherwise_(boolean)"
><code class="code">$nocheck</code> - true if the pattern should not be tested; false otherwise (boolean)
<dt><a name="$real_pattern_-_the_real_regular_expression_to_search_for._This_argument_is_used_when_$pattern_is_a_placeholder_(&#39;?&#39;)."
><code class="code">$real_pattern</code> - the real regular expression to search for. This argument is used when <code class="code">$pattern</code> is a placeholder (&#39;?&#39;).</a></dt>
</dl>
<dt><a name="Returns"
@ -382,12 +386,8 @@ name="SQL_Generation"
><b>Params</b></a></dt>
<dd>
<dl>
<dt><a name="$expr_-_SQL_expression_for_the_text_to_be_searched_(scalar)"
><code class="code">$expr</code> - SQL expression for the text to be searched (scalar)
<dt><a name="$pattern_-_the_regular_expression_to_search_for_(scalar)"
><code class="code">$pattern</code> - the regular expression to search for (scalar)</a></dt>
</dl>
<p>Same as <a href="#sql_regexp" class="podlinkpod"
>&#34;sql_regexp&#34;</a>.</p>
<dt><a name="Returns"
><b>Returns</b></a></dt>
@ -653,6 +653,29 @@ name="SQL_Generation"
</dd>
</dl>
<dt><a name="sql_string_until"
><code class="code">sql_string_until</code></a></dt>
<dd>
<dl>
<dt><a name="Description"
><b>Description</b></a></dt>
<dd>
<p>Returns SQL for truncating a string at the first occurrence of a certain substring.</p>
<dt><a name="Params"
><b>Params</b></a></dt>
<dd>
<p>Note that both parameters need to be sql-quoted.</p>
<dt><a name="$string_The_string_we&#39;re_truncating"
><code class="code">$string</code> The string we&#39;re truncating
<dt><a name="$substring_The_substring_we&#39;re_truncating_at."
><code class="code">$substring</code> The substring we&#39;re truncating at.</a></dt>
</dl>
<dt><a name="sql_fulltext_search"
><code class="code">sql_fulltext_search</code></a></dt>

View File

@ -151,11 +151,79 @@ name="Instance_Properties"
</dl>
<dl>
<dt><a name="buglist"
><code class="code">buglist</code></a></dt>
<dd>
<p>A boolean specifying whether or not this field is selectable as a display or order column in buglist.cgi</p>
</dd>
</dl>
<dl>
<dt><a name="is_select"
><code class="code">is_select</code></a></dt>
<dd>
<p>True if this is a <code class="code">FIELD_TYPE_SINGLE_SELECT</code> or <code class="code">FIELD_TYPE_MULTI_SELECT</code> field. It is only safe to call <a href="#legal_values" class="podlinkpod"
>&#34;legal_values&#34;</a> if this is true.</p>
<dt><a name="legal_values"
><code class="code">legal_values</code></a></dt>
<dd>
<p>A reference to an array with valid active values for this field.</p>
<p>Valid values for this field, as an array of <a href="../Bugzilla/Field/Choice.html" class="podlinkpod"
>Bugzilla::Field::Choice</a> objects.</p>
</dd>
</dl>
<dl>
<dt><a name="visibility_field"
><code class="code">visibility_field</code></a></dt>
<dd>
<p>What field controls this field&#39;s visibility? Returns a <code class="code">Bugzilla::Field</code> object representing the field that controls this field&#39;s visibility.</p>
<p>Returns undef if there is no field that controls this field&#39;s visibility.</p>
</dd>
</dl>
<dl>
<dt><a name="visibility_value"
><code class="code">visibility_value</code></a></dt>
<dd>
<p>If we have a <a href="#visibility_field" class="podlinkpod"
>&#34;visibility_field&#34;</a>, then what value does that field have to be set to in order to show this field? Returns a <a href="../Bugzilla/Field/Choice.html" class="podlinkpod"
>Bugzilla::Field::Choice</a> or undef if there is no <code class="code">visibility_field</code> set.</p>
</dd>
</dl>
<dl>
<dt><a name="controls_visibility_of"
><code class="code">controls_visibility_of</code></a></dt>
<dd>
<p>An arrayref of <code class="code">Bugzilla::Field</code> objects, representing fields that this field controls the visibility of.</p>
</dd>
</dl>
<dl>
<dt><a name="value_field"
><code class="code">value_field</code></a></dt>
<dd>
<p>The Bugzilla::Field that controls the list of values for this field.</p>
<p>Returns undef if there is no field that controls this field&#39;s visibility.</p>
</dd>
</dl>
<dl>
<dt><a name="controls_values_of"
><code class="code">controls_values_of</code></a></dt>
<dd>
<p>An arrayref of <code class="code">Bugzilla::Field</code> objects, representing fields that this field controls the values of.</p>
</dd>
</dl>
@ -179,7 +247,15 @@ name="Instance_Mutators"
<dt><a name="set_sortkey"
><code class="code">set_sortkey</code>
<dt><a name="set_in_new_bugmail"
><code class="code">set_in_new_bugmail</code></a></dt>
><code class="code">set_in_new_bugmail</code>
<dt><a name="set_buglist"
><code class="code">set_buglist</code>
<dt><a name="set_visibility_field"
><code class="code">set_visibility_field</code>
<dt><a name="set_visibility_value"
><code class="code">set_visibility_value</code>
<dt><a name="set_value_field"
><code class="code">set_value_field</code></a></dt>
</dl>
<h2><a class='u' href='#___top' title='click to go to top of document'
@ -219,7 +295,9 @@ name="Class_Methods"
<dt><a name="sortkey_-_integer_-_The_sortkey_of_the_field._Defaults_to_0."
><code class="code">sortkey</code> - integer - The sortkey of the field. Defaults to 0.
<dt><a name="enter_bug_-_boolean_-_Whether_this_field_is_editable_on_the_bug_creation_form._Defaults_to_0."
><code class="code">enter_bug</code> - boolean - Whether this field is editable on the bug creation form. Defaults to 0.</a></dt>
><code class="code">enter_bug</code> - boolean - Whether this field is editable on the bug creation form. Defaults to 0.
<dt><a name="buglist_-_boolean_-_Whether_this_field_is_selectable_as_a_display_or_order_column_in_bug_lists._Defaults_to_0."
><code class="code">buglist</code> - boolean - Whether this field is selectable as a display or order column in bug lists. Defaults to 0.</a></dt>
<dd>
<p><code class="code">obsolete</code> - boolean - Whether this field is obsolete. Defaults to 0.</p>

View File

@ -0,0 +1,114 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>
Bugzilla::Field::Choice</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet" title="style" type="text/css" href="../.././../../../style.css" media="all" >
</head>
<body id="pod">
<p class="backlinktop"><b><a name="___top" href="../../index.html" accesskey="1" title="All Documents">&lt;&lt;</a></b></p>
<h1>Bugzilla::Field::Choice</h1>
<div class='indexgroup'>
<ul class='indexList indexList1'>
<li class='indexItem indexItem1'><a href='#NAME'>NAME</a>
<li class='indexItem indexItem1'><a href='#SYNOPSIS'>SYNOPSIS</a>
<li class='indexItem indexItem1'><a href='#DESCRIPTION'>DESCRIPTION</a>
<li class='indexItem indexItem1'><a href='#METHODS'>METHODS</a>
<ul class='indexList indexList2'>
<li class='indexItem indexItem2'><a href='#Class_Factory'>Class Factory</a>
<li class='indexItem indexItem2'><a href='#Accessors'>Accessors</a>
</ul>
</ul>
</div>
<h1><a class='u' href='#___top' title='click to go to top of document'
name="NAME"
>NAME</a></h1>
<p>Bugzilla::Field::Choice - A legal value for a &#60;select&#62;-type field.</p>
<h1><a class='u' href='#___top' title='click to go to top of document'
name="SYNOPSIS"
>SYNOPSIS</a></h1>
<pre class="code"> my $field = new Bugzilla::Field({name =&#62; &#39;bug_status&#39;});
my $choice = new Bugzilla::Field::Choice-&#62;type($field)-&#62;new(1);
my $choices = Bugzilla::Field::Choice-&#62;type($field)-&#62;new_from_list([1,2,3]);
my $choices = Bugzilla::Field::Choice-&#62;type($field)-&#62;get_all();
my $choices = Bugzilla::Field::Choice-&#62;type($field-&#62;match({ sortkey =&#62; 10 }); </pre>
<h1><a class='u' href='#___top' title='click to go to top of document'
name="DESCRIPTION"
>DESCRIPTION</a></h1>
<p>This is an implementation of <a href="../../Bugzilla/Object.html" class="podlinkpod"
>Bugzilla::Object</a>, but with a twist. You can&#39;t call any class methods (such as <code class="code">new</code>, <code class="code">create</code>, etc.) directly on <code class="code">Bugzilla::Field::Choice</code> itself. Instead, you have to call <code class="code">Bugzilla::Field::Choice-&#62;type($field)</code> to get the class you&#39;re going to instantiate, and then you call the methods on that.</p>
<p>We do that because each field has its own database table for its values, so each value type needs its own class.</p>
<p>See the <a href="#SYNOPSIS" class="podlinkpod"
>&#34;SYNOPSIS&#34;</a> for examples of how this works.</p>
<h1><a class='u' href='#___top' title='click to go to top of document'
name="METHODS"
>METHODS</a></h1>
<h2><a class='u' href='#___top' title='click to go to top of document'
name="Class_Factory"
>Class Factory</a></h2>
<p>In object-oriented design, a &#34;class factory&#34; is a method that picks and returns the right class for you, based on an argument that you pass.</p>
<dl>
<dt><a name="type"
><code class="code">type</code></a></dt>
<dd>
<p>Takes a single argument, which is either the name of a field from the <code class="code">fielddefs</code> table, or a <a href="../../Bugzilla/Field.html" class="podlinkpod"
>Bugzilla::Field</a> object representing a field.</p>
<p>Returns an appropriate subclass of <code class="code">Bugzilla::Field::Choice</code> that you can now call class methods on (like <code class="code">new</code>, <code class="code">create</code>, <code class="code">match</code>, etc.)</p>
<p><b>NOTE</b>: YOU CANNOT CALL CLASS METHODS ON <code class="code">Bugzilla::Field::Choice</code>. You must call <code class="code">type</code> to get a class you can call methods on.</p>
</dd>
</dl>
<h2><a class='u' href='#___top' title='click to go to top of document'
name="Accessors"
>Accessors</a></h2>
<p>These are in addition to the standard <a href="../../Bugzilla/Object.html" class="podlinkpod"
>Bugzilla::Object</a> accessors.</p>
<dl>
<dt><a name="sortkey"
><code class="code">sortkey</code></a></dt>
<dd>
<p>The key that determines the sort order of this item.</p>
<dt><a name="field"
><code class="code">field</code></a></dt>
<dd>
<p>The <a href="../../Bugzilla/Field.html" class="podlinkpod"
>Bugzilla::Field</a> object that this field value belongs to.</p>
<dt><a name="controlled_values"
><code class="code">controlled_values</code></a></dt>
<dd>
<p>Tells you which values in <b>other</b> fields appear (become visible) when this value is set in its field.</p>
<p>Returns a hashref of arrayrefs. The hash keys are the names of fields, and the values are arrays of <code class="code">Bugzilla::Field::Choice</code> objects, representing values that this value controls the visibility of, for that field.</p>
</dd>
</dl>
<p class="backlinkbottom"><b><a name="___bottom" href="../../index.html" title="All Documents">&lt;&lt;</a></b></p>
<!-- end doc -->
</body></html>

View File

@ -93,6 +93,12 @@ name="METHODS"
<dd>
<p>Returns an arrayref of <a href="../Bugzilla/User.html" class="podlinkpod"
>Bugzilla::User</a> objects representing people who are &#34;directly&#34; in this group, meaning that they&#39;re in it because they match the group regular expression, or they have been actually added to the group manually.</p>
<dt><a name="flatten_group_membership"
><code class="code">flatten_group_membership</code></a></dt>
<dd>
<p>Accepts a list of groups and returns a list of all the groups whose members inherit membership in any group on the list. So, we can determine if a user is in any of the groups input to flatten_group_membership by querying the user_group_map for any user with DIRECT or REGEXP membership IN() the list of groups returned.</p>
</dd>
</dl>
<p class="backlinkbottom"><b><a name="___bottom" href="../index.html" title="All Documents">&lt;&lt;</a></b></p>

View File

@ -23,15 +23,23 @@ Bugzilla::Hook</title>
<li class='indexItem indexItem1'><a href='#SUBROUTINES'>SUBROUTINES</a>
<li class='indexItem indexItem1'><a href='#HOOKS'>HOOKS</a>
<ul class='indexList indexList2'>
<li class='indexItem indexItem2'><a href='#auth-login_methods'>auth-login_methods</a>
<li class='indexItem indexItem2'><a href='#auth-verify_methods'>auth-verify_methods</a>
<li class='indexItem indexItem2'><a href='#bug-columns'>bug-columns</a>
<li class='indexItem indexItem2'><a href='#bug-end_of_create'>bug-end_of_create</a>
<li class='indexItem indexItem2'><a href='#bug-end_of_update'>bug-end_of_update</a>
<li class='indexItem indexItem2'><a href='#bug-fields'>bug-fields</a>
<li class='indexItem indexItem2'><a href='#buglist-columns'>buglist-columns</a>
<li class='indexItem indexItem2'><a href='#colchange-columns'>colchange-columns</a>
<li class='indexItem indexItem2'><a href='#config-add_panels'>config-add_panels</a>
<li class='indexItem indexItem2'><a href='#config-modify_panels'>config-modify_panels</a>
<li class='indexItem indexItem2'><a href='#enter_bug-entrydefaultvars'>enter_bug-entrydefaultvars</a>
<li class='indexItem indexItem2'><a href='#flag-end_of_update'>flag-end_of_update</a>
<li class='indexItem indexItem2'><a href='#install-before_final_checks'>install-before_final_checks</a>
<li class='indexItem indexItem2'><a href='#install-requirements'>install-requirements</a>
<li class='indexItem indexItem2'><a href='#install-update_db'>install-update_db</a>
<li class='indexItem indexItem2'><a href='#db_schema-abstract_schema'>db_schema-abstract_schema</a>
<li class='indexItem indexItem2'><a href='#mailer-before_send'>mailer-before_send</a>
<li class='indexItem indexItem2'><a href='#product-confirm_delete'>product-confirm_delete</a>
<li class='indexItem indexItem2'><a href='#webservice'>webservice</a>
<li class='indexItem indexItem2'><a href='#webservice-error_codes'>webservice-error_codes</a>
@ -129,6 +137,72 @@ name="HOOKS"
<p>This describes what hooks exist in Bugzilla currently. They are mostly in alphabetical order, but some related hooks are near each other instead of being alphabetical.</p>
<h2><a class='u' href='#___top' title='click to go to top of document'
name="auth-login_methods"
>auth-login_methods</a></h2>
<p>This allows you to add new login types to Bugzilla. (See <a href="../Bugzilla/Auth/Login.html" class="podlinkpod"
>Bugzilla::Auth::Login</a>.)</p>
<p>Params:</p>
<dl>
<dt><a name="modules"
><code class="code">modules</code></a></dt>
<dd>
<p>This is a hash--a mapping from login-type &#34;names&#34; to the actual module on disk. The keys will be all the values that were passed to <a href="../Bugzilla/Auth.html#login" class="podlinkpod"
>&#34;login&#34; in Bugzilla::Auth</a> for the <code class="code">Login</code> parameter. The values are the actual path to the module on disk. (For example, if the key is <code class="code">DB</code>, the value is <em class="code">Bugzilla/Auth/Login/DB.pm</em>.)</p>
<p>For your extension, the path will start with <em class="code">extensions/yourextension/lib/</em>. (See the code in the example extension.)</p>
<p>If your login type is in the hash as a key, you should set that key to the right path to your module. That module&#39;s <code class="code">new</code> method will be called, probably with empty parameters. If your login type is <i>not</i> in the hash, you should not set it.</p>
<p>You will be prevented from adding new keys to the hash, so make sure your key is in there before you modify it. (In other words, you can&#39;t add in login methods that weren&#39;t passed to <a href="../Bugzilla/Auth.html#login" class="podlinkpod"
>&#34;login&#34; in Bugzilla::Auth</a>.)</p>
</dd>
</dl>
<h2><a class='u' href='#___top' title='click to go to top of document'
name="auth-verify_methods"
>auth-verify_methods</a></h2>
<p>This works just like <a href="#auth-login_methods" class="podlinkpod"
>&#34;auth-login_methods&#34;</a> except it&#39;s for login verification methods (See <a href="../Bugzilla/Auth/Verify.html" class="podlinkpod"
>Bugzilla::Auth::Verify</a>.) It also takes a <code class="code">modules</code> parameter, just like <a href="#auth-login_methods" class="podlinkpod"
>&#34;auth-login_methods&#34;</a>.</p>
<h2><a class='u' href='#___top' title='click to go to top of document'
name="bug-columns"
>bug-columns</a></h2>
<p>This allows you to add new fields that will show up in every <a href="../Bugzilla/Bug.html" class="podlinkpod"
>Bugzilla::Bug</a> object. Note that you will also need to use the <a href="#bug-fields" class="podlinkpod"
>&#34;bug-fields&#34;</a> hook in conjunction with this hook to make this work.</p>
<p>Params:</p>
<dl>
<dt><a name="columns_-_An_arrayref_containing_an_array_of_column_names._Push_your_column_name(s)_onto_the_array."
><code class="code">columns</code> - An arrayref containing an array of column names. Push your column name(s) onto the array.</a></dt>
</dl>
<h2><a class='u' href='#___top' title='click to go to top of document'
name="bug-end_of_create"
>bug-end_of_create</a></h2>
<p>This happens at the end of <a href="../Bugzilla/Bug.html#create" class="podlinkpod"
>&#34;create&#34; in Bugzilla::Bug</a>, after all other changes are made to the database. This occurs inside a database transaction.</p>
<p>Params:</p>
<dl>
<dt><a name="bug_-_The_changed_bug_object,_with_all_fields_set_to_their_updated_values."
><code class="code">bug</code> - The changed bug object, with all fields set to their updated values.
<dt><a name="timestamp_-_The_timestamp_used_for_all_updates_in_this_transaction."
><code class="code">timestamp</code> - The timestamp used for all updates in this transaction.</a></dt>
</dl>
<h2><a class='u' href='#___top' title='click to go to top of document'
name="bug-end_of_update"
>bug-end_of_update</a></h2>
@ -147,6 +221,23 @@ name="bug-end_of_update"
><code class="code">changes</code> - The hash of changed fields. <code class="code">$changes-&#62;{field} = [old, new]</code></a></dt>
</dl>
<h2><a class='u' href='#___top' title='click to go to top of document'
name="bug-fields"
>bug-fields</a></h2>
<p>Allows the addition of database fields from the bugs table to the standard list of allowable fields in a <a href="../Bugzilla/Bug.html" class="podlinkpod"
>Bugzilla::Bug</a> object, so that you can call the field as a method.</p>
<p>Note: You should add here the names of any fields you added in <a href="#bug-columns" class="podlinkpod"
>&#34;bug-columns&#34;</a>.</p>
<p>Params:</p>
<dl>
<dt><a name="columns_-_A_arrayref_containing_an_array_of_column_names._Push_your_column_name(s)_onto_the_array."
><code class="code">columns</code> - A arrayref containing an array of column names. Push your column name(s) onto the array.</a></dt>
</dl>
<h2><a class='u' href='#___top' title='click to go to top of document'
name="buglist-columns"
>buglist-columns</a></h2>
@ -187,6 +278,45 @@ name="colchange-columns"
>&#34;buglist-columns&#34;</a>.</a></dt>
</dl>
<h2><a class='u' href='#___top' title='click to go to top of document'
name="config-add_panels"
>config-add_panels</a></h2>
<p>If you want to add new panels to the Parameters administrative interface, this is where you do it.</p>
<p>Params:</p>
<dl>
<dt><a name="panel_modules"
><code class="code">panel_modules</code></a></dt>
<dd>
<p>A hashref, where the keys are the &#34;name&#34; of the module and the value is the Perl module containing that config module. For example, if the name is <code class="code">Auth</code>, the value would be <code class="code">Bugzilla::Config::Auth</code>.</p>
<p>For your extension, the Perl module name must start with <code class="code">extensions::yourextension::lib</code>. (See the code in the example extension.)</p>
</dd>
</dl>
<h2><a class='u' href='#___top' title='click to go to top of document'
name="config-modify_panels"
>config-modify_panels</a></h2>
<p>This is how you modify already-existing panels in the Parameters administrative interface. For example, if you wanted to add a new Auth method (modifying Bugzilla::Config::Auth) this is how you&#39;d do it.</p>
<p>Params:</p>
<dl>
<dt><a name="panels"
><code class="code">panels</code></a></dt>
<dd>
<p>A hashref, where the keys are lower-case panel &#34;names&#34; (like <code class="code">auth</code>, <code class="code">admin</code>, etc.) and the values are hashrefs. The hashref contains a single key, <code class="code">params</code>. <code class="code">params</code> is an arrayref--the return value from <code class="code">get_param_list</code> for that module. You can modify <code class="code">params</code> and your changes will be reflected in the interface.</p>
<p>Adding new keys to <code class="code">panels</code> will have no effect. You should use <a href="#config-add_panels" class="podlinkpod"
>&#34;config-add_panels&#34;</a> if you want to add new panels.</p>
</dd>
</dl>
<h2><a class='u' href='#___top' title='click to go to top of document'
name="enter_bug-entrydefaultvars"
>enter_bug-entrydefaultvars</a></h2>
@ -277,6 +407,20 @@ name="db_schema-abstract_schema"
>&#34;ABSTRACT_SCHEMA&#34; in Bugzilla::DB::Schema</a>. Add new hash keys to make new table definitions. <em class="code">checksetup.pl</em> will automatically add these tables to the database when run.</a></dt>
</dl>
<h2><a class='u' href='#___top' title='click to go to top of document'
name="mailer-before_send"
>mailer-before_send</a></h2>
<p>Called right before <a href="../Bugzilla/Mailer.html" class="podlinkpod"
>Bugzilla::Mailer</a> sends a message to the MTA.</p>
<p>Params:</p>
<dl>
<dt><a name="email_-_The_Email::MIME_object_that&#39;s_about_to_be_sent."
><code class="code">email</code> - The <code class="code">Email::MIME</code> object that&#39;s about to be sent.</a></dt>
</dl>
<h2><a class='u' href='#___top' title='click to go to top of document'
name="product-confirm_delete"
>product-confirm_delete</a></h2>

View File

@ -73,23 +73,46 @@ name="SUBROUTINES"
>SUBROUTINES</a></h1>
<dl>
<dt><a name="read_localconfig($include_deprecated)"
><code class="code">read_localconfig($include_deprecated)</code></a></dt>
<dt><a name="read_localconfig"
><code class="code">read_localconfig</code></a></dt>
<dd>
<p>Description: Reads the localconfig file and returns all valid values in a hashref.</p>
<dl>
<dt><a name="Description"
><b>Description</b></a></dt>
<p>Params: <code class="code">$include_deprecated</code> - <code class="code">true</code> if you want the returned hashref to also include variables listed in <code class="code">OLD_LOCALCONFIG_VARS</code>, if they exist. Generally this is only for use by <code class="code">update_localconfig</code>.</p>
<dd>
<p>Reads the localconfig file and returns all valid values in a hashref.</p>
<p>Returns: A hashref of the localconfig variables. If an array is defined, it will be an arrayref in the returned hash. If a hash is defined, it will be a hashref in the returned hash. Only includes variables specified in <code class="code">LOCALCONFIG_VARS</code> (and <code class="code">OLD_LOCALCONFIG_VARS</code> if <code class="code">$include_deprecated</code> is specified).</p>
<dt><a name="Params"
><b>Params</b></a></dt>
<dt><a name="update_localconfig({_output_=&#62;_1_})"
><code class="code">update_localconfig({ output =&#62; 1 })</code></a></dt>
<dd>
<dl>
<dt><a name="$include_deprecated"
><code class="code">$include_deprecated</code></a></dt>
<dd>
<p><code class="code">true</code> if you want the returned hashref to include *any* variable currently defined in localconfig, even if it doesn&#39;t exist in <code class="code">LOCALCONFIG_VARS</code>. Generally this is is only for use by <a href="#update_localconfig" class="podlinkpod"
>&#34;update_localconfig&#34;</a>.</p>
</dd>
</dl>
<dt><a name="Returns"
><b>Returns</b></a></dt>
<dd>
<p>A hashref of the localconfig variables. If an array is defined in localconfig, it will be an arrayref in the returned hash. If a hash is defined, it will be a hashref in the returned hash. Only includes variables specified in <code class="code">LOCALCONFIG_VARS</code>, unless <code class="code">$include_deprecated</code> is true.</p>
</dd>
</dl>
<dt><a name="update_localconfig"
><code class="code">update_localconfig</code></a></dt>
<dd>
<p>Description: Adds any new variables to localconfig that aren&#39;t currently defined there. Also optionally prints out a message about vars that *should* be there and aren&#39;t. Exits the program if it adds any new vars.</p>
<p>Params: <code class="code">output</code> - <code class="code">true</code> if the function should display informational output and warnings. It will always display errors or any message which would cause program execution to halt.</p>
<p>Params: <code class="code">$output</code> - <code class="code">true</code> if the function should display informational output and warnings. It will always display errors or any message which would cause program execution to halt.</p>
<p>Returns: A hashref, with <code class="code">old_vals</code> being an array of names of variables that were removed, and <code class="code">new_vals</code> being an array of names of variables that were added to localconfig.</p>
</dd>

View File

@ -0,0 +1,54 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>
Bugzilla::JobQueue</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet" title="style" type="text/css" href=".././../../../style.css" media="all" >
</head>
<body id="pod">
<p class="backlinktop"><b><a name="___top" href="../index.html" accesskey="1" title="All Documents">&lt;&lt;</a></b></p>
<h1>Bugzilla::JobQueue</h1>
<div class='indexgroup'>
<ul class='indexList indexList1'>
<li class='indexItem indexItem1'><a href='#NAME'>NAME</a>
<li class='indexItem indexItem1'><a href='#SYNOPSIS'>SYNOPSIS</a>
<li class='indexItem indexItem1'><a href='#DESCRIPTION'>DESCRIPTION</a>
<ul class='indexList indexList2'>
<li class='indexItem indexItem2'><a href='#Inserting_a_Job'>Inserting a Job</a>
</ul>
</ul>
</div>
<h1><a class='u' href='#___top' title='click to go to top of document'
name="NAME"
>NAME</a></h1>
<p>Bugzilla::JobQueue - Interface between Bugzilla and TheSchwartz.</p>
<h1><a class='u' href='#___top' title='click to go to top of document'
name="SYNOPSIS"
>SYNOPSIS</a></h1>
<pre class="code"> use Bugzilla;
my $obj = Bugzilla-&#62;job_queue();
$obj-&#62;insert(&#39;send_mail&#39;, { msg =&#62; $message });</pre>
<h1><a class='u' href='#___top' title='click to go to top of document'
name="DESCRIPTION"
>DESCRIPTION</a></h1>
<p>Certain tasks should be done asyncronously. The job queue system allows Bugzilla to use some sort of service to schedule jobs to happen asyncronously.</p>
<h2><a class='u' href='#___top' title='click to go to top of document'
name="Inserting_a_Job"
>Inserting a Job</a></h2>
<p>See the synopsis above for an easy to follow example on how to insert a job into the queue. Give it a name and some arguments and the job will be sent away to be done later.</p>
<p class="backlinkbottom"><b><a name="___bottom" href="../index.html" title="All Documents">&lt;&lt;</a></b></p>
<!-- end doc -->
</body></html>

View File

@ -0,0 +1,45 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>
Bugzilla::JobQueue::Runner</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet" title="style" type="text/css" href="../.././../../../style.css" media="all" >
</head>
<body id="pod">
<p class="backlinktop"><b><a name="___top" href="../../index.html" accesskey="1" title="All Documents">&lt;&lt;</a></b></p>
<h1>Bugzilla::JobQueue::Runner</h1>
<div class='indexgroup'>
<ul class='indexList indexList1'>
<li class='indexItem indexItem1'><a href='#NAME'>NAME</a>
<li class='indexItem indexItem1'><a href='#SYNOPSIS'>SYNOPSIS</a>
<li class='indexItem indexItem1'><a href='#DESCRIPTION'>DESCRIPTION</a>
</ul>
</div>
<h1><a class='u' href='#___top' title='click to go to top of document'
name="NAME"
>NAME</a></h1>
<p>Bugzilla::JobQueue::Runner - A class representing the daemon that runs the job queue.</p>
<h1><a class='u' href='#___top' title='click to go to top of document'
name="SYNOPSIS"
>SYNOPSIS</a></h1>
<pre class="code"> use Bugzilla::JobQueue::Runner;
Bugzilla::JobQueue::Runner-&#62;new();</pre>
<h1><a class='u' href='#___top' title='click to go to top of document'
name="DESCRIPTION"
>DESCRIPTION</a></h1>
<p>This is a subclass of <a href="../../Daemon/Generic.html" class="podlinkpod"
>Daemon::Generic</a> that is used by <a href="../../jobqueue.html" class="podlinkpod"
>jobqueue</a> to run the Bugzilla job queue.</p>
<p class="backlinkbottom"><b><a name="___bottom" href="../../index.html" title="All Documents">&lt;&lt;</a></b></p>
<!-- end doc -->
</body></html>

View File

@ -20,7 +20,7 @@ Bugzilla::Object</title>
<ul class='indexList indexList2'>
<li class='indexItem indexItem2'><a href='#Constructors'>Constructors</a>
<li class='indexItem indexItem2'><a href='#Database_Manipulation'>Database Manipulation</a>
<li class='indexItem indexItem2'><a href='#Subclass_Helpers'>Subclass Helpers</a>
<li class='indexItem indexItem2'><a href='#Mutators'>Mutators</a>
<li class='indexItem indexItem2'><a href='#Simple_Validators'>Simple Validators</a>
</ul>
<li class='indexItem indexItem1'><a href='#CLASS_FUNCTIONS'>CLASS FUNCTIONS</a>
@ -188,7 +188,7 @@ name="Constructors"
>&#34;ID_FIELD&#34;</a> column).</p>
<p>If you pass in a hashref, you can pass a <code class="code">name</code> key. The value of the <code class="code">name</code> key is the case-insensitive name of the object (from <a href="#NAME_FIELD" class="podlinkpod"
>&#34;NAME_FIELD&#34;</a>) in the DB.</p>
>&#34;NAME_FIELD&#34;</a>) in the DB. You can also pass in an <code class="code">id</code> key which will be interpreted as the id of the object you want (overriding the <code class="code">name</code> key).</p>
<p><b>Additional Parameters Available for Subclasses</b></p>
@ -279,6 +279,16 @@ name="Constructors"
<p>There are two special values, the constants <code class="code">NULL</code> and <code class="code">NOT_NULL</code>, which means &#34;give me objects where this field is NULL or NOT NULL, respectively.&#34;</p>
<p>In addition to the column keys, there are a few special keys that can be used to rig the underlying database queries. These are <code class="code">LIMIT</code>, <code class="code">OFFSET</code>, and <code class="code">WHERE</code>.</p>
<p>The value for the <code class="code">LIMIT</code> key is expected to be an integer defining the number of objects to return, while the value for <code class="code">OFFSET</code> defines the position, relative to the number of objects the query would normally return, at which to begin the result set. If <code class="code">OFFSET</code> is defined without a corresponding <code class="code">LIMIT</code> it is silently ignored.</p>
<p>The <code class="code">WHERE</code> key provides a mechanism for adding arbitrary WHERE clauses to the underlying query. Its value is expected to a hash reference whose keys are the columns, operators and placeholders, and the values are the placeholders&#39; bind value. For example:</p>
<pre class="code"> WHERE =&#62; { &#39;some_column &#62;= ?&#39; =&#62; $some_value }</pre>
<p>would constrain the query to only those objects in the table whose &#39;some_column&#39; column has a value greater than or equal to $some_value.</p>
<p>If you don&#39;t specify any criteria, calling this function is the same as doing <code class="code">[$class-&#62;get_all]</code>.</p>
<dt><a name="Returns"
@ -388,20 +398,32 @@ name="Database_Manipulation"
><b>Returns</b></a></dt>
<dd>
<p><b>In scalar context:</b></p>
<p>A hashref showing what changed during the update. The keys are the column names from <a href="#UPDATE_COLUMNS" class="podlinkpod"
>&#34;UPDATE_COLUMNS&#34;</a>. If a field was not changed, it will not be in the hash at all. If the field was changed, the key will point to an arrayref. The first item of the arrayref will be the old value, and the second item will be the new value.</p>
<p>If there were no changes, we return a reference to an empty hash.</p>
<p><b>In array context:</b></p>
<p>Returns a list, where the first item is the above hashref. The second item is the object as it was in the database before update() was called. (This is mostly useful to subclasses of <code class="code">Bugzilla::Object</code> that are implementing <code class="code">update</code>.)</p>
</dd>
</dl>
<dt><a name="remove_from_db"
><code class="code">remove_from_db</code></a></dt>
<dd>
<p>Removes this object from the database. Will throw an error if you can&#39;t remove it for some reason. The object will then be destroyed, as it is not safe to use the object after it has been removed from the database.</p>
</dd>
</dl>
<h2><a class='u' href='#___top' title='click to go to top of document'
name="Subclass_Helpers"
>Subclass Helpers</a></h2>
name="Mutators"
>Mutators</a></h2>
<p>These functions are intended only for use by subclasses. If you call them from anywhere else, they will throw a <code class="code">CodeError</code>.</p>
<p>These are used for updating the values in objects, before calling <code class="code">update</code>.</p>
<dl>
<dt><a name="set"
@ -420,6 +442,8 @@ name="Subclass_Helpers"
<p>See <a href="#VALIDATORS" class="podlinkpod"
>&#34;VALIDATORS&#34;</a> for more information.</p>
<p><b>NOTE</b>: This function is intended only for use by subclasses. If you call it from anywhere else, it will throw a <code class="code">CodeError</code>.</p>
<dt><a name="Params"
><b>Params</b></a></dt>
@ -432,6 +456,27 @@ name="Subclass_Helpers"
><code class="code">$value</code> - The value that you&#39;re setting the field to.</a></dt>
</dl>
<dt><a name="Returns_(nothing)"
><b>Returns</b> (nothing)</a></dt>
</dl>
<dt><a name="set_all"
><code class="code">set_all</code></a></dt>
<dd>
<dl>
<dt><a name="Description"
><b>Description</b></a></dt>
<dd>
<p>This is a convenience function which is simpler than calling many different <code class="code">set_</code> functions in a row. You pass a hashref of parameters and it calls <code class="code">set_$key($value)</code> for every item in the hashref.</p>
<dt><a name="Params"
><b>Params</b></a></dt>
<dd>
<p>Takes a hashref of the fields that need to be set, pointing to the value that should be passed to the <code class="code">set_</code> function that is called.</p>
<dt><a name="Returns_(nothing)"
><b>Returns</b> (nothing)</a></dt>
</dl>

View File

@ -89,7 +89,10 @@ name="METHODS"
<pre class="code"> Description: Returns a hash (group id as key) with all product
group controls.
Params: none.
Params: $full_data (optional, false by default) - when true,
the number of bugs per group applicable to the product
is also returned. Moreover, bug groups which have no
special settings for the product are also returned.
Returns: A hash with group id as key and hash containing
a Bugzilla::Group object and the properties of group

View File

@ -213,11 +213,18 @@ name="Other_Methods"
<dd>
<p>Returns a hash of hashes which holds the user&#39;s settings. The first key is the name of the setting, as found in setting.name. The second key is one of: is_enabled - true if the user is allowed to set the preference themselves; false to force the site defaults for themselves or must accept the global site default value default_value - the global site default for this setting value - the value of this setting for this user. Will be the same as the default_value if the user is not logged in, or if is_default is true. is_default - a boolean to indicate whether the user has chosen to make a preference for themself or use the site default.</p>
<dt><a name="timezone"
><code class="code">timezone</code></a></dt>
<dd>
<p>Returns the timezone used to display dates and times to the user, as a DateTime::TimeZone object.</p>
<dt><a name="groups"
><code class="code">groups</code></a></dt>
<dd>
<p>Returns a hashref of group names for groups the user is a member of. The keys are the names of the groups, whilst the values are the respective group ids. (This is so that a set of all groupids for groups the user is in can be obtained by <code class="code">values(%{$user-&#62;groups})</code>.)</p>
<p>Returns an arrayref of <a href="../Bugzilla/Group.html" class="podlinkpod"
>Bugzilla::Group</a> objects representing groups that this user is a member of.</p>
<dt><a name="groups_as_string"
><code class="code">groups_as_string</code></a></dt>
@ -241,7 +248,10 @@ name="Other_Methods"
><code class="code">bless_groups</code></a></dt>
<dd>
<p>Returns an arrayref of hashes of <code class="code">groups</code> entries, where the keys of each hash are the names of <code class="code">id</code>, <code class="code">name</code> and <code class="code">description</code> columns of the <code class="code">groups</code> table. The arrayref consists of the groups the user can bless, taking into account that having editusers permissions means that you can bless all groups, and that you need to be aware of a group in order to bless a group.</p>
<p>Returns an arrayref of <a href="../Bugzilla/Group.html" class="podlinkpod"
>Bugzilla::Group</a> objects.</p>
<p>The arrayref consists of the groups the user can bless, taking into account that having editusers permissions means that you can bless all groups, and that you need to be able to see a group in order to bless it.</p>
<dt><a name="get_products_by_permission($group)"
><code class="code">get_products_by_permission($group)</code></a></dt>
@ -389,12 +399,6 @@ name="Other_Methods"
<dd>
<p>Returns a reference to an array of users. The array is populated with hashrefs containing the login, identity and visibility. Users that are not visible to this user will have &#39;visible&#39; set to zero.</p>
<dt><a name="flatten_group_membership"
><code class="code">flatten_group_membership</code></a></dt>
<dd>
<p>Accepts a list of groups and returns a list of all the groups whose members inherit membership in any group on the list. So, we can determine if a user is in any of the groups input to flatten_group_membership by querying the user_group_map for any user with DIRECT or REGEXP membership IN() the list of groups returned.</p>
<dt><a name="direct_group_membership"
><code class="code">direct_group_membership</code></a></dt>

View File

@ -0,0 +1,53 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>
Bugzilla::User::Setting::Timezone</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet" title="style" type="text/css" href="../../.././../../../style.css" media="all" >
</head>
<body id="pod">
<p class="backlinktop"><b><a name="___top" href="../../../index.html" accesskey="1" title="All Documents">&lt;&lt;</a></b></p>
<h1>Bugzilla::User::Setting::Timezone</h1>
<div class='indexgroup'>
<ul class='indexList indexList1'>
<li class='indexItem indexItem1'><a href='#NAME'>NAME</a>
<li class='indexItem indexItem1'><a href='#DESCRIPTION'>DESCRIPTION</a>
<li class='indexItem indexItem1'><a href='#METHODS'>METHODS</a>
</ul>
</div>
<h1><a class='u' href='#___top' title='click to go to top of document'
name="NAME"
>NAME</a></h1>
<p>Bugzilla::User::Setting::Timezone - Object for a user preference setting for desired timezone</p>
<h1><a class='u' href='#___top' title='click to go to top of document'
name="DESCRIPTION"
>DESCRIPTION</a></h1>
<p>Timezone.pm extends Bugzilla::User::Setting and implements a class specialized for setting the desired timezone.</p>
<h1><a class='u' href='#___top' title='click to go to top of document'
name="METHODS"
>METHODS</a></h1>
<dl>
<dt><a name="legal_values()"
><code class="code">legal_values()</code></a></dt>
<dd>
<p>Description: Returns all legal timezones</p>
<p>Params: none</p>
<p>Returns: A reference to an array containing the names of all legal timezones</p>
</dd>
</dl>
<p class="backlinkbottom"><b><a name="___bottom" href="../../../index.html" title="All Documents">&lt;&lt;</a></b></p>
<!-- end doc -->
</body></html>

View File

@ -44,7 +44,6 @@ name="SYNOPSIS"
<pre class="code"> use Bugzilla::Util;
# Functions for dealing with variable tainting
$rv = is_tainted($var);
trick_taint($var);
detaint_natural($var);
detaint_signed($var);
@ -53,6 +52,7 @@ name="SYNOPSIS"
html_quote($var);
url_quote($var);
xml_quote($var);
email_filter($var);
# Functions for decoding
$rv = url_decode($var);
@ -70,7 +70,6 @@ name="SYNOPSIS"
# Functions for manipulating strings
$val = trim(&#34; abc &#34;);
($removed, $added) = diff_strings($old, $new);
$wrapped = wrap_comment($comment);
# Functions for formatting time
@ -108,12 +107,6 @@ name="Tainting"
<p>Several functions are available to deal with tainted variables. <b>Use these with care</b> to avoid security holes.</p>
<dl>
<dt><a name="is_tainted"
><code class="code">is_tainted</code></a></dt>
<dd>
<p>Determines whether a particular variable is tainted</p>
<dt><a name="trick_taint($val)"
><code class="code">trick_taint($val)</code></a></dt>
@ -180,6 +173,12 @@ name="Quoting"
<dd>
<p>Converts the %xx encoding from the given URL back to its original form.</p>
<dt><a name="email_filter"
><code class="code">email_filter</code></a></dt>
<dd>
<p>Removes the hostname from email addresses in the string, if the user currently viewing Bugzilla is logged out. If the user is logged-in, this filter just returns the input string.</p>
</dd>
</dl>
@ -265,12 +264,6 @@ name="String_Manipulation"
<dd>
<p>Removes any leading or trailing whitespace from a string. This routine does not modify the existing string.</p>
<dt><a name="diff_strings($oldstr,_$newstr)"
><code class="code">diff_strings($oldstr, $newstr)</code></a></dt>
<dd>
<p>Takes two strings containing a list of comma- or space-separated items and returns what items were removed from or added to the new one, compared to the old one. Returns a list, where the first entry is a scalar containing removed items, and the second entry is a scalar containing added items.</p>
<dt><a name="wrap_hard($string,_$size)"
><code class="code">wrap_hard($string, $size)</code></a></dt>
@ -348,9 +341,10 @@ name="Formatting_Time"
><code class="code">format_time($time)</code></a></dt>
<dd>
<p>Takes a time, converts it to the desired format and appends the timezone as defined in editparams.cgi, if desired. This routine will be expanded in the future to adjust for user preferences regarding what timezone to display times in.</p>
<p>Takes a time and converts it to the desired format and timezone. If no format is given, the routine guesses the correct one and returns an empty array if it cannot. If no timezone is given, the user&#39;s timezone is used, as defined in his preferences.</p>
<p>This routine is mainly called from templates to filter dates, see &#34;FILTER time&#34; in Templates.pm. In this case, $format is undefined and the routine has to &#34;guess&#34; the date format that was passed to $dbh-&#62;sql_date_format().</p>
<p>This routine is mainly called from templates to filter dates, see &#34;FILTER time&#34; in <a href="../Bugzilla/Template.html" class="podlinkpod"
>Bugzilla::Template</a>.</p>
<dt><a name="format_time_decimal($time)"
><code class="code">format_time_decimal($time)</code></a></dt>
@ -378,13 +372,13 @@ name="Cryptography"
>Cryptography</a></h2>
<dl>
<dt><a name="bz_crypt($password)"
><code class="code">bz_crypt($password)</code></a></dt>
<dt><a name="bz_crypt($password,_$salt)"
><code class="code">bz_crypt($password, $salt)</code></a></dt>
<dd>
<p>Takes a string and returns a <code class="code">crypt</code>ed value for it, using a random salt.</p>
<p>Takes a string and returns a hashed (encrypted) value for it, using a random salt. An optional salt string may also be passed in.</p>
<p>Please always use this function instead of the built-in perl &#34;crypt&#34; when initially encrypting a password.</p>
<p>Please always use this function instead of the built-in perl <code class="code">crypt</code> function, when checking or setting a password. Bugzilla does not use <code class="code">crypt</code>.</p>
<dt><a name="generate_random_password($password_length)"
><code class="code">generate_random_password($password_length)</code></a></dt>

View File

@ -28,6 +28,14 @@ Bugzilla::WebService</title>
<li class='indexItem indexItem2'><a href='#Transient_vs._Fatal_Errors'>Transient vs. Fatal Errors</a>
<li class='indexItem indexItem2'><a href='#Unknown_Errors'>Unknown Errors</a>
</ul>
<li class='indexItem indexItem1'><a href='#COMMON_PARAMETERS'>COMMON PARAMETERS</a>
<ul class='indexList indexList2'>
<li class='indexItem indexItem2'><a href='#Limiting_What_Fields_Are_Returned'>Limiting What Fields Are Returned</a>
</ul>
<li class='indexItem indexItem1'><a href='#EXTENSIONS_TO_THE_XML-RPC_STANDARD'>EXTENSIONS TO THE XML-RPC STANDARD</a>
<ul class='indexList indexList2'>
<li class='indexItem indexItem2'><a href='#Undefined_Values'>Undefined Values</a>
</ul>
</ul>
</div>
@ -178,6 +186,77 @@ name="Unknown_Errors"
>Unknown Errors</a></h2>
<p>Sometimes a function will throw an error that doesn&#39;t have a specific error code. In this case, the code will be <code class="code">-32000</code> if it&#39;s a &#34;fatal&#34; error, and <code class="code">32000</code> if it&#39;s a &#34;transient&#34; error.</p>
<h1><a class='u' href='#___top' title='click to go to top of document'
name="COMMON_PARAMETERS"
>COMMON PARAMETERS</a></h1>
<p>Many Webservice methods take similar arguments. Instead of re-writing the documentation for each method, we document the parameters here, once, and then refer back to this documentation from the individual methods where these parameters are used.</p>
<h2><a class='u' href='#___top' title='click to go to top of document'
name="Limiting_What_Fields_Are_Returned"
>Limiting What Fields Are Returned</a></h2>
<p>Many WebService methods return an array of structs with various fields in the structs. (For example, <a href="../Bugzilla/WebService/Bug.html#get" class="podlinkpod"
>&#34;get&#34; in Bugzilla::WebService::Bug</a> returns a list of <code class="code">bugs</code> that have fields like <code class="code">id</code>, <code class="code">summary</code>, <code class="code">creation_time</code>, etc.)</p>
<p>These parameters allow you to limit what fields are present in the structs, to possibly improve performance or save some bandwidth.</p>
<dl>
<dt><a name="include_fields_(array)"
><code class="code">include_fields</code> (array)</a></dt>
<dd>
<p>An array of strings, representing the (case-sensitive) names of fields. Only the fields specified in this hash will be returned, the rest will not be included.</p>
<p>If you specify an empty array, then this function will return empty hashes.</p>
<p>Invalid field names are ignored.</p>
<p>Example:</p>
<pre class="code"> User.get( ids =&#62; [1], include_fields =&#62; [&#39;id&#39;, &#39;name&#39;] )</pre>
<p>would return something like:</p>
<pre class="code"> { users =&#62; [{ id =&#62; 1, name =&#62; &#39;user@domain.com&#39; }] }</pre>
<dt><a name="exclude_fields_(array)"
><code class="code">exclude_fields</code> (array)</a></dt>
<dd>
<p>An array of strings, representing the (case-sensitive) names of fields. The fields specified will not be included in the returned hashes.</p>
<p>If you specify all the fields, then this function will return empty hashes.</p>
<p>Invalid field names are ignored.</p>
<p>Specifying fields here overrides <code class="code">include_fields</code>, so if you specify a field in both, it will be excluded, not included.</p>
<p>Example:</p>
<pre class="code"> User.get( ids =&#62; [1], exclude_fields =&#62; [&#39;name&#39;] )</pre>
<p>would return something like:</p>
<pre class="code"> { users =&#62; [{ id =&#62; 1, real_name =&#62; &#39;John Smith&#39; }] }</pre>
</dd>
</dl>
<h1><a class='u' href='#___top' title='click to go to top of document'
name="EXTENSIONS_TO_THE_XML-RPC_STANDARD"
>EXTENSIONS TO THE XML-RPC STANDARD</a></h1>
<h2><a class='u' href='#___top' title='click to go to top of document'
name="Undefined_Values"
>Undefined Values</a></h2>
<p>Normally, XML-RPC does not allow empty values for <code class="code">int</code>, <code class="code">double</code>, or <code class="code">dateTime.iso8601</code> fields. Bugzilla does--it treats empty values as <code class="code">undef</code> (called <code class="code">NULL</code> or <code class="code">None</code> in some programming languages).</p>
<p>Bugzilla also accepts an element called <code class="code">&#60;nil&#62;</code>, as specified by the XML-RPC extension here: <a href="http://ontosys.com/xml-rpc/extensions.php" class="podlinkurl"
>http://ontosys.com/xml-rpc/extensions.php</a>, which is always considered to be <code class="code">undef</code>, no matter what it contains.</p>
<p>Bugzilla does not use <code class="code">&#60;nil&#62;</code> values in returned data, because currently most clients do not support <code class="code">&#60;nil&#62;</code>. Instead, any fields with <code class="code">undef</code> values will be stripped from the response completely. Therefore <b>the client must handle the fact that some expected fields may not be returned</b>.</p>
<p class="backlinkbottom"><b><a name="___bottom" href="../index.html" title="All Documents">&lt;&lt;</a></b></p>
<!-- end doc -->

File diff suppressed because it is too large Load Diff

View File

@ -109,7 +109,9 @@ One of the values that must be returned is the &#39;version&#39; of the extensio
><code class="code">timezone</code></a></dt>
<dd>
<p><b>STABLE</b></p>
<p><b>DEPRECATED</b> This method may be removed in a future version of Bugzilla.
Use <a href="#time" class="podlinkpod"
>&#34;time&#34;</a> instead.</p>
<dl>
<dt><a name="Description"
@ -130,6 +132,100 @@ This is important because all dates/times that the webservice interface returns
that is the timezone offset as a string in (+/-)XXXX (RFC 2822) format.</p>
</dd>
</dl>
<dt><a name="time"
><code class="code">time</code></a></dt>
<dd>
<p><b>UNSTABLE</b></p>
<dl>
<dt><a name="Description"
><b>Description</b></a></dt>
<dd>
<p>Gets information about what time the Bugzilla server thinks it is,
and what timezone it&#39;s running in.</p>
<dt><a name="Params_(none)"
><b>Params</b> (none)
<dt><a name="Returns"
><b>Returns</b></a></dt>
<dd>
<p>A struct with the following items:</p>
<dl>
<dt><a name="db_time"
><code class="code">db_time</code></a></dt>
<dd>
<p><code class="code">dateTime</code> The current time in Bugzilla&#39;s <b>local time zone</b>,
according to the Bugzilla <i>database server</i>.</p>
<p>Note that Bugzilla assumes that the database and the webserver are running in the same time zone.
However,
if the web server and the database server aren&#39;t synchronized for some reason,
<i>this</i> is the time that you should rely on for doing searches and other input to the WebService.</p>
<dt><a name="web_time"
><code class="code">web_time</code></a></dt>
<dd>
<p><code class="code">dateTime</code> This is the current time in Bugzilla&#39;s <b>local time zone</b>,
according to Bugzilla&#39;s <i>web server</i>.</p>
<p>This might be different by a second from <code class="code">db_time</code> since this comes from a different source.
If it&#39;s any more different than a second,
then there is likely some problem with this Bugzilla instance.
In this case you should rely on the <code class="code">db_time</code>,
not the <code class="code">web_time</code>.</p>
<dt><a name="web_time_utc"
><code class="code">web_time_utc</code></a></dt>
<dd>
<p>The same as <code class="code">web_time</code>,
but in the <b>UTC</b> time zone instead of the local time zone.</p>
<dt><a name="tz_name"
><code class="code">tz_name</code></a></dt>
<dd>
<p><code class="code">string</code> The long name of the time zone that the Bugzilla web server is in.
Will usually look something like: <code class="code">America/Los Angeles</code></p>
<dt><a name="tz_short_name"
><code class="code">tz_short_name</code></a></dt>
<dd>
<p><code class="code">string</code> The &#34;short name&#34; of the time zone that the Bugzilla web server is in.
This should only be used for display,
and not relied on for your programs,
because different time zones can have the same short name.
(For example,
there are two <code class="code">EST</code>s.)</p>
<p>This will look something like: <code class="code">PST</code>.</p>
<dt><a name="tz_offset"
><code class="code">tz_offset</code></a></dt>
<dd>
<p><code class="code">string</code> The timezone offset as a string in (+/-)XXXX (RFC 2822) format.</p>
</dd>
</dl>
<dt><a name="History"
><b>History</b></a></dt>
<dd>
<dl>
<dt><a name="Added_in_Bugzilla_3.4."
>Added in Bugzilla <b>3.4</b>.</a></dt>
</dl>
</dd>
</dl>
</dd>
</dl>
<p class="backlinkbottom"><b><a name="___bottom" href="../../index.html" title="All Documents">&lt;&lt;</a></b></p>

View File

@ -18,6 +18,7 @@ Bugzilla::Webservice::User</title>
<ul class='indexList indexList2'>
<li class='indexItem indexItem2'><a href='#Logging_In_and_Out'>Logging In and Out</a>
<li class='indexItem indexItem2'><a href='#Account_Creation'>Account Creation</a>
<li class='indexItem indexItem2'><a href='#User_Info'>User Info</a>
</ul>
</ul>
</div>
@ -280,6 +281,180 @@ this means the password is under three characters.)</p>
this means the password is over ten characters.)</p>
</dd>
</dl>
</dd>
</dl>
</dd>
</dl>
<h2><a class='u' href='#___top' title='click to go to top of document'
name="User_Info"
>User Info</a></h2>
<dl>
<dt><a name="get"
><code class="code">get</code></a></dt>
<dd>
<p><b>UNSTABLE</b></p>
<dl>
<dt><a name="Description"
><b>Description</b></a></dt>
<dd>
<p>Gets information about user accounts in Bugzilla.</p>
<dt><a name="Params"
><b>Params</b></a></dt>
<dd>
<p><b>Note</b>: At least one of <code class="code">ids</code>,
<code class="code">names</code>,
or <code class="code">match</code> must be specified.</p>
<p><b>Note</b>: Users will not be returned more than once,
so even if a user is matched by more than one argument,
only one user will be returned.</p>
<p>In addition to the parameters below,
this method also accepts the standard <a href="../../Bugzilla/WebService.html#include_fields" class="podlinkpod"
>include_fields</a> and <a href="../../Bugzilla/WebService.html#exclude_fields" class="podlinkpod"
>exclude_fields</a> arguments.</p>
<dl>
<dt><a name="ids_(array)"
><code class="code">ids</code> (array)</a></dt>
<dd>
<p>An array of integers,
representing user ids.</p>
<p>Logged-out users cannot pass this parameter to this function.
If they try,
they will get an error.
Logged-in users will get an error if they specify the id of a user they cannot see.</p>
<dt><a name="names_(array)_-_An_array_of_login_names_(strings)."
><code class="code">names</code> (array) - An array of login names (strings).
<dt><a name="match_(array)"
><code class="code">match</code> (array)</a></dt>
<dd>
<p>An array of strings.
This works just like &#34;user matching&#34; in Bugzilla itself.
Users will be returned whose real name or login name contains any one of the specified strings.
Users that you cannot see will not be included in the returned list.</p>
<p>Some Bugzilla installations have user-matching turned off,
in which case you will only be returned exact matches.</p>
<p>Most installations have a limit on how many matches are returned for each string,
which defaults to 1000 but can be changed by the Bugzilla administrator.</p>
<p>Logged-out users cannot use this argument,
and an error will be thrown if they try.
(This is to make it harder for spammers to harvest email addresses from Bugzilla,
and also to enforce the user visibility restrictions that are implemented on some Bugzillas.)</p>
</dd>
</dl>
<dt><a name="Returns"
><b>Returns</b></a></dt>
<dd>
<p>A hash containing one item,
<code class="code">users</code>,
that is an array of hashes.
Each hash describes a user,
and has the following items:</p>
<dl>
<dt><a name="id"
>id</a></dt>
<dd>
<p><code class="code">int</code> The unique integer ID that Bugzilla uses to represent this user.
Even if the user&#39;s login name changes,
this will not change.</p>
<dt><a name="real_name"
>real_name</a></dt>
<dd>
<p><code class="code">string</code> The actual name of the user.
May be blank.</p>
<dt><a name="email"
>email</a></dt>
<dd>
<p><code class="code">string</code> The email address of the user.</p>
<dt><a name="name"
>name</a></dt>
<dd>
<p><code class="code">string</code> The login name of the user.
Note that in some situations this is different than their email.</p>
<dt><a name="can_login"
>can_login</a></dt>
<dd>
<p><code class="code">boolean</code> A boolean value to indicate if the user can login into bugzilla.</p>
<dt><a name="email_enabled"
>email_enabled</a></dt>
<dd>
<p><code class="code">boolean</code> A boolean value to indicate if bug-related mail will be sent to the user or not.</p>
<dt><a name="login_denied_text"
>login_denied_text</a></dt>
<dd>
<p><code class="code">string</code> A text field that holds the reason for disabling a user from logging into bugzilla,
if empty then the user account is enabled.
Otherwise it is disabled/closed.</p>
<p><b>Note</b>: If you are not logged in to Bugzilla when you call this function,
you will only be returned the <code class="code">id</code>,
<code class="code">name</code>,
and <code class="code">real_name</code> items.
If you are logged in and not in editusers group,
you will only be returned the <code class="code">id</code>,
<code class="code">name</code>,
<code class="code">real_name</code>,
<code class="code">email</code>,
and <code class="code">can_login</code> items.</p>
</dd>
</dl>
<dt><a name="Errors"
><b>Errors</b></a></dt>
<dd>
<dl>
<dt><a name="51_(Bad_Login_Name)"
>51 (Bad Login Name)</a></dt>
<dd>
<p>You passed an invalid login name in the &#34;names&#34; array.</p>
<dt><a name="304_(Authorization_Required)"
>304 (Authorization Required)</a></dt>
<dd>
<p>You are logged in,
but you are not authorized to see one of the users you wanted to get information about by user id.</p>
<dt><a name="505_(User_Access_By_Id_or_User-Matching_Denied)"
>505 (User Access By Id or User-Matching Denied)</a></dt>
<dd>
<p>Logged-out users cannot use the &#34;ids&#34; or &#34;match&#34; arguments to this function.</p>
</dd>
</dl>
<dt><a name="History"
><b>History</b></a></dt>

View File

@ -0,0 +1,68 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>
Bugzilla::WebService::Util</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet" title="style" type="text/css" href="../.././../../../style.css" media="all" >
</head>
<body id="pod">
<p class="backlinktop"><b><a name="___top" href="../../index.html" accesskey="1" title="All Documents">&lt;&lt;</a></b></p>
<h1></h1>
<div class='indexgroup'>
<ul class='indexList indexList1'>
<li class='indexItem indexItem1'><a href='#NAME'>NAME</a>
<li class='indexItem indexItem1'><a href='#DESCRIPTION'>DESCRIPTION</a>
<li class='indexItem indexItem1'><a href='#SYNOPSIS'>SYNOPSIS</a>
<li class='indexItem indexItem1'><a href='#METHODS'>METHODS</a>
</ul>
</div>
<h1><a class='u' href='#___top' title='click to go to top of document'
name="NAME"
>NAME</a></h1>
<p>Bugzilla::WebService::Util - Utility functions used inside of the WebService code.
These are <b>not</b> functions that can be called via the WebService.</p>
<h1><a class='u' href='#___top' title='click to go to top of document'
name="DESCRIPTION"
>DESCRIPTION</a></h1>
<p>This is somewhat like <a href="../../Bugzilla/Util.html" class="podlinkpod"
>Bugzilla::Util</a>,
but these functions are only used internally in the WebService code.</p>
<h1><a class='u' href='#___top' title='click to go to top of document'
name="SYNOPSIS"
>SYNOPSIS</a></h1>
<pre class="code"> filter({ include_fields =&#62; [&#39;id&#39;, &#39;name&#39;],
exclude_fields =&#62; [&#39;name&#39;] }, $hash);
validate(@_, &#39;ids&#39;);</pre>
<h1><a class='u' href='#___top' title='click to go to top of document'
name="METHODS"
>METHODS</a></h1>
<dl>
<dt><a name="filter_fields"
><code class="code">filter_fields</code></a></dt>
<dd>
<p>This helps implement the <code class="code">include_fields</code> and <code class="code">exclude_fields</code> arguments of WebService methods. Given a hash (the second argument to this subroutine), this will remove any keys that are <i>not</i> in <code class="code">include_fields</code> and then remove any keys that <i>are</i> in <code class="code">exclude_fields</code>.</p>
<dt><a name="validate"
><code class="code">validate</code></a></dt>
<dd>
<p>This helps in the validation of parameters passed into the WebSerice methods. Currently it converts listed parameters into an array reference if the client only passed a single scalar value. It modifies the parameters hash in place so other parameters should be unaltered.</p>
</dd>
</dl>
<p class="backlinkbottom"><b><a name="___bottom" href="../../index.html" title="All Documents">&lt;&lt;</a></b></p>
<!-- end doc -->
</body></html>

View File

@ -2,13 +2,13 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Bugzilla 3.2.4 API Documentation</title>
<title>Bugzilla 3.4 API Documentation</title>
<link rel="stylesheet" title="style" type="text/css" href="./../../../style.css" media="all" >
</head>
<body class="contentspage">
<h1>Bugzilla 3.2.4 API Documentation</h1>
<h1>Bugzilla 3.4 API Documentation</h1>
<dl class='superindex'>
<dt><a name="Files">Files</a></dt>
<dd>
@ -46,6 +46,10 @@
<td>Installs or upgrades modules from CPAN. This script does not run on Windows.</td>
</tr>
<tr class="even">
<th><a href="./jobqueue.html">jobqueue</a></th>
<td>Runs jobs in the background for Bugzilla.</td>
</tr>
<tr class="odd">
<th><a href="./sanitycheck.html">sanitycheck</a></th>
<td>Perl script to perform a sanity check at the command line</td>
</tr>
@ -60,7 +64,7 @@
</tr>
<tr class="odd">
<th><a href="./Bugzilla/Attachment.html">Bugzilla::Attachment</a></th>
<td>a file related to a bug that a user has uploaded to the Bugzilla server</td>
<td>Bugzilla attachment class.</td>
</tr>
<tr class="even">
<th><a href="./Bugzilla/Auth.html">Bugzilla::Auth</a></th>
@ -123,117 +127,133 @@
<td>a particular piece of information about bugs and useful routines for form field manipulation</td>
</tr>
<tr class="odd">
<th><a href="./Bugzilla/Field/Choice.html">Bugzilla::Field::Choice</a></th>
<td>A legal value for a &#60;select&#62;-type field.</td>
</tr>
<tr class="even">
<th><a href="./Bugzilla/Flag.html">Bugzilla::Flag</a></th>
<td>A module to deal with Bugzilla flag values.</td>
</tr>
<tr class="even">
<tr class="odd">
<th><a href="./Bugzilla/FlagType.html">Bugzilla::FlagType</a></th>
<td>A module to deal with Bugzilla flag types.</td>
</tr>
<tr class="odd">
<tr class="even">
<th><a href="./Bugzilla/Group.html">Bugzilla::Group</a></th>
<td>Bugzilla group class.</td>
</tr>
<tr class="even">
<tr class="odd">
<th><a href="./Bugzilla/Hook.html">Bugzilla::Hook</a></th>
<td>Extendable extension hooks for Bugzilla code</td>
</tr>
<tr class="odd">
<tr class="even">
<th><a href="./Bugzilla/Install.html">Bugzilla::Install</a></th>
<td>Functions and variables having to do with installation.</td>
</tr>
<tr class="even">
<tr class="odd">
<th><a href="./Bugzilla/Install/CPAN.html">Bugzilla::Install::CPAN</a></th>
<td>Routines to install Perl modules from CPAN.</td>
</tr>
<tr class="odd">
<tr class="even">
<th><a href="./Bugzilla/Install/DB.html">Bugzilla::Install::DB</a></th>
<td>Fix up the database during installation.</td>
</tr>
<tr class="even">
<tr class="odd">
<th><a href="./Bugzilla/Install/Filesystem.html">Bugzilla::Install::Filesystem</a></th>
<td>Fix up the filesystem during installation.</td>
</tr>
<tr class="odd">
<tr class="even">
<th><a href="./Bugzilla/Install/Localconfig.html">Bugzilla::Install::Localconfig</a></th>
<td></td>
</tr>
<tr class="even">
<tr class="odd">
<th><a href="./Bugzilla/Install/Requirements.html">Bugzilla::Install::Requirements</a></th>
<td>Functions and variables dealing with Bugzilla&#39;s perl-module requirements.</td>
</tr>
<tr class="odd">
<tr class="even">
<th><a href="./Bugzilla/Install/Util.html">Bugzilla::Install::Util</a></th>
<td>Utility functions that are useful both during installation and afterwards.</td>
</tr>
<tr class="odd">
<th><a href="./Bugzilla/JobQueue.html">Bugzilla::JobQueue</a></th>
<td>Interface between Bugzilla and TheSchwartz.</td>
</tr>
<tr class="even">
<th><a href="./Bugzilla/JobQueue/Runner.html">Bugzilla::JobQueue::Runner</a></th>
<td>A class representing the daemon that runs the job queue.</td>
</tr>
<tr class="odd">
<th><a href="./Bugzilla/Keyword.html">Bugzilla::Keyword</a></th>
<td>A Keyword that can be added to a bug.</td>
</tr>
<tr class="odd">
<tr class="even">
<th><a href="./Bugzilla/Milestone.html">Bugzilla::Milestone</a></th>
<td>Bugzilla product milestone class.</td>
</tr>
<tr class="even">
<tr class="odd">
<th><a href="./Bugzilla/Object.html">Bugzilla::Object</a></th>
<td>A base class for objects in Bugzilla.</td>
</tr>
<tr class="odd">
<tr class="even">
<th><a href="./Bugzilla/Product.html">Bugzilla::Product</a></th>
<td>Bugzilla product class.</td>
</tr>
<tr class="even">
<tr class="odd">
<th><a href="./Bugzilla/Search/Saved.html">Bugzilla::Search::Saved</a></th>
<td>A saved search</td>
</tr>
<tr class="odd">
<tr class="even">
<th><a href="./Bugzilla/Status.html">Bugzilla::Status</a></th>
<td>Bug status class.</td>
</tr>
<tr class="even">
<tr class="odd">
<th><a href="./Bugzilla/Template.html">Bugzilla::Template</a></th>
<td>Wrapper around the Template Toolkit Template object</td>
</tr>
<tr class="odd">
<tr class="even">
<th><a href="./Bugzilla/Template/Parser.html">Bugzilla::Template::Parser</a></th>
<td>Wrapper around the Template Toolkit Template::Parser object</td>
</tr>
<tr class="even">
<tr class="odd">
<th><a href="./Bugzilla/Template/Plugin/Bugzilla.html">Bugzilla::Template::Plugin::Bugzilla</a></th>
<td></td>
</tr>
<tr class="odd">
<tr class="even">
<th><a href="./Bugzilla/Template/Plugin/Hook.html">Bugzilla::Template::Plugin::Hook</a></th>
<td></td>
</tr>
<tr class="even">
<tr class="odd">
<th><a href="./Bugzilla/Template/Plugin/User.html">Bugzilla::Template::Plugin::User</a></th>
<td></td>
</tr>
<tr class="odd">
<tr class="even">
<th><a href="./Bugzilla/Token.html">Bugzilla::Token</a></th>
<td>Provides different routines to manage tokens.</td>
</tr>
<tr class="even">
<tr class="odd">
<th><a href="./Bugzilla/Update.html">Bugzilla::Update</a></th>
<td>Update routines for Bugzilla</td>
</tr>
<tr class="odd">
<tr class="even">
<th><a href="./Bugzilla/User.html">Bugzilla::User</a></th>
<td>Object for a Bugzilla user</td>
</tr>
<tr class="even">
<tr class="odd">
<th><a href="./Bugzilla/User/Setting.html">Bugzilla::User::Setting</a></th>
<td>Object for a user preference setting</td>
</tr>
<tr class="odd">
<tr class="even">
<th><a href="./Bugzilla/User/Setting/Lang.html">Bugzilla::User::Setting::Lang</a></th>
<td>Object for a user preference setting for preferred language</td>
</tr>
<tr class="even">
<tr class="odd">
<th><a href="./Bugzilla/User/Setting/Skin.html">Bugzilla::User::Setting::Skin</a></th>
<td>Object for a user preference setting for skins</td>
</tr>
<tr class="even">
<th><a href="./Bugzilla/User/Setting/Timezone.html">Bugzilla::User::Setting::Timezone</a></th>
<td>Object for a user preference setting for desired timezone</td>
</tr>
<tr class="odd">
<th><a href="./Bugzilla/Util.html">Bugzilla::Util</a></th>
<td>Generic utility functions for bugzilla</td>
@ -262,6 +282,10 @@
<th><a href="./Bugzilla/WebService/User.html">Bugzilla::WebService::User</a></th>
<td>The User Account and Login API</td>
</tr>
<tr class="even">
<th><a href="./Bugzilla/WebService/Util.html">Bugzilla::WebService::Util</a></th>
<td></td>
</tr>
</table></dd>
</dl>

Some files were not shown because too many files have changed in this diff Show More