Bug 40933 - НЕ ДО КОНЦА оттестированная объединённая версия Bugzilla 3.6 - НИКУДА НЕ РАЗВОРАЧИВАТЬ! :)
git-svn-id: svn://svn.office.custis.ru/3rdparty/bugzilla.org/trunk@738 6955db30-a419-402b-8a0d-67ecbb4d7f56master
parent
0804786698
commit
d2a48e6dc8
196
Bugzilla.pm
196
Bugzilla.pm
|
@ -32,9 +32,11 @@ use Bugzilla::Constants;
|
|||
use Bugzilla::Auth;
|
||||
use Bugzilla::Auth::Persist::Cookie;
|
||||
use Bugzilla::CGI;
|
||||
use Bugzilla::Extension;
|
||||
use Bugzilla::DB;
|
||||
use Bugzilla::Install::Localconfig qw(read_localconfig);
|
||||
use Bugzilla::JobQueue;
|
||||
use Bugzilla::Install::Requirements qw(OPTIONAL_MODULES);
|
||||
use Bugzilla::Install::Util;
|
||||
use Bugzilla::Template;
|
||||
use Bugzilla::User;
|
||||
use Bugzilla::Error;
|
||||
|
@ -78,9 +80,6 @@ BEGIN
|
|||
};
|
||||
}
|
||||
|
||||
# This creates the request cache for non-mod_perl installations.
|
||||
our $_request_cache = {};
|
||||
|
||||
#####################################################################
|
||||
# Constants
|
||||
#####################################################################
|
||||
|
@ -89,6 +88,7 @@ our $_request_cache = {};
|
|||
use constant SHUTDOWNHTML_EXEMPT => [
|
||||
'editparams.cgi',
|
||||
'checksetup.pl',
|
||||
'migrate.pl',
|
||||
'recode.pl',
|
||||
];
|
||||
|
||||
|
@ -119,7 +119,11 @@ my $re_encoded_word = qr{
|
|||
my $re_especials = qr{$re_encoded_word}xo;
|
||||
# >>>
|
||||
|
||||
*Encode::MIME::Header::encode = sub($$;$) {
|
||||
undef &Encode::MIME::Header::encode;
|
||||
|
||||
*Encode::MIME::Header::encode = *encode_mime_header;
|
||||
|
||||
sub encode_mime_header($$;$) {
|
||||
my ( $obj, $str, $chk ) = @_;
|
||||
my @line = ();
|
||||
for my $line ( split /\r\n|[\r\n]/o, $str ) {
|
||||
|
@ -151,6 +155,7 @@ my $re_especials = qr{$re_encoded_word}xo;
|
|||
$_[1] = '' if $chk;
|
||||
return join( "\n", @line );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#####################################################################
|
||||
|
@ -163,15 +168,20 @@ my $re_especials = qr{$re_encoded_word}xo;
|
|||
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'} = '';
|
||||
# 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'} = '';
|
||||
}
|
||||
|
||||
# Because this function is run live from perl "use" commands of
|
||||
# other scripts, we're skipping the rest of this function if we get here
|
||||
# during a perl syntax check (perl -c, like we do during the
|
||||
# 001compile.t test).
|
||||
return if $^C;
|
||||
|
||||
# IIS prints out warnings to the webpage, so ignore them, or log them
|
||||
# to a file if the file exists.
|
||||
if ($ENV{SERVER_SOFTWARE} && $ENV{SERVER_SOFTWARE} =~ /microsoft-iis/i) {
|
||||
|
@ -186,18 +196,18 @@ sub init_page {
|
|||
};
|
||||
}
|
||||
|
||||
# Because of attachment_base, attachment.cgi handles this itself.
|
||||
if (basename($0) ne 'attachment.cgi') {
|
||||
do_ssl_redirect_if_required();
|
||||
}
|
||||
|
||||
# If Bugzilla is shut down, do not allow anything to run, just display a
|
||||
# message to the user about the downtime and log out. Scripts listed in
|
||||
# SHUTDOWNHTML_EXEMPT are exempt from this message.
|
||||
#
|
||||
# Because this is code which is run live from perl "use" commands of other
|
||||
# scripts, we're skipping this part if we get here during a perl syntax
|
||||
# check -- runtests.pl compiles scripts without running them, so we
|
||||
# need to make sure that this check doesn't apply to 'perl -c' calls.
|
||||
#
|
||||
# This code must go here. It cannot go anywhere in Bugzilla::CGI, because
|
||||
# it uses Template, and that causes various dependency loops.
|
||||
if (!$^C && Bugzilla->params->{"shutdownhtml"}
|
||||
if (Bugzilla->params->{"shutdownhtml"}
|
||||
&& lsearch(SHUTDOWNHTML_EXEMPT, basename($0)) == -1)
|
||||
{
|
||||
# Allow non-cgi scripts to exit silently (without displaying any
|
||||
|
@ -244,8 +254,6 @@ sub init_page {
|
|||
}
|
||||
}
|
||||
|
||||
init_page() if !$ENV{MOD_PERL};
|
||||
|
||||
#####################################################################
|
||||
# Subroutines and Methods
|
||||
#####################################################################
|
||||
|
@ -266,12 +274,83 @@ sub template_inner {
|
|||
return $class->request_cache->{"template_inner_$lang"};
|
||||
}
|
||||
|
||||
our $extension_packages;
|
||||
sub extensions {
|
||||
my ($class) = @_;
|
||||
my $cache = $class->request_cache;
|
||||
if (!$cache->{extensions}) {
|
||||
# Under mod_perl, mod_perl.pl populates $extension_packages for us.
|
||||
if (!$extension_packages) {
|
||||
$extension_packages = Bugzilla::Extension->load_all();
|
||||
}
|
||||
my @extensions;
|
||||
foreach my $package (@$extension_packages) {
|
||||
my $extension = $package->new();
|
||||
if ($extension->enabled) {
|
||||
push(@extensions, $extension);
|
||||
}
|
||||
}
|
||||
$cache->{extensions} = \@extensions;
|
||||
}
|
||||
return $cache->{extensions};
|
||||
}
|
||||
|
||||
sub feature {
|
||||
my ($class, $feature) = @_;
|
||||
my $cache = $class->request_cache;
|
||||
return $cache->{feature}->{$feature}
|
||||
if exists $cache->{feature}->{$feature};
|
||||
|
||||
my $feature_map = $cache->{feature_map};
|
||||
if (!$feature_map) {
|
||||
foreach my $package (@{ OPTIONAL_MODULES() }) {
|
||||
foreach my $f (@{ $package->{feature} }) {
|
||||
$feature_map->{$f} ||= [];
|
||||
push(@{ $feature_map->{$f} }, $package->{module});
|
||||
}
|
||||
}
|
||||
$cache->{feature_map} = $feature_map;
|
||||
}
|
||||
|
||||
if (!$feature_map->{$feature}) {
|
||||
ThrowCodeError('invalid_feature', { feature => $feature });
|
||||
}
|
||||
|
||||
my $success = 1;
|
||||
foreach my $module (@{ $feature_map->{$feature} }) {
|
||||
# We can't use a string eval and "use" here (it kills Template-Toolkit,
|
||||
# see https://rt.cpan.org/Public/Bug/Display.html?id=47929), so we have
|
||||
# to do a block eval.
|
||||
$module =~ s{::}{/}g;
|
||||
$module .= ".pm";
|
||||
eval { require $module; 1; } or $success = 0;
|
||||
}
|
||||
$cache->{feature}->{$feature} = $success;
|
||||
return $success;
|
||||
}
|
||||
|
||||
sub cgi {
|
||||
my $class = shift;
|
||||
$class->request_cache->{cgi} ||= new Bugzilla::CGI();
|
||||
return $class->request_cache->{cgi};
|
||||
}
|
||||
|
||||
sub input_params {
|
||||
my ($class, $params) = @_;
|
||||
my $cache = $class->request_cache;
|
||||
# This is how the WebService and other places set input_params.
|
||||
if (defined $params) {
|
||||
$cache->{input_params} = $params;
|
||||
}
|
||||
return $cache->{input_params} if defined $cache->{input_params};
|
||||
|
||||
# Making this scalar makes it a tied hash to the internals of $cgi,
|
||||
# so if a variable is changed, then it actually changes the $cgi object
|
||||
# as well.
|
||||
$cache->{input_params} = $class->cgi->Vars;
|
||||
return $cache->{input_params};
|
||||
}
|
||||
|
||||
sub localconfig {
|
||||
my $class = shift;
|
||||
$class->request_cache->{localconfig} ||= read_localconfig();
|
||||
|
@ -318,6 +397,7 @@ sub login {
|
|||
|
||||
my $authorizer = new Bugzilla::Auth();
|
||||
$type = LOGIN_REQUIRED if $class->cgi->param('GoAheadAndLogIn');
|
||||
|
||||
if (!defined $type || $type == LOGIN_NORMAL) {
|
||||
$type = $class->params->{'requirelogin'} ? LOGIN_REQUIRED : LOGIN_NORMAL;
|
||||
}
|
||||
|
@ -361,14 +441,6 @@ sub login {
|
|||
$class->set_user($authenticated_user);
|
||||
}
|
||||
|
||||
# We run after the login has completed since
|
||||
# some of the checks in ssl_require_redirect
|
||||
# look for Bugzilla->user->id to determine
|
||||
# if redirection is required.
|
||||
if (i_am_cgi() && ssl_require_redirect()) {
|
||||
$class->cgi->require_https($class->params->{'sslbase'});
|
||||
}
|
||||
|
||||
return $class->user;
|
||||
}
|
||||
|
||||
|
@ -408,6 +480,7 @@ sub logout_request {
|
|||
|
||||
sub job_queue {
|
||||
my $class = shift;
|
||||
require Bugzilla::JobQueue;
|
||||
$class->request_cache->{job_queue} ||= Bugzilla::JobQueue->new();
|
||||
return $class->request_cache->{job_queue};
|
||||
}
|
||||
|
@ -455,6 +528,15 @@ sub error_mode {
|
|||
|| (i_am_cgi() ? ERROR_MODE_WEBPAGE : ERROR_MODE_DIE);
|
||||
}
|
||||
|
||||
# This is used only by Bugzilla::Error to throw errors.
|
||||
sub _json_server {
|
||||
my ($class, $newval) = @_;
|
||||
if (defined $newval) {
|
||||
$class->request_cache->{_json_server} = $newval;
|
||||
}
|
||||
return $class->request_cache->{_json_server};
|
||||
}
|
||||
|
||||
sub usage_mode {
|
||||
my ($class, $newval) = @_;
|
||||
if (defined $newval) {
|
||||
|
@ -464,9 +546,12 @@ sub usage_mode {
|
|||
elsif ($newval == USAGE_MODE_CMDLINE) {
|
||||
$class->error_mode(ERROR_MODE_DIE);
|
||||
}
|
||||
elsif ($newval == USAGE_MODE_WEBSERVICE) {
|
||||
elsif ($newval == USAGE_MODE_XMLRPC) {
|
||||
$class->error_mode(ERROR_MODE_DIE_SOAP_FAULT);
|
||||
}
|
||||
elsif ($newval == USAGE_MODE_JSON) {
|
||||
$class->error_mode(ERROR_MODE_JSON_RPC);
|
||||
}
|
||||
elsif ($newval == USAGE_MODE_EMAIL) {
|
||||
$class->error_mode(ERROR_MODE_DIE);
|
||||
}
|
||||
|
@ -541,7 +626,7 @@ sub active_custom_fields {
|
|||
my $class = shift;
|
||||
if (!exists $class->request_cache->{active_custom_fields}) {
|
||||
$class->request_cache->{active_custom_fields} =
|
||||
Bugzilla::Field->match({ custom => 1, obsolete => 0 });
|
||||
Bugzilla::Field->match({ custom => 1, obsolete => 0 });
|
||||
}
|
||||
return @{$class->request_cache->{active_custom_fields}};
|
||||
}
|
||||
|
@ -555,12 +640,6 @@ sub has_flags {
|
|||
return $class->request_cache->{has_flags};
|
||||
}
|
||||
|
||||
sub hook_args {
|
||||
my ($class, $args) = @_;
|
||||
$class->request_cache->{hook_args} = $args if $args;
|
||||
return $class->request_cache->{hook_args};
|
||||
}
|
||||
|
||||
sub local_timezone {
|
||||
my $class = shift;
|
||||
|
||||
|
@ -571,10 +650,20 @@ sub local_timezone {
|
|||
return $class->request_cache->{local_timezone};
|
||||
}
|
||||
|
||||
# This creates the request cache for non-mod_perl installations.
|
||||
# This is identical to Install::Util::_cache so that things loaded
|
||||
# into Install::Util::_cache during installation can be read out
|
||||
# of request_cache later in installation.
|
||||
our $_request_cache = $Bugzilla::Install::Util::_cache;
|
||||
|
||||
sub request_cache {
|
||||
if ($ENV{MOD_PERL}) {
|
||||
require Apache2::RequestUtil;
|
||||
return Apache2::RequestUtil->request->pnotes();
|
||||
# Sometimes (for example, during mod_perl.pl), the request
|
||||
# object isn't available, and we should use $_request_cache instead.
|
||||
my $request = eval { Apache2::RequestUtil->request };
|
||||
return $_request_cache if !$request;
|
||||
return $request->pnotes();
|
||||
}
|
||||
return $_request_cache ||= {};
|
||||
}
|
||||
|
@ -599,6 +688,8 @@ sub END {
|
|||
_cleanup() unless $ENV{MOD_PERL};
|
||||
}
|
||||
|
||||
init_page() if !$ENV{MOD_PERL};
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
@ -682,6 +773,26 @@ The current C<cgi> object. Note that modules should B<not> be using this in
|
|||
general. Not all Bugzilla actions are cgi requests. Its useful as a convenience
|
||||
method for those scripts/templates which are only use via CGI, though.
|
||||
|
||||
=item C<input_params>
|
||||
|
||||
When running under the WebService, this is a hashref containing the arguments
|
||||
passed to the WebService method that was called. When running in a normal
|
||||
script, this is a hashref containing the contents of the CGI parameters.
|
||||
|
||||
Modifying this hashref will modify the CGI parameters or the WebService
|
||||
arguments (depending on what C<input_params> currently represents).
|
||||
|
||||
This should be used instead of L</cgi> in situations where your code
|
||||
could be being called by either a normal CGI script or a WebService method,
|
||||
such as during a code hook.
|
||||
|
||||
B<Note:> When C<input_params> represents the CGI parameters, any
|
||||
parameter specified more than once (like C<foo=bar&foo=baz>) will appear
|
||||
as an arrayref in the hash, but any value specified only once will appear
|
||||
as a scalar. This means that even if a value I<can> appear multiple times,
|
||||
if it only I<does> appear once, then it will be a scalar in C<input_params>,
|
||||
not an arrayref.
|
||||
|
||||
=item C<user>
|
||||
|
||||
C<undef> if there is no currently logged in user or if the login code has not
|
||||
|
@ -766,10 +877,11 @@ usage mode changes.
|
|||
=item C<usage_mode>
|
||||
|
||||
Call either C<Bugzilla->usage_mode(Bugzilla::Constants::USAGE_MODE_CMDLINE)>
|
||||
or C<Bugzilla->usage_mode(Bugzilla::Constants::USAGE_MODE_WEBSERVICE)> near the
|
||||
or C<Bugzilla->usage_mode(Bugzilla::Constants::USAGE_MODE_XMLRPC)> near the
|
||||
beginning of your script to change this flag's default of
|
||||
C<Bugzilla::Constants::USAGE_MODE_BROWSER> and to indicate that Bugzilla is
|
||||
being called in a non-interactive manner.
|
||||
|
||||
This influences error handling because on usage mode changes, C<usage_mode>
|
||||
calls C<Bugzilla->error_mode> to set an error mode which makes sense for the
|
||||
usage mode.
|
||||
|
@ -813,11 +925,6 @@ The current Parameters of Bugzilla, as a hashref. If C<data/params>
|
|||
does not exist, then we return an empty hashref. If C<data/params>
|
||||
is unreadable or is not valid perl, we C<die>.
|
||||
|
||||
=item C<hook_args>
|
||||
|
||||
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,
|
||||
|
@ -830,4 +937,9 @@ 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.
|
||||
|
||||
=item C<feature>
|
||||
|
||||
Tells you whether or not a specific feature is enabled. For names
|
||||
of features, see C<OPTIONAL_MODULES> in C<Bugzilla::Install::Requirements>.
|
||||
|
||||
=back
|
||||
|
|
|
@ -57,12 +57,12 @@ use Bugzilla::Flag;
|
|||
use Bugzilla::User;
|
||||
use Bugzilla::Util;
|
||||
use Bugzilla::Field;
|
||||
use Bugzilla::Hook;
|
||||
|
||||
use LWP::MediaTypes;
|
||||
|
||||
use base qw(Bugzilla::Object);
|
||||
|
||||
use Encode;
|
||||
|
||||
###############################
|
||||
#### Initialization ####
|
||||
###############################
|
||||
|
@ -89,6 +89,38 @@ sub DB_COLUMNS {
|
|||
$dbh->sql_date_format('attachments.creation_ts', '%Y.%m.%d %H:%i') . ' AS creation_ts';
|
||||
}
|
||||
|
||||
use constant REQUIRED_CREATE_FIELDS => qw(
|
||||
bug
|
||||
data
|
||||
description
|
||||
filename
|
||||
mimetype
|
||||
);
|
||||
|
||||
use constant UPDATE_COLUMNS => qw(
|
||||
description
|
||||
filename
|
||||
isobsolete
|
||||
ispatch
|
||||
isprivate
|
||||
mimetype
|
||||
);
|
||||
|
||||
use constant VALIDATORS => {
|
||||
bug => \&_check_bug,
|
||||
description => \&_check_description,
|
||||
ispatch => \&Bugzilla::Object::check_boolean,
|
||||
isprivate => \&_check_is_private,
|
||||
isurl => \&_check_is_url,
|
||||
mimetype => \&_check_content_type,
|
||||
store_in_file => \&_check_store_in_file,
|
||||
};
|
||||
|
||||
use constant UPDATE_VALIDATORS => {
|
||||
filename => \&_check_filename,
|
||||
isobsolete => \&Bugzilla::Object::check_boolean,
|
||||
};
|
||||
|
||||
###############################
|
||||
#### Accessors ######
|
||||
###############################
|
||||
|
@ -126,7 +158,7 @@ sub bug {
|
|||
my $self = shift;
|
||||
|
||||
require Bugzilla::Bug;
|
||||
$self->{bug} = Bugzilla::Bug->new($self->bug_id);
|
||||
$self->{bug} ||= Bugzilla::Bug->new($self->bug_id);
|
||||
return $self->{bug};
|
||||
}
|
||||
|
||||
|
@ -396,6 +428,13 @@ sub datasize {
|
|||
return $self->{datasize};
|
||||
}
|
||||
|
||||
sub _get_local_filename {
|
||||
my $self = shift;
|
||||
my $hash = ($self->id % 100) + 100;
|
||||
$hash =~ s/.*(\d\d)$/group.$1/;
|
||||
return bz_locations()->{'attachdir'} . "/$hash/attachment." . $self->id;
|
||||
}
|
||||
|
||||
=over
|
||||
|
||||
=item C<flags>
|
||||
|
@ -408,9 +447,9 @@ flags that have been set on the attachment
|
|||
|
||||
sub flags {
|
||||
my $self = shift;
|
||||
return $self->{flags} if exists $self->{flags};
|
||||
|
||||
$self->{flags} = Bugzilla::Flag->match({ 'attach_id' => $self->id });
|
||||
# Don't cache it as it must be in sync with ->flag_types.
|
||||
$self->{flags} = [map { @{$_->{flags}} } @{$self->flag_types}];
|
||||
return $self->{flags};
|
||||
}
|
||||
|
||||
|
@ -434,7 +473,7 @@ sub flag_types {
|
|||
component_id => $self->bug->component_id,
|
||||
attach_id => $self->id };
|
||||
|
||||
$self->{flag_types} = Bugzilla::Flag::_flag_types($vars);
|
||||
$self->{flag_types} = Bugzilla::Flag->_flag_types($vars);
|
||||
return $self->{flag_types};
|
||||
}
|
||||
|
||||
|
@ -442,33 +481,136 @@ sub flag_types {
|
|||
#### Validators ######
|
||||
###############################
|
||||
|
||||
# Instance methods; no POD documentation here yet because the only ones so far
|
||||
# are private.
|
||||
sub set_content_type { $_[0]->set('mimetype', $_[1]); }
|
||||
sub set_description { $_[0]->set('description', $_[1]); }
|
||||
sub set_filename { $_[0]->set('filename', $_[1]); }
|
||||
sub set_is_patch { $_[0]->set('ispatch', $_[1]); }
|
||||
sub set_is_private { $_[0]->set('isprivate', $_[1]); }
|
||||
|
||||
sub _get_local_filename {
|
||||
my $self = shift;
|
||||
my $hash = ($self->id % 100) + 100;
|
||||
$hash =~ s/.*(\d\d)$/group.$1/;
|
||||
return bz_locations()->{'attachdir'} . "/$hash/attachment." . $self->id;
|
||||
sub set_is_obsolete {
|
||||
my ($self, $obsolete) = @_;
|
||||
|
||||
my $old = $self->isobsolete;
|
||||
$self->set('isobsolete', $obsolete);
|
||||
my $new = $self->isobsolete;
|
||||
|
||||
# If the attachment is being marked as obsolete, cancel pending requests.
|
||||
if ($new && $old != $new) {
|
||||
my @requests = grep { $_->status eq '?' } @{$self->flags};
|
||||
return unless scalar @requests;
|
||||
|
||||
my %flag_ids = map { $_->id => 1 } @requests;
|
||||
foreach my $flagtype (@{$self->flag_types}) {
|
||||
@{$flagtype->{flags}} = grep { !$flag_ids{$_->id} } @{$flagtype->{flags}};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sub _validate_filename {
|
||||
my ($throw_error) = @_;
|
||||
my $cgi = Bugzilla->cgi;
|
||||
defined $cgi->upload('data')
|
||||
|| ($cgi->param('text_attachment') !~ /^\s*$/so)
|
||||
|| ($throw_error ? ThrowUserError("file_not_specified") : return 0);
|
||||
sub set_flags {
|
||||
my ($self, $flags, $new_flags) = @_;
|
||||
|
||||
my $filename = $cgi->upload('data') || $cgi->param('filename');
|
||||
$filename = $cgi->param('description')
|
||||
if !$filename && $cgi->param('text_attachment') !~ /^\s*$/so;
|
||||
if (Bugzilla->params->{utf8})
|
||||
{
|
||||
# CGI::upload() will probably return non-UTF8 string, so set UTF8 flag on
|
||||
# utf8::decode() or Encode::_utf8_on() does not work on tainted values...
|
||||
$filename = trick_taint_copy($filename);
|
||||
Encode::_utf8_on($filename);
|
||||
Bugzilla::Flag->set_flag($self, $_) foreach (@$flags, @$new_flags);
|
||||
}
|
||||
|
||||
sub _check_bug {
|
||||
my ($invocant, $bug) = @_;
|
||||
my $user = Bugzilla->user;
|
||||
|
||||
$bug = ref $invocant ? $invocant->bug : $bug;
|
||||
($user->can_see_bug($bug->id) && $user->can_edit_product($bug->product_id))
|
||||
|| ThrowUserError("illegal_attachment_edit_bug", { bug_id => $bug->id });
|
||||
|
||||
return $bug;
|
||||
}
|
||||
|
||||
sub _legal_content_type {
|
||||
my ($content_type) = @_;
|
||||
my $legal_types = join('|', LEGAL_CONTENT_TYPES);
|
||||
return $content_type !~ /^($legal_types)\/.+$/;
|
||||
}
|
||||
|
||||
sub _check_content_type {
|
||||
my ($invocant, $content_type) = @_;
|
||||
|
||||
$content_type = 'text/plain' if (ref $invocant && ($invocant->isurl || $invocant->ispatch));
|
||||
if (!_legal_content_type($content_type)) {
|
||||
ThrowUserError("invalid_content_type", { contenttype => $content_type });
|
||||
}
|
||||
trick_taint($content_type);
|
||||
|
||||
return $content_type;
|
||||
}
|
||||
|
||||
sub _check_data {
|
||||
my ($invocant, $params) = @_;
|
||||
|
||||
my $data;
|
||||
if ($params->{isurl}) {
|
||||
$data = $params->{data};
|
||||
($data && $data =~ m#^(http|https|ftp)://\S+#)
|
||||
|| ThrowUserError('attachment_illegal_url', { url => $data });
|
||||
|
||||
$params->{mimetype} = 'text/plain';
|
||||
$params->{ispatch} = 0;
|
||||
$params->{store_in_file} = 0;
|
||||
}
|
||||
else {
|
||||
if ($params->{store_in_file} || !ref $params->{data}) {
|
||||
# If it's a filehandle, just store it, not the content of the file
|
||||
# itself as the file may be quite large. If it's not a filehandle,
|
||||
# it already contains the content of the file.
|
||||
$data = $params->{data};
|
||||
}
|
||||
else {
|
||||
# The file will be stored in the DB. We need the content of the file.
|
||||
local $/;
|
||||
my $fh = $params->{data};
|
||||
$data = <$fh>;
|
||||
}
|
||||
}
|
||||
Bugzilla::Hook::process('attachment_process_data', { data => \$data,
|
||||
attributes => $params });
|
||||
|
||||
# Do not validate the size if we have a filehandle. It will be checked later.
|
||||
return $data if ref $data;
|
||||
|
||||
$data || ThrowUserError('zero_length_file');
|
||||
# Make sure the attachment does not exceed the maximum permitted size.
|
||||
my $len = length($data);
|
||||
my $max_size = $params->{store_in_file} ? Bugzilla->params->{'maxlocalattachment'} * 1048576
|
||||
: Bugzilla->params->{'maxattachmentsize'} * 1024;
|
||||
if ($len > $max_size) {
|
||||
my $vars = { filesize => sprintf("%.0f", $len/1024) };
|
||||
if ($params->{ispatch}) {
|
||||
ThrowUserError('patch_too_large', $vars);
|
||||
}
|
||||
elsif ($params->{store_in_file}) {
|
||||
ThrowUserError('local_file_too_large');
|
||||
}
|
||||
else {
|
||||
ThrowUserError('file_too_large', $vars);
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
sub _check_description {
|
||||
my ($invocant, $description) = @_;
|
||||
|
||||
$description = trim($description);
|
||||
$description || ThrowUserError('missing_attachment_description');
|
||||
return $description;
|
||||
}
|
||||
|
||||
sub _check_filename {
|
||||
my ($invocant, $filename, $is_url) = @_;
|
||||
|
||||
$is_url = $invocant->isurl if ref $invocant;
|
||||
# No file is attached, so it has no name.
|
||||
return '' if $is_url;
|
||||
|
||||
$filename = trim($filename);
|
||||
$filename || ThrowUserError('file_not_specified');
|
||||
|
||||
# Remove path info (if any) from the file name. The browser should do this
|
||||
# for us, but some are buggy. This may not work on Mac file names and could
|
||||
|
@ -480,70 +622,39 @@ sub _validate_filename {
|
|||
# Truncate the filename to 100 characters, counting from the end of the
|
||||
# string to make sure we keep the filename extension.
|
||||
$filename = substr($filename, -100, 100);
|
||||
trick_taint($filename);
|
||||
|
||||
return $filename;
|
||||
}
|
||||
|
||||
sub _validate_data {
|
||||
my ($throw_error, $hr_vars) = @_;
|
||||
my $cgi = Bugzilla->cgi;
|
||||
sub _check_is_private {
|
||||
my ($invocant, $is_private) = @_;
|
||||
|
||||
my $fh;
|
||||
# Skip uploading into a local variable if the user wants to upload huge
|
||||
# attachments into local files.
|
||||
if (!$cgi->param('bigfile')) {
|
||||
$fh = $cgi->upload('data');
|
||||
$is_private = $is_private ? 1 : 0;
|
||||
if (((!ref $invocant && $is_private)
|
||||
|| (ref $invocant && $invocant->isprivate != $is_private))
|
||||
&& !Bugzilla->user->is_insider) {
|
||||
ThrowUserError('user_not_insider');
|
||||
}
|
||||
my $data;
|
||||
return $is_private;
|
||||
}
|
||||
|
||||
# We could get away with reading only as much as required, except that then
|
||||
# we wouldn't have a size to print to the error handler below.
|
||||
if (!$cgi->param('bigfile')) {
|
||||
# enable 'slurp' mode
|
||||
local $/;
|
||||
$data = <$fh>;
|
||||
sub _check_is_url {
|
||||
my ($invocant, $is_url) = @_;
|
||||
|
||||
if ($is_url && !Bugzilla->params->{'allow_attach_url'}) {
|
||||
ThrowCodeError('attachment_url_disabled');
|
||||
}
|
||||
return $is_url ? 1 : 0;
|
||||
}
|
||||
|
||||
$data
|
||||
|| ($cgi->param('bigfile'))
|
||||
|| ($cgi->param('text_attachment') !~ /^\s*$/so)
|
||||
|| ($throw_error ? ThrowUserError("zero_length_file") : return 0);
|
||||
sub _check_store_in_file {
|
||||
my ($invocant, $store_in_file) = @_;
|
||||
|
||||
if (!$data && $cgi->param('text_attachment') !~ /^\s*$/so)
|
||||
{
|
||||
$data = $cgi->param('text_attachment');
|
||||
if ($store_in_file && !Bugzilla->params->{'maxlocalattachment'}) {
|
||||
ThrowCodeError('attachment_local_storage_disabled');
|
||||
}
|
||||
|
||||
# Windows screenshots are usually uncompressed BMP files which
|
||||
# makes for a quick way to eat up disk space. Let's compress them.
|
||||
# We do this before we check the size since the uncompressed version
|
||||
# could easily be greater than maxattachmentsize.
|
||||
if (Bugzilla->params->{'convert_uncompressed_images'}
|
||||
&& $cgi->param('contenttype') eq 'image/bmp') {
|
||||
require Image::Magick;
|
||||
my $img = Image::Magick->new(magick=>'bmp');
|
||||
$img->BlobToImage($data);
|
||||
$img->set(magick=>'png');
|
||||
my $imgdata = $img->ImageToBlob();
|
||||
$data = $imgdata;
|
||||
$cgi->param('contenttype', 'image/png');
|
||||
$hr_vars->{'convertedbmp'} = 1;
|
||||
}
|
||||
|
||||
# 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) };
|
||||
if ($cgi->param('ispatch')) {
|
||||
$throw_error ? ThrowUserError("patch_too_large", $vars) : return 0;
|
||||
}
|
||||
else {
|
||||
$throw_error ? ThrowUserError("file_too_large", $vars) : return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return $data || '';
|
||||
return $store_in_file ? 1 : 0;
|
||||
}
|
||||
|
||||
=pod
|
||||
|
@ -606,128 +717,6 @@ sub get_attachments_by_bug {
|
|||
|
||||
=pod
|
||||
|
||||
=item C<validate_is_patch()>
|
||||
|
||||
Description: validates the "patch" flag passed in by CGI.
|
||||
|
||||
Returns: 1 on success.
|
||||
|
||||
=cut
|
||||
|
||||
sub validate_is_patch {
|
||||
my ($class, $throw_error) = @_;
|
||||
my $cgi = Bugzilla->cgi;
|
||||
|
||||
# Set the ispatch flag to zero if it is undefined, since the UI uses
|
||||
# an HTML checkbox to represent this flag, and unchecked HTML checkboxes
|
||||
# do not get sent in HTML requests.
|
||||
$cgi->param('ispatch', $cgi->param('ispatch') ? 1 : 0);
|
||||
|
||||
# Set the content type to text/plain if the attachment is a patch.
|
||||
$cgi->param('contenttype', 'text/plain') if $cgi->param('ispatch');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
=pod
|
||||
|
||||
=item C<validate_description()>
|
||||
|
||||
Description: validates the description passed in by CGI.
|
||||
|
||||
Returns: 1 on success.
|
||||
|
||||
=cut
|
||||
|
||||
sub validate_description {
|
||||
my ($class, $throw_error) = @_;
|
||||
my $cgi = Bugzilla->cgi;
|
||||
|
||||
$cgi->param('description')
|
||||
|| ($throw_error ? ThrowUserError("missing_attachment_description") : return 0);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
=pod
|
||||
|
||||
=item C<validate_content_type()>
|
||||
|
||||
Description: validates the content type passed in by CGI.
|
||||
|
||||
Returns: 1 on success.
|
||||
|
||||
=cut
|
||||
|
||||
sub valid_content_type { $_[0] =~ /^(application|audio|image|message|model|multipart|text|video)\/.+$/ }
|
||||
|
||||
my $lwp_read_mime_types;
|
||||
sub validate_content_type {
|
||||
my ($class, $throw_error) = @_;
|
||||
my $cgi = Bugzilla->cgi;
|
||||
|
||||
if (!defined $cgi->param('contenttypemethod')) {
|
||||
$throw_error ? ThrowUserError("missing_content_type_method") : return 0;
|
||||
}
|
||||
elsif ($cgi->param('contenttypemethod') eq 'autodetect') {
|
||||
my $contenttype;
|
||||
if ($cgi->param('data'))
|
||||
{
|
||||
$contenttype = $cgi->uploadInfo($cgi->param('data'))->{'Content-Type'};
|
||||
if (!valid_content_type($contenttype) && Bugzilla->params->{mime_types_file})
|
||||
{
|
||||
if (!$lwp_read_mime_types)
|
||||
{
|
||||
LWP::MediaTypes::read_media_types(Bugzilla->params->{mime_types_file});
|
||||
$lwp_read_mime_types = 1;
|
||||
}
|
||||
my $file = $cgi->param('data');
|
||||
$contenttype = LWP::MediaTypes::guess_media_type("$file");
|
||||
}
|
||||
if (!valid_content_type($contenttype))
|
||||
{
|
||||
$contenttype = 'application/octet-stream';
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$contenttype = 'text/plain';
|
||||
}
|
||||
# The user asked us to auto-detect the content type, so use the type
|
||||
# specified in the HTTP request headers.
|
||||
if ( !$contenttype ) {
|
||||
$throw_error ? ThrowUserError("missing_content_type") : return 0;
|
||||
}
|
||||
$cgi->param('contenttype', $contenttype);
|
||||
}
|
||||
elsif ($cgi->param('contenttypemethod') eq 'list') {
|
||||
# The user selected a content type from the list, so use their
|
||||
# selection.
|
||||
$cgi->param('contenttype', $cgi->param('contenttypeselection'));
|
||||
}
|
||||
elsif ($cgi->param('contenttypemethod') eq 'manual') {
|
||||
# The user entered a content type manually, so use their entry.
|
||||
$cgi->param('contenttype', $cgi->param('contenttypeentry'));
|
||||
}
|
||||
else {
|
||||
$throw_error ?
|
||||
ThrowCodeError("illegal_content_type_method",
|
||||
{ contenttypemethod => $cgi->param('contenttypemethod') }) :
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!valid_content_type($cgi->param('contenttype'))) {
|
||||
$throw_error ?
|
||||
ThrowUserError("invalid_content_type",
|
||||
{ contenttype => $cgi->param('contenttype') }) :
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
=pod
|
||||
|
||||
=item C<validate_can_edit($attachment, $product_id)>
|
||||
|
||||
Description: validates if the user is allowed to view and edit the attachment.
|
||||
|
@ -738,7 +727,7 @@ Description: validates if the user is allowed to view and edit the attachment.
|
|||
Params: $attachment - the attachment object being edited.
|
||||
$product_id - the product ID the attachment belongs to.
|
||||
|
||||
Returns: 1 on success. Else an error is thrown.
|
||||
Returns: 1 on success, 0 otherwise.
|
||||
|
||||
=cut
|
||||
|
||||
|
@ -747,12 +736,9 @@ sub validate_can_edit {
|
|||
my $user = Bugzilla->user;
|
||||
|
||||
# The submitter can edit their attachments.
|
||||
return 1 if ($attachment->attacher->id == $user->id
|
||||
|| ((!$attachment->isprivate || $user->is_insider)
|
||||
&& $user->in_group('editbugs', $product_id)));
|
||||
|
||||
# If we come here, then this attachment cannot be seen by the user.
|
||||
ThrowUserError('illegal_attachment_edit', { attach_id => $attachment->id });
|
||||
return ($attachment->attacher->id == $user->id
|
||||
|| ((!$attachment->isprivate || $user->is_insider)
|
||||
&& $user->in_group('editbugs', $product_id))) ? 1 : 0;
|
||||
}
|
||||
|
||||
=item C<validate_obsolete($bug)>
|
||||
|
@ -769,14 +755,13 @@ Returns: 1 on success. Else an error is thrown.
|
|||
=cut
|
||||
|
||||
sub validate_obsolete {
|
||||
my ($class, $bug) = @_;
|
||||
my $cgi = Bugzilla->cgi;
|
||||
my ($class, $bug, $list) = @_;
|
||||
|
||||
# Make sure the attachment id is valid and the user has permissions to view
|
||||
# the bug to which it is attached. Make sure also that the user can view
|
||||
# the attachment itself.
|
||||
my @obsolete_attachments;
|
||||
foreach my $attachid ($cgi->param('obsolete')) {
|
||||
foreach my $attachid (@$list) {
|
||||
my $vars = {};
|
||||
$vars->{'attach_id'} = $attachid;
|
||||
|
||||
|
@ -788,7 +773,8 @@ sub validate_obsolete {
|
|||
|| ThrowUserError('invalid_attach_id', $vars);
|
||||
|
||||
# Check that the user can view and edit this attachment.
|
||||
$attachment->validate_can_edit($bug->product_id);
|
||||
$attachment->validate_can_edit($bug->product_id)
|
||||
|| ThrowUserError('illegal_attachment_edit', { attach_id => $attachment->id });
|
||||
|
||||
$vars->{'description'} = $attachment->description;
|
||||
|
||||
|
@ -813,144 +799,75 @@ sub validate_obsolete {
|
|||
|
||||
=pod
|
||||
|
||||
=item C<create($throw_error, $bug, $user, $timestamp, $hr_vars)>
|
||||
=item C<create>
|
||||
|
||||
Description: inserts an attachment from CGI input for the given bug.
|
||||
Description: inserts an attachment into the given bug.
|
||||
|
||||
Params: C<$bug> - Bugzilla::Bug object - the bug for which to insert
|
||||
Params: takes a hashref with the following keys:
|
||||
C<bug> - Bugzilla::Bug object - the bug for which to insert
|
||||
the attachment.
|
||||
C<$user> - Bugzilla::User object - the user we're inserting an
|
||||
attachment for.
|
||||
C<$timestamp> - scalar - timestamp of the insert as returned
|
||||
by SELECT NOW().
|
||||
C<$hr_vars> - hash reference - reference to a hash of template
|
||||
variables.
|
||||
C<data> - Either a filehandle pointing to the content of the
|
||||
attachment, or the content of the attachment itself.
|
||||
C<description> - string - describe what the attachment is about.
|
||||
C<filename> - string - the name of the attachment (used by the
|
||||
browser when downloading it). If the attachment is a URL, this
|
||||
parameter has no effect.
|
||||
C<mimetype> - string - a valid MIME type.
|
||||
C<creation_ts> - string (optional) - timestamp of the insert
|
||||
as returned by SELECT LOCALTIMESTAMP(0).
|
||||
C<ispatch> - boolean (optional, default false) - true if the
|
||||
attachment is a patch.
|
||||
C<isprivate> - boolean (optional, default false) - true if
|
||||
the attachment is private.
|
||||
C<isurl> - boolean (optional, default false) - true if the
|
||||
attachment is a URL pointing to some external ressource.
|
||||
C<store_in_file> - boolean (optional, default false) - true
|
||||
if the attachment must be stored in data/attachments/ instead
|
||||
of in the DB.
|
||||
|
||||
Returns: the ID of the new attachment.
|
||||
Returns: The new attachment object.
|
||||
|
||||
=cut
|
||||
|
||||
# FIXME: needs to follow the way Object->create() works.
|
||||
sub create {
|
||||
my ($class, $throw_error, $bug, $user, $timestamp, $hr_vars) = @_;
|
||||
|
||||
my $cgi = Bugzilla->cgi;
|
||||
my $class = shift;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $attachurl = $cgi->param('attachurl') || '';
|
||||
my $data;
|
||||
my $filename;
|
||||
my $contenttype;
|
||||
my $isurl;
|
||||
$class->validate_is_patch($throw_error) || return;
|
||||
$class->validate_description($throw_error) || return;
|
||||
|
||||
if (Bugzilla->params->{force_attach_bigfile})
|
||||
{
|
||||
# Force uploading into files instead of DB
|
||||
$cgi->param('bigfile', 1);
|
||||
}
|
||||
if (Bugzilla->params->{'allow_attach_url'}
|
||||
&& ($attachurl =~ /^(http|https|ftp):\/\/\S+/)
|
||||
&& !defined $cgi->upload('data'))
|
||||
{
|
||||
$filename = '';
|
||||
$data = $attachurl;
|
||||
$isurl = 1;
|
||||
$contenttype = 'text/plain';
|
||||
$cgi->param('ispatch', 0);
|
||||
$cgi->delete('bigfile');
|
||||
}
|
||||
else {
|
||||
$filename = _validate_filename($throw_error) || return;
|
||||
# need to validate content type before data as
|
||||
# we now check the content type for image/bmp in _validate_data()
|
||||
unless ($cgi->param('ispatch')) {
|
||||
$class->validate_content_type($throw_error) || return;
|
||||
$class->check_required_create_fields(@_);
|
||||
my $params = $class->run_create_validators(@_);
|
||||
|
||||
# Set the ispatch flag to 1 if we're set to autodetect
|
||||
# and the content type is text/x-diff or text/x-patch
|
||||
if ($cgi->param('contenttypemethod') eq 'autodetect'
|
||||
&& $cgi->param('contenttype') =~ m{text/x-(?:diff|patch)})
|
||||
{
|
||||
$cgi->param('ispatch', 1);
|
||||
$cgi->param('contenttype', 'text/plain');
|
||||
}
|
||||
}
|
||||
$data = _validate_data($throw_error, $hr_vars);
|
||||
# If the attachment is stored locally, $data eq ''.
|
||||
# If an error is thrown, $data eq '0'.
|
||||
($data ne '0') || return;
|
||||
$contenttype = $cgi->param('contenttype');
|
||||
# Extract everything which is not a valid column name.
|
||||
my $bug = delete $params->{bug};
|
||||
$params->{bug_id} = $bug->id;
|
||||
my $fh = delete $params->{data};
|
||||
my $store_in_file = delete $params->{store_in_file};
|
||||
|
||||
# These are inserted using placeholders so no need to panic
|
||||
trick_taint($filename);
|
||||
trick_taint($contenttype);
|
||||
$isurl = 0;
|
||||
}
|
||||
|
||||
# Check attachments the user tries to mark as obsolete.
|
||||
my @obsolete_attachments;
|
||||
if ($cgi->param('obsolete')) {
|
||||
@obsolete_attachments = $class->validate_obsolete($bug);
|
||||
}
|
||||
|
||||
# The order of these function calls is important, as Flag::validate
|
||||
# assumes User::match_field has ensured that the
|
||||
# values in the requestee fields are legitimate user email addresses.
|
||||
my $match_status = Bugzilla::User::match_field($cgi, {
|
||||
'^requestee(_type)?-(\d+)$' => { 'type' => 'multi' },
|
||||
}, MATCH_SKIP_CONFIRM);
|
||||
|
||||
$hr_vars->{'match_field'} = 'requestee';
|
||||
if ($match_status == USER_MATCH_FAILED) {
|
||||
$hr_vars->{'message'} = 'user_match_failed';
|
||||
}
|
||||
elsif ($match_status == USER_MATCH_MULTIPLE) {
|
||||
$hr_vars->{'message'} = 'user_match_multiple';
|
||||
}
|
||||
|
||||
# Escape characters in strings that will be used in SQL statements.
|
||||
my $description = $cgi->param('description');
|
||||
trick_taint($description);
|
||||
my $isprivate = $cgi->param('isprivate') ? 1 : 0;
|
||||
|
||||
# Insert the attachment into the database.
|
||||
my $sth = $dbh->do(
|
||||
"INSERT INTO attachments
|
||||
(bug_id, creation_ts, modification_time, filename, description,
|
||||
mimetype, ispatch, isurl, isprivate, submitter_id)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?)", undef, ($bug->bug_id, $timestamp, $timestamp,
|
||||
$filename, $description, $contenttype, $cgi->param('ispatch'),
|
||||
$isurl, $isprivate, $user->id));
|
||||
# Retrieve the ID of the newly created attachment record.
|
||||
my $attachid = $dbh->bz_last_key('attachments', 'attach_id');
|
||||
my $attachment = $class->insert_create_data($params);
|
||||
my $attachid = $attachment->id;
|
||||
|
||||
# We only use $data here in this INSERT with a placeholder,
|
||||
# so it's safe.
|
||||
$sth = $dbh->prepare("INSERT INTO attach_data
|
||||
(id, thedata) VALUES ($attachid, ?)");
|
||||
if (!$cgi->param('bigfile') && $data)
|
||||
{
|
||||
trick_taint($data);
|
||||
$sth->bind_param(1, $data, $dbh->BLOB_TYPE);
|
||||
$sth->execute();
|
||||
}
|
||||
my $sth = $dbh->prepare("INSERT INTO attach_data
|
||||
(id, thedata) VALUES ($attachid, ?)");
|
||||
|
||||
my $data = $store_in_file ? "" : $fh;
|
||||
trick_taint($data);
|
||||
$sth->bind_param(1, $data, $dbh->BLOB_TYPE);
|
||||
$sth->execute();
|
||||
|
||||
# If the file is to be stored locally, stream the file from the web server
|
||||
# to the local file without reading it into a local variable.
|
||||
if ($cgi->param('bigfile'))
|
||||
{
|
||||
if ($store_in_file) {
|
||||
my $attachdir = bz_locations()->{'attachdir'};
|
||||
my $hash = ($attachid % 100) + 100;
|
||||
$hash =~ s/.*(\d\d)$/group.$1/;
|
||||
mkdir "$attachdir/$hash", 0770;
|
||||
chmod 0770, "$attachdir/$hash";
|
||||
open AH, ">$attachdir/$hash/attachment.$attachid" or die "Could not write into $attachdir/$hash/attachment.$attachid: $!";
|
||||
open(AH, '>', "$attachdir/$hash/attachment.$attachid") or die "Could not write into $attachdir/$hash/attachment.$attachid: $!";
|
||||
binmode AH;
|
||||
if (my $fh = $cgi->upload('data'))
|
||||
{
|
||||
if (ref $fh) {
|
||||
my $limit = Bugzilla->params->{"maxlocalattachment"} * 1048576;
|
||||
my $sizecount = 0;
|
||||
my $limit = (Bugzilla->params->{"maxlocalattachment"} * 1048576);
|
||||
while (<$fh>) {
|
||||
print AH $_;
|
||||
$sizecount += length($_);
|
||||
|
@ -958,64 +875,73 @@ sub create {
|
|||
close AH;
|
||||
close $fh;
|
||||
unlink "$attachdir/$hash/attachment.$attachid";
|
||||
$throw_error ? ThrowUserError("local_file_too_large") : return;
|
||||
ThrowUserError("local_file_too_large");
|
||||
}
|
||||
}
|
||||
close $fh;
|
||||
}
|
||||
elsif ($data)
|
||||
{
|
||||
print AH $data;
|
||||
else {
|
||||
print AH $fh;
|
||||
}
|
||||
close AH;
|
||||
}
|
||||
|
||||
# Make existing attachments obsolete.
|
||||
my $fieldid = get_field_id('attachments.isobsolete');
|
||||
|
||||
foreach my $obsolete_attachment (@obsolete_attachments) {
|
||||
# If the obsolete attachment has request flags, cancel them.
|
||||
# This call must be done before updating the 'attachments' table.
|
||||
Bugzilla::Flag->CancelRequests($bug, $obsolete_attachment, $timestamp);
|
||||
|
||||
$dbh->do('UPDATE attachments SET isobsolete = 1, modification_time = ?
|
||||
WHERE attach_id = ?',
|
||||
undef, ($timestamp, $obsolete_attachment->id));
|
||||
|
||||
$dbh->do('INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when,
|
||||
fieldid, removed, added)
|
||||
VALUES (?,?,?,?,?,?,?)',
|
||||
undef, ($bug->bug_id, $obsolete_attachment->id, $user->id,
|
||||
$timestamp, $fieldid, 0, 1));
|
||||
}
|
||||
|
||||
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.
|
||||
# This requires errors to die().
|
||||
# XXX: this can go away as soon as flag validation is able to
|
||||
# fail without dying.
|
||||
#
|
||||
# 2. Flag::validate() should not detect any reference to existing flags
|
||||
# when creating a new attachment. Setting the third param to -1 will
|
||||
# force this function to check this point.
|
||||
my $error_mode_cache = Bugzilla->error_mode;
|
||||
Bugzilla->error_mode(ERROR_MODE_DIE);
|
||||
eval {
|
||||
Bugzilla::Flag::validate($bug->bug_id, -1, SKIP_REQUESTEE_ON_ERROR);
|
||||
Bugzilla::Flag->process($bug, $attachment, $timestamp, $hr_vars);
|
||||
};
|
||||
Bugzilla->error_mode($error_mode_cache);
|
||||
if ($@) {
|
||||
$hr_vars->{'message'} = 'flag_creation_failed';
|
||||
$hr_vars->{'flag_creation_error'} = $@;
|
||||
}
|
||||
|
||||
# Return the new attachment object.
|
||||
return $attachment;
|
||||
}
|
||||
|
||||
sub run_create_validators {
|
||||
my ($class, $params) = @_;
|
||||
|
||||
# Let's validate the attachment content first as it may
|
||||
# alter some other attachment attributes.
|
||||
$params->{data} = $class->_check_data($params);
|
||||
$params = $class->SUPER::run_create_validators($params);
|
||||
|
||||
$params->{filename} = $class->_check_filename($params->{filename}, $params->{isurl});
|
||||
$params->{creation_ts} ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
|
||||
$params->{modification_time} = $params->{creation_ts};
|
||||
$params->{submitter_id} = Bugzilla->user->id || ThrowCodeError('invalid_user');
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
sub update {
|
||||
my $self = shift;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $user = Bugzilla->user;
|
||||
my $timestamp = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
|
||||
|
||||
my ($changes, $old_self) = $self->SUPER::update(@_);
|
||||
|
||||
my ($removed, $added) = Bugzilla::Flag->update_flags($self, $old_self, $timestamp);
|
||||
if ($removed || $added) {
|
||||
$changes->{'flagtypes.name'} = [$removed, $added];
|
||||
}
|
||||
|
||||
# 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 (?, ?, ?, ?, ?, ?, ?)');
|
||||
|
||||
foreach my $field (keys %$changes) {
|
||||
my $change = $changes->{$field};
|
||||
$field = "attachments.$field" unless $field eq "flagtypes.name";
|
||||
my $fieldid = get_field_id($field);
|
||||
$sth->execute($self->bug_id, $self->id, $user->id, $timestamp,
|
||||
$fieldid, $change->[0], $change->[1]);
|
||||
}
|
||||
|
||||
if (scalar(keys %$changes)) {
|
||||
$dbh->do('UPDATE attachments SET modification_time = ? WHERE attach_id = ?',
|
||||
undef, ($timestamp, $self->id));
|
||||
$dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?',
|
||||
undef, ($timestamp, $self->bug_id));
|
||||
}
|
||||
|
||||
return $changes;
|
||||
}
|
||||
|
||||
=pod
|
||||
|
||||
=item C<remove_from_db()>
|
||||
|
@ -1042,4 +968,67 @@ sub remove_from_db {
|
|||
$dbh->bz_commit_transaction();
|
||||
}
|
||||
|
||||
###############################
|
||||
#### Helpers #####
|
||||
###############################
|
||||
|
||||
# Extract the content type from the attachment form.
|
||||
my $lwp_read_mime_types;
|
||||
sub get_content_type {
|
||||
my $cgi = Bugzilla->cgi;
|
||||
|
||||
return 'text/plain' if ($cgi->param('ispatch') ||
|
||||
$cgi->param('text_attachment') !~ /^\s*$/so ||
|
||||
$cgi->param('attachurl'));
|
||||
|
||||
my $content_type;
|
||||
if (!defined $cgi->param('contenttypemethod')) {
|
||||
ThrowUserError("missing_content_type_method");
|
||||
}
|
||||
elsif ($cgi->param('contenttypemethod') eq 'autodetect') {
|
||||
defined $cgi->upload('data') || ThrowUserError('file_not_specified');
|
||||
# The user asked us to auto-detect the content type, so use the type
|
||||
# specified in the HTTP request headers.
|
||||
$content_type =
|
||||
$cgi->uploadInfo($cgi->param('data'))->{'Content-Type'};
|
||||
if (!_valid_content_type($content_type) && Bugzilla->params->{mime_types_file})
|
||||
{
|
||||
if (!$lwp_read_mime_types)
|
||||
{
|
||||
LWP::MediaTypes::read_media_types(Bugzilla->params->{mime_types_file});
|
||||
$lwp_read_mime_types = 1;
|
||||
}
|
||||
my $file = $cgi->param('data');
|
||||
$content_type = LWP::MediaTypes::guess_media_type("$file");
|
||||
}
|
||||
if (!_valid_content_type($content_type))
|
||||
{
|
||||
$content_type = 'application/octet-stream';
|
||||
}
|
||||
$content_type || ThrowUserError("missing_content_type");
|
||||
|
||||
# Set the ispatch flag to 1 if the content type
|
||||
# is text/x-diff or text/x-patch
|
||||
if ($content_type =~ m{text/x-(?:diff|patch)}) {
|
||||
$cgi->param('ispatch', 1);
|
||||
$content_type = 'text/plain';
|
||||
}
|
||||
}
|
||||
elsif ($cgi->param('contenttypemethod') eq 'list') {
|
||||
# The user selected a content type from the list, so use their
|
||||
# selection.
|
||||
$content_type = $cgi->param('contenttypeselection');
|
||||
}
|
||||
elsif ($cgi->param('contenttypemethod') eq 'manual') {
|
||||
# The user entered a content type manually, so use their entry.
|
||||
$content_type = $cgi->param('contenttypeentry');
|
||||
}
|
||||
else {
|
||||
ThrowCodeError("illegal_content_type_method",
|
||||
{ contenttypemethod => $cgi->param('contenttypemethod') });
|
||||
}
|
||||
return $content_type;
|
||||
}
|
||||
|
||||
|
||||
1;
|
||||
|
|
|
@ -32,6 +32,9 @@ use fields qw(
|
|||
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::Error;
|
||||
use Bugzilla::Mailer;
|
||||
use Bugzilla::Util qw(datetime_from);
|
||||
use Bugzilla::User::Setting ();
|
||||
use Bugzilla::Auth::Login::Stack;
|
||||
use Bugzilla::Auth::Verify::Stack;
|
||||
use Bugzilla::Auth::Persist::Cookie;
|
||||
|
@ -185,7 +188,10 @@ sub _handle_login_result {
|
|||
# the password was just wrong. (This makes it harder for a cracker
|
||||
# to find account names by brute force)
|
||||
elsif ($fail_code == AUTH_LOGINFAILED or $fail_code == AUTH_NO_SUCH_USER) {
|
||||
ThrowUserError("invalid_username_or_password");
|
||||
my $remaining_attempts = MAX_LOGIN_ATTEMPTS
|
||||
- ($result->{failure_count} || 0);
|
||||
ThrowUserError("invalid_username_or_password",
|
||||
{ remaining => $remaining_attempts });
|
||||
}
|
||||
# The account may be disabled
|
||||
elsif ($fail_code == AUTH_DISABLED) {
|
||||
|
@ -196,6 +202,40 @@ sub _handle_login_result {
|
|||
ThrowUserError("account_disabled",
|
||||
{'disabled_reason' => $result->{user}->disabledtext});
|
||||
}
|
||||
elsif ($fail_code == AUTH_LOCKOUT) {
|
||||
my $attempts = $user->account_ip_login_failures;
|
||||
|
||||
# We want to know when the account will be unlocked. This is
|
||||
# determined by the 5th-from-last login failure (or more/less than
|
||||
# 5th, if MAX_LOGIN_ATTEMPTS is not 5).
|
||||
my $determiner = $attempts->[scalar(@$attempts) - MAX_LOGIN_ATTEMPTS];
|
||||
my $unlock_at = datetime_from($determiner->{login_time},
|
||||
Bugzilla->local_timezone);
|
||||
$unlock_at->add(minutes => LOGIN_LOCKOUT_INTERVAL);
|
||||
|
||||
# If we were *just* locked out, notify the maintainer about the
|
||||
# lockout.
|
||||
if ($result->{just_locked_out}) {
|
||||
# We're sending to the maintainer, who may be not a Bugzilla
|
||||
# account, but just an email address. So we use the
|
||||
# installation's default language for sending the email.
|
||||
my $default_settings = Bugzilla::User::Setting::get_defaults();
|
||||
my $template = Bugzilla->template_inner($default_settings->{lang});
|
||||
my $vars = {
|
||||
locked_user => $user,
|
||||
attempts => $attempts,
|
||||
unlock_at => $unlock_at,
|
||||
};
|
||||
my $message;
|
||||
$template->process('email/lockout.txt.tmpl', $vars, \$message)
|
||||
|| ThrowTemplateError($template->error);
|
||||
MessageToMTA($message);
|
||||
}
|
||||
|
||||
$unlock_at->set_time_zone($user->timezone);
|
||||
ThrowUserError('account_locked',
|
||||
{ ip_addr => $determiner->{ip_addr}, unlock_at => $unlock_at });
|
||||
}
|
||||
# If we get here, then we've run out of options, which shouldn't happen.
|
||||
else {
|
||||
ThrowCodeError("authres_unhandled", { value => $fail_code });
|
||||
|
@ -257,6 +297,11 @@ various fields to be used in the error message.
|
|||
|
||||
An incorrect username or password was given.
|
||||
|
||||
The hashref may also contain a C<failure_count> element, which specifies
|
||||
how many times the account has failed to log in within the lockout
|
||||
period (see L</AUTH_LOCKOUT>). This is used to warn the user when
|
||||
he is getting close to being locked out.
|
||||
|
||||
=head2 C<AUTH_NO_SUCH_USER>
|
||||
|
||||
This is an optional more-specific version of C<AUTH_LOGINFAILED>.
|
||||
|
@ -274,6 +319,15 @@ should never be communicated to the user, for security reasons.
|
|||
The user successfully logged in, but their account has been disabled.
|
||||
Usually this is throw only by C<Bugzilla::Auth::login>.
|
||||
|
||||
=head2 C<AUTH_LOCKOUT>
|
||||
|
||||
The user's account is locked out after having failed to log in too many
|
||||
times within a certain period of time (as specified by
|
||||
L<Bugzilla::Constants/LOGIN_LOCKOUT_INTERVAL>).
|
||||
|
||||
The hashref will also contain a C<user> element, representing the
|
||||
L<Bugzilla::User> whose account is locked out.
|
||||
|
||||
=head1 LOGIN TYPES
|
||||
|
||||
The C<login> function (below) can do different types of login, depending
|
||||
|
|
|
@ -40,12 +40,10 @@ use Bugzilla::Error;
|
|||
|
||||
sub get_login_info {
|
||||
my ($self) = @_;
|
||||
my $cgi = Bugzilla->cgi;
|
||||
my $params = Bugzilla->input_params;
|
||||
|
||||
my $username = trim($cgi->param("Bugzilla_login"));
|
||||
my $password = $cgi->param("Bugzilla_password");
|
||||
|
||||
$cgi->delete('Bugzilla_login', 'Bugzilla_password');
|
||||
my $username = trim(delete $params->{"Bugzilla_login"});
|
||||
my $password = delete $params->{"Bugzilla_password"};
|
||||
|
||||
if (!defined $username || !defined $password) {
|
||||
return { failure => AUTH_NODATA };
|
||||
|
@ -59,21 +57,8 @@ sub fail_nodata {
|
|||
my $cgi = Bugzilla->cgi;
|
||||
my $template = Bugzilla->template;
|
||||
|
||||
if (Bugzilla->error_mode == Bugzilla::Constants::ERROR_MODE_DIE_SOAP_FAULT) {
|
||||
die SOAP::Fault
|
||||
->faultcode(ERROR_AUTH_NODATA)
|
||||
->faultstring('Login Required');
|
||||
}
|
||||
|
||||
# If system is not configured to never require SSL connections
|
||||
# we want to always redirect to SSL since passing usernames and
|
||||
# passwords over an unprotected connection is a bad idea. If we
|
||||
# get here then a login form will be provided to the user so we
|
||||
# want this to be protected if possible.
|
||||
if ($cgi->protocol ne 'https' && Bugzilla->params->{'sslbase'} ne ''
|
||||
&& Bugzilla->params->{'ssl'} ne 'never')
|
||||
{
|
||||
$cgi->require_https(Bugzilla->params->{'sslbase'});
|
||||
if (Bugzilla->usage_mode != USAGE_MODE_BROWSER) {
|
||||
ThrowUserError('login_required');
|
||||
}
|
||||
|
||||
print $cgi->header();
|
||||
|
|
|
@ -35,8 +35,7 @@ sub get_login_info {
|
|||
my $cgi = Bugzilla->cgi;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
my $ip_addr = $cgi->remote_addr();
|
||||
my $net_addr = get_netaddr($ip_addr);
|
||||
my $ip_addr = remote_ip();
|
||||
my $login_cookie = $cgi->cookie("Bugzilla_logincookie");
|
||||
my $user_id = $cgi->cookie("Bugzilla_login");
|
||||
|
||||
|
@ -60,24 +59,16 @@ sub get_login_info {
|
|||
trick_taint($login_cookie);
|
||||
detaint_natural($user_id);
|
||||
|
||||
my $query = "SELECT userid
|
||||
FROM logincookies
|
||||
WHERE logincookies.cookie = ?
|
||||
AND logincookies.userid = ?
|
||||
AND (logincookies.ipaddr = ?";
|
||||
|
||||
# If we have a network block that's allowed to use this cookie,
|
||||
# as opposed to just a single IP.
|
||||
my @params = ($login_cookie, $user_id, $ip_addr);
|
||||
if (defined $net_addr) {
|
||||
trick_taint($net_addr);
|
||||
$query .= " OR logincookies.ipaddr = ?";
|
||||
push(@params, $net_addr);
|
||||
}
|
||||
$query .= ")";
|
||||
my $is_valid =
|
||||
$dbh->selectrow_array('SELECT 1
|
||||
FROM logincookies
|
||||
WHERE cookie = ?
|
||||
AND userid = ?
|
||||
AND (ipaddr = ? OR ipaddr IS NULL)',
|
||||
undef, ($login_cookie, $user_id, $ip_addr));
|
||||
|
||||
# If the cookie is valid, return a valid username.
|
||||
if ($dbh->selectrow_array($query, undef, @params)) {
|
||||
if ($is_valid) {
|
||||
# If we logged in successfully, then update the lastused
|
||||
# time on the login cookie
|
||||
$dbh->do("UPDATE logincookies SET lastused = NOW()
|
||||
|
|
|
@ -35,7 +35,7 @@ sub new {
|
|||
my $list = shift;
|
||||
my %methods = map { $_ => "Bugzilla/Auth/Login/$_.pm" } split(',', $list);
|
||||
lock_keys(%methods);
|
||||
Bugzilla::Hook::process('auth-login_methods', { modules => \%methods });
|
||||
Bugzilla::Hook::process('auth_login_methods', { modules => \%methods });
|
||||
|
||||
$self->{_stack} = [];
|
||||
foreach my $login_method (split(',', $list)) {
|
||||
|
|
|
@ -48,18 +48,16 @@ sub persist_login {
|
|||
my ($self, $user) = @_;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $cgi = Bugzilla->cgi;
|
||||
my $input_params = Bugzilla->input_params;
|
||||
|
||||
my $ip_addr = $cgi->remote_addr;
|
||||
unless ($cgi->param('Bugzilla_restrictlogin') ||
|
||||
Bugzilla->params->{'loginnetmask'} == 32)
|
||||
{
|
||||
$ip_addr = get_netaddr($ip_addr);
|
||||
my $ip_addr;
|
||||
if ($input_params->{'Bugzilla_restrictlogin'}) {
|
||||
$ip_addr = remote_ip();
|
||||
# The IP address is valid, at least for comparing with itself in a
|
||||
# subsequent login
|
||||
trick_taint($ip_addr);
|
||||
}
|
||||
|
||||
# The IP address is valid, at least for comparing with itself in a
|
||||
# subsequent login
|
||||
trick_taint($ip_addr);
|
||||
|
||||
$dbh->bz_start_transaction();
|
||||
|
||||
my $login_cookie =
|
||||
|
@ -83,17 +81,15 @@ sub persist_login {
|
|||
# or admin didn't forbid it and user told to remember.
|
||||
if ( Bugzilla->params->{'rememberlogin'} eq 'on' ||
|
||||
(Bugzilla->params->{'rememberlogin'} ne 'off' &&
|
||||
$cgi->param('Bugzilla_remember') &&
|
||||
$cgi->param('Bugzilla_remember') eq 'on') )
|
||||
$input_params->{'Bugzilla_remember'} &&
|
||||
$input_params->{'Bugzilla_remember'} eq 'on') )
|
||||
{
|
||||
# Not a session cookie, so set an infinite expiry
|
||||
$cookieargs{'-expires'} = 'Fri, 01-Jan-2038 00:00:00 GMT';
|
||||
}
|
||||
if (Bugzilla->params->{'ssl'} ne 'never'
|
||||
&& Bugzilla->params->{'sslbase'} ne '')
|
||||
{
|
||||
# Bugzilla->login will automatically redirect to https://,
|
||||
# so it's safe to turn on the 'secure' bit.
|
||||
if (Bugzilla->params->{'ssl_redirect'}) {
|
||||
# Make these cookies only be sent to us by the browser during
|
||||
# HTTPS sessions, if we're using SSL.
|
||||
$cookieargs{'-secure'} = 1;
|
||||
}
|
||||
|
||||
|
|
|
@ -41,37 +41,51 @@ sub check_credentials {
|
|||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
my $username = $login_data->{username};
|
||||
my $user_id = login_to_id($username);
|
||||
my $user = new Bugzilla::User({ name => $username });
|
||||
|
||||
return { failure => AUTH_NO_SUCH_USER } unless $user_id;
|
||||
return { failure => AUTH_NO_SUCH_USER } unless $user;
|
||||
|
||||
$login_data->{user} = $user;
|
||||
$login_data->{bz_username} = $user->login;
|
||||
|
||||
if ($user->account_is_locked_out) {
|
||||
return { failure => AUTH_LOCKOUT, user => $user };
|
||||
}
|
||||
|
||||
$login_data->{bz_username} = $username;
|
||||
my $password = $login_data->{password};
|
||||
|
||||
trick_taint($username);
|
||||
my ($real_password_crypted) = $dbh->selectrow_array(
|
||||
"SELECT cryptpassword FROM profiles WHERE userid = ?",
|
||||
undef, $user_id);
|
||||
my $real_password_crypted = $user->cryptpassword;
|
||||
|
||||
# Using the internal crypted password as the salt,
|
||||
# crypt the password the user entered.
|
||||
my $entered_password_crypted = bz_crypt($password, $real_password_crypted);
|
||||
|
||||
return { failure => AUTH_LOGINFAILED }
|
||||
if $entered_password_crypted ne $real_password_crypted;
|
||||
if ($entered_password_crypted ne $real_password_crypted) {
|
||||
# Record the login failure
|
||||
$user->note_login_failure();
|
||||
|
||||
# Immediately check if we are locked out
|
||||
if ($user->account_is_locked_out) {
|
||||
return { failure => AUTH_LOCKOUT, user => $user,
|
||||
just_locked_out => 1 };
|
||||
}
|
||||
|
||||
return { failure => AUTH_LOGINFAILED,
|
||||
failure_count => scalar(@{ $user->account_ip_login_failures }),
|
||||
};
|
||||
}
|
||||
|
||||
# The user's credentials are okay, so delete any outstanding
|
||||
# password tokens they may have generated.
|
||||
Bugzilla::Token::DeletePasswordTokens($user_id, "user_logged_in");
|
||||
# password tokens or login failures they may have generated.
|
||||
Bugzilla::Token::DeletePasswordTokens($user->id, "user_logged_in");
|
||||
$user->clear_login_failures();
|
||||
|
||||
# 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);
|
||||
$user->set_password($password);
|
||||
$user->update();
|
||||
}
|
||||
|
||||
return $login_data;
|
||||
|
|
|
@ -56,7 +56,7 @@ sub check_credentials {
|
|||
# just appending the Base DN to the uid isn't sufficient to get the
|
||||
# user's DN. For servers which don't work this way, there will still
|
||||
# be no harm done.
|
||||
$self->_bind_ldap_anonymously();
|
||||
$self->_bind_ldap_for_search();
|
||||
|
||||
# Now, we verify that the user exists, and get a LDAP Distinguished
|
||||
# Name for the user.
|
||||
|
@ -76,12 +76,35 @@ sub check_credentials {
|
|||
return { failure => AUTH_LOGINFAILED } if $pw_result->code;
|
||||
|
||||
# And now we fill in the user's details.
|
||||
my $detail_result = $self->ldap->search(_bz_search_params($username));
|
||||
return { failure => AUTH_ERROR, error => "ldap_search_error",
|
||||
details => {errstr => $detail_result->error, username => $username}
|
||||
} if $detail_result->code;
|
||||
|
||||
my $user_entry = $detail_result->shift_entry;
|
||||
# First try the search as the (already bound) user in question.
|
||||
my $user_entry;
|
||||
my $error_string;
|
||||
my $detail_result = $self->ldap->search(_bz_search_params($username));
|
||||
if ($detail_result->code) {
|
||||
# Stash away the original error, just in case
|
||||
$error_string = $detail_result->error;
|
||||
} else {
|
||||
$user_entry = $detail_result->shift_entry;
|
||||
}
|
||||
|
||||
# If that failed (either because the search failed, or returned no
|
||||
# results) then try re-binding as the initial search user, but only
|
||||
# if the LDAPbinddn parameter is set.
|
||||
if (!$user_entry && Bugzilla->params->{"LDAPbinddn"}) {
|
||||
$self->_bind_ldap_for_search();
|
||||
|
||||
$detail_result = $self->ldap->search(_bz_search_params($username));
|
||||
if (!$detail_result->code) {
|
||||
$user_entry = $detail_result->shift_entry;
|
||||
}
|
||||
}
|
||||
|
||||
# If we *still* don't have anything in $user_entry then give up.
|
||||
return { failure => AUTH_ERROR, error => "ldap_search_error",
|
||||
details => {errstr => $error_string, username => $username}
|
||||
} if !$user_entry;
|
||||
|
||||
|
||||
my $mail_attr = Bugzilla->params->{"LDAPmailattribute"};
|
||||
if ($mail_attr) {
|
||||
|
@ -128,7 +151,7 @@ sub _bz_search_params {
|
|||
. Bugzilla->params->{"LDAPfilter"} . ')');
|
||||
}
|
||||
|
||||
sub _bind_ldap_anonymously {
|
||||
sub _bind_ldap_for_search {
|
||||
my ($self) = @_;
|
||||
my $bind_result;
|
||||
if (Bugzilla->params->{"LDAPbinddn"}) {
|
||||
|
|
|
@ -30,7 +30,7 @@ sub new {
|
|||
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 });
|
||||
Bugzilla::Hook::process('auth_verify_methods', { modules => \%methods });
|
||||
|
||||
$self->{_stack} = [];
|
||||
foreach my $verify_method (split(',', $list)) {
|
||||
|
|
646
Bugzilla/Bug.pm
646
Bugzilla/Bug.pm
File diff suppressed because it is too large
Load Diff
|
@ -110,7 +110,6 @@ sub three_columns {
|
|||
# roles when the email is sent.
|
||||
# All the names are email addresses, not userids
|
||||
# values are scalars, except for cc, which is a list
|
||||
# This hash usually comes from the "mailrecipients" var in a template call.
|
||||
sub Send {
|
||||
my ($id, $forced) = (@_);
|
||||
|
||||
|
@ -121,6 +120,7 @@ sub Send {
|
|||
my $msg = "";
|
||||
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $bug = new Bugzilla::Bug($id);
|
||||
|
||||
# XXX - These variables below are useless. We could use field object
|
||||
# methods directly. But we first have to implement a cache in
|
||||
|
@ -327,7 +327,7 @@ sub Send {
|
|||
}
|
||||
}
|
||||
|
||||
my ($comments, $anyprivate) = get_comments_by_bug($id, $start, $end);
|
||||
my $comments = $bug->comments({ after => $start, to => $end });
|
||||
|
||||
###########################################################################
|
||||
# Start of email filtering code
|
||||
|
@ -388,6 +388,9 @@ sub Send {
|
|||
}
|
||||
}
|
||||
|
||||
Bugzilla::Hook::process('bugmail_recipients',
|
||||
{ bug => $bug, recipients => \%recipients });
|
||||
|
||||
# Find all those user-watching anyone on the current list, who is not
|
||||
# on it already themselves.
|
||||
my $involved = join(",", keys %recipients);
|
||||
|
@ -449,11 +452,6 @@ sub Send {
|
|||
# So the user exists, can see the bug, and wants mail in at least
|
||||
# one role. But do we want to send it to them?
|
||||
|
||||
# If we are using insiders, and the comment is private, only send
|
||||
# to insiders
|
||||
my $insider_ok = 1;
|
||||
$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
|
||||
# visible to the user. This is to avoid leaking the summaries of
|
||||
|
@ -468,10 +466,7 @@ sub Send {
|
|||
|
||||
# Make sure the user isn't in the nomail list, and the insider and
|
||||
# dep checks passed.
|
||||
if ($user->email_enabled &&
|
||||
$insider_ok &&
|
||||
$dep_ok)
|
||||
{
|
||||
if ($user->email_enabled && $dep_ok) {
|
||||
# OK, OK, if we must. Email the user.
|
||||
$sent_mail = sendMail(
|
||||
user => $user,
|
||||
|
@ -482,7 +477,6 @@ sub Send {
|
|||
fields => \%fielddescription,
|
||||
diffs => $diffs,
|
||||
newcomm => $comments,
|
||||
anypriv => $anyprivate,
|
||||
isnew => !$start,
|
||||
id => $id,
|
||||
watch => exists $watching{$user_id} ? $watching{$user_id} : undef,
|
||||
|
@ -508,14 +502,15 @@ sub sendMail
|
|||
{
|
||||
my %arguments = @_;
|
||||
my ($user, $hlRef, $relRef, $valueRef, $dmhRef, $fdRef,
|
||||
$diffs, $newcomments, $anyprivate, $isnew,
|
||||
$diffs, $comments_in, $isnew,
|
||||
$id, $watchingRef
|
||||
) = @arguments{qw(
|
||||
user headers rels values defhead fields
|
||||
diffs newcomm anypriv isnew
|
||||
diffs newcomm isnew
|
||||
id watch
|
||||
)};
|
||||
|
||||
my @send_comments = @$comments_in;
|
||||
my %values = %$valueRef;
|
||||
my @headerlist = @$hlRef;
|
||||
my %mailhead = %$dmhRef;
|
||||
|
@ -539,7 +534,11 @@ sub sendMail
|
|||
|
||||
$diffs = $new_diffs;
|
||||
|
||||
if (!@$diffs && !scalar(@$newcomments) && !$isnew) {
|
||||
if (!$user->is_insider) {
|
||||
@send_comments = grep { !$_->is_private } @send_comments;
|
||||
}
|
||||
|
||||
if (!@$diffs && !scalar(@send_comments) && !$isnew) {
|
||||
# Whoops, no differences!
|
||||
return 0;
|
||||
}
|
||||
|
@ -598,7 +597,7 @@ sub sendMail
|
|||
reporter => $values{'reporter'},
|
||||
reportername => Bugzilla::User->new({name => $values{'reporter'}})->name,
|
||||
diffs => $diffs,
|
||||
new_comments => $newcomments,
|
||||
new_comments => \@send_comments,
|
||||
threadingmarker => build_thread_marker($id, $user->id, $isnew),
|
||||
three_columns => \&three_columns,
|
||||
};
|
||||
|
@ -623,43 +622,4 @@ sub sendMail
|
|||
return 1;
|
||||
}
|
||||
|
||||
# Get bug comments for the given period.
|
||||
sub get_comments_by_bug {
|
||||
my ($id, $start, $end) = @_;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
my $result = "";
|
||||
my $count = 0;
|
||||
my $anyprivate = 0;
|
||||
|
||||
# $start will be undef for new bugs, and defined for pre-existing bugs.
|
||||
if ($start) {
|
||||
# If $start is not NULL, obtain the count-index
|
||||
# of this comment for the leading "Comment #xxx" line.
|
||||
$count = $dbh->selectrow_array('SELECT COUNT(*) FROM longdescs
|
||||
WHERE bug_id = ? AND bug_when <= ?',
|
||||
undef, ($id, $start));
|
||||
}
|
||||
|
||||
my $raw = 0; # Do not format comments which are not of type CMT_NORMAL.
|
||||
my $comments = Bugzilla::Bug::GetComments($id, "oldest_to_newest", $start, $end, $raw);
|
||||
my $attach_base = correct_urlbase() . 'attachment.cgi?id=';
|
||||
|
||||
foreach my $comment (@$comments) {
|
||||
$comment->{count} = $count++;
|
||||
# If an attachment was created, then add an URL. (Note: the 'g'lobal
|
||||
# replace should work with comments with multiple attachments.)
|
||||
if ($comment->{body} =~ /Created an attachment \(/) {
|
||||
$comment->{body} =~ s/(Created an attachment \(id=([0-9]+)\))/$1\n --> \($attach_base$2\)/g;
|
||||
}
|
||||
$comment->{body} = $comment->{'already_wrapped'} ? $comment->{body} : wrap_comment($comment->{body});
|
||||
}
|
||||
|
||||
if (Bugzilla->params->{'insidergroup'}) {
|
||||
$anyprivate = 1 if scalar(grep {$_->{'isprivate'} > 0} @$comments);
|
||||
}
|
||||
|
||||
return ($comments, $anyprivate);
|
||||
}
|
||||
|
||||
1;
|
||||
|
|
150
Bugzilla/CGI.pm
150
Bugzilla/CGI.pm
|
@ -21,26 +21,27 @@
|
|||
# Byron Jones <bugzilla@glob.com.au>
|
||||
# Marc Schumann <wurblzap@gmail.com>
|
||||
|
||||
package Bugzilla::CGI;
|
||||
use strict;
|
||||
|
||||
package Bugzilla::CGI;
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::Error;
|
||||
use Bugzilla::Util;
|
||||
|
||||
use File::Basename;
|
||||
|
||||
BEGIN {
|
||||
if ($^O =~ /MSWin32/i) {
|
||||
if (ON_WINDOWS) {
|
||||
# Help CGI find the correct temp directory as the default list
|
||||
# isn't Windows friendly (Bug 248988)
|
||||
$ENV{'TMPDIR'} = $ENV{'TEMP'} || $ENV{'TMP'} || "$ENV{'WINDIR'}\\TEMP";
|
||||
}
|
||||
}
|
||||
|
||||
use CGI qw(-no_xhtml -oldstyle_urls :private_tempfiles :unique_headers SERVER_PUSH);
|
||||
|
||||
use CGI qw(-no_xhtml -oldstyle_urls :private_tempfiles
|
||||
:unique_headers SERVER_PUSH);
|
||||
use base qw(CGI);
|
||||
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::Error;
|
||||
use Bugzilla::Util;
|
||||
|
||||
# We need to disable output buffering - see bug 179174
|
||||
$| = 1;
|
||||
|
||||
|
@ -72,15 +73,9 @@ sub new {
|
|||
$self->charset(Bugzilla->params->{'utf8'} ? 'UTF-8' : '');
|
||||
|
||||
# Redirect to urlbase/sslbase if we are not viewing an attachment.
|
||||
if (use_attachbase() && i_am_cgi()) {
|
||||
my $cgi_file = $self->url('-path_info' => 0, '-query' => 0, '-relative' => 1);
|
||||
$cgi_file =~ s/\?$//;
|
||||
my $urlbase = Bugzilla->params->{'urlbase'};
|
||||
my $sslbase = Bugzilla->params->{'sslbase'};
|
||||
my $path_regexp = $sslbase ? qr/^(\Q$urlbase\E|\Q$sslbase\E)/ : qr/^\Q$urlbase\E/;
|
||||
if ($cgi_file ne 'attachment.cgi' && $self->self_url !~ /$path_regexp/) {
|
||||
$self->redirect_to_urlbase;
|
||||
}
|
||||
my $script = basename($0);
|
||||
if ($self->url_is_attachment_base and $script ne 'attachment.cgi') {
|
||||
$self->redirect_to_urlbase();
|
||||
}
|
||||
|
||||
# Check for errors
|
||||
|
@ -122,6 +117,7 @@ sub parse_params {
|
|||
sub canonicalise_query {
|
||||
my ($self, @exclude) = @_;
|
||||
|
||||
$self->convert_old_params();
|
||||
# Reconstruct the URL by concatenating the sorted param=value pairs
|
||||
my @parameters;
|
||||
foreach my $key (sort($self->param())) {
|
||||
|
@ -146,6 +142,17 @@ sub canonicalise_query {
|
|||
return join("&", @parameters);
|
||||
}
|
||||
|
||||
sub convert_old_params {
|
||||
my $self = shift;
|
||||
|
||||
# bugidtype is now bug_id_type.
|
||||
if ($self->param('bugidtype')) {
|
||||
my $value = $self->param('bugidtype') eq 'exclude' ? 'nowords' : 'anyexact';
|
||||
$self->param('bug_id_type', $value);
|
||||
$self->delete('bugidtype');
|
||||
}
|
||||
}
|
||||
|
||||
sub clean_search_url {
|
||||
my $self = shift;
|
||||
# Delete any empty URL parameter.
|
||||
|
@ -165,9 +172,6 @@ sub clean_search_url {
|
|||
}
|
||||
}
|
||||
|
||||
# Delete certain parameters if the associated parameter is empty.
|
||||
$self->delete('bugidtype') if !$self->param('bug_id');
|
||||
|
||||
# Delete leftovers from the login form
|
||||
$self->delete('Bugzilla_remember', 'GoAheadAndLogIn');
|
||||
|
||||
|
@ -306,7 +310,7 @@ sub param {
|
|||
}
|
||||
|
||||
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.
|
||||
|
@ -374,25 +378,26 @@ sub remove_cookie {
|
|||
'-value' => 'X');
|
||||
}
|
||||
|
||||
# Redirect to https if required
|
||||
sub require_https {
|
||||
my ($self, $url) = @_;
|
||||
# Do not create query string if data submitted via XMLRPC
|
||||
# since we want the data to be resubmitted over POST method.
|
||||
my $query = Bugzilla->usage_mode == USAGE_MODE_WEBSERVICE ? 0 : 1;
|
||||
# XMLRPC clients (SOAP::Lite at least) requires 301 to redirect properly
|
||||
# and do not work with 302.
|
||||
my $status = Bugzilla->usage_mode == USAGE_MODE_WEBSERVICE ? 301 : 302;
|
||||
if (defined $url) {
|
||||
$url .= $self->url('-path_info' => 1, '-query' => $query, '-relative' => 1);
|
||||
} else {
|
||||
$url = $self->self_url;
|
||||
$url =~ s/^http:/https:/i;
|
||||
}
|
||||
print $self->redirect(-location => $url, -status => $status);
|
||||
# When using XML-RPC with mod_perl, we need the headers sent immediately.
|
||||
$self->r->rflush if $ENV{MOD_PERL};
|
||||
exit;
|
||||
sub redirect_to_https {
|
||||
my $self = shift;
|
||||
my $sslbase = Bugzilla->params->{'sslbase'};
|
||||
# If this is a POST, we don't want ?POSTDATA in the query string.
|
||||
# We expect the client to re-POST, which may be a violation of
|
||||
# the HTTP spec, but the only time we're expecting it often is
|
||||
# in the WebService, and WebService clients usually handle this
|
||||
# correctly.
|
||||
$self->delete('POSTDATA');
|
||||
my $url = $sslbase . $self->url('-path_info' => 1, '-query' => 1,
|
||||
'-relative' => 1);
|
||||
|
||||
# XML-RPC clients (SOAP::Lite at least) require a 301 to redirect properly
|
||||
# and do not work with 302. Our redirect really is permanent anyhow, so
|
||||
# it doesn't hurt to make it a 301.
|
||||
print $self->redirect(-location => $url, -status => 301);
|
||||
|
||||
# When using XML-RPC with mod_perl, we need the headers sent immediately.
|
||||
$self->r->rflush if $ENV{MOD_PERL};
|
||||
exit;
|
||||
}
|
||||
|
||||
# Redirect to the urlbase version of the current URL.
|
||||
|
@ -403,6 +408,61 @@ sub redirect_to_urlbase {
|
|||
exit;
|
||||
}
|
||||
|
||||
sub url_is_attachment_base {
|
||||
my ($self, $id) = @_;
|
||||
return 0 if !use_attachbase() or !i_am_cgi();
|
||||
my $attach_base = Bugzilla->params->{'attachment_base'};
|
||||
# If we're passed an id, we only want one specific attachment base
|
||||
# for a particular bug. If we're not passed an ID, we just want to
|
||||
# know if our current URL matches the attachment_base *pattern*.
|
||||
my $regex;
|
||||
if ($id) {
|
||||
$attach_base =~ s/\%bugid\%/$id/;
|
||||
$regex = quotemeta($attach_base);
|
||||
}
|
||||
else {
|
||||
# In this circumstance we run quotemeta first because we need to
|
||||
# insert an active regex meta-character afterward.
|
||||
$regex = quotemeta($attach_base);
|
||||
$regex =~ s/\\\%bugid\\\%/\\d+/;
|
||||
}
|
||||
$regex = "^$regex";
|
||||
return ($self->self_url =~ $regex) ? 1 : 0;
|
||||
}
|
||||
|
||||
##########################
|
||||
# Vars TIEHASH Interface #
|
||||
##########################
|
||||
|
||||
# Fix the TIEHASH interface (scalar $cgi->Vars) to return and accept
|
||||
# arrayrefs.
|
||||
sub STORE {
|
||||
my $self = shift;
|
||||
my ($param, $value) = @_;
|
||||
if (defined $value and ref $value eq 'ARRAY') {
|
||||
return $self->param(-name => $param, -value => $value);
|
||||
}
|
||||
return $self->SUPER::STORE(@_);
|
||||
}
|
||||
|
||||
sub FETCH {
|
||||
my ($self, $param) = @_;
|
||||
return $self if $param eq 'CGI'; # CGI.pm did this, so we do too.
|
||||
my @result = $self->param($param);
|
||||
return undef if !scalar(@result);
|
||||
return $result[0] if scalar(@result) == 1;
|
||||
return \@result;
|
||||
}
|
||||
|
||||
# For the Vars TIEHASH interface: the normal CGI.pm DELETE doesn't return
|
||||
# the value deleted, but Perl's "delete" expects that value.
|
||||
sub DELETE {
|
||||
my ($self, $param) = @_;
|
||||
my $value = $self->FETCH($param);
|
||||
$self->delete($param);
|
||||
return $value;
|
||||
}
|
||||
|
||||
# cookie() with UTF-8 support...
|
||||
sub cookie
|
||||
{
|
||||
|
@ -518,13 +578,13 @@ effectively removing the cookie.
|
|||
|
||||
As its only argument, it takes the name of the cookie to expire.
|
||||
|
||||
=item C<require_https($baseurl)>
|
||||
=item C<redirect_to_https>
|
||||
|
||||
This routine redirects the client to a different location using the https protocol.
|
||||
If the client is using XMLRPC, it will not retain the QUERY_STRING since XMLRPC uses POST.
|
||||
This routine redirects the client to the https version of the page that
|
||||
they're looking at, using the C<sslbase> parameter for the redirection.
|
||||
|
||||
It takes an optional argument which will be used as the base URL. If $baseurl
|
||||
is not provided, the current URL is used.
|
||||
Generally you should use L<Bugzilla::Util/do_ssl_redirect_if_required>
|
||||
instead of calling this directly.
|
||||
|
||||
=item C<redirect_to_urlbase>
|
||||
|
||||
|
|
|
@ -70,7 +70,7 @@ sub remove_from_db {
|
|||
$dbh->do("UPDATE products SET classification_id = 1
|
||||
WHERE classification_id = ?", undef, $self->id);
|
||||
|
||||
$dbh->do("DELETE FROM classifications WHERE id = ?", undef, $self->id);
|
||||
$self->SUPER::remove_from_db();
|
||||
|
||||
$dbh->bz_commit_transaction();
|
||||
|
||||
|
|
|
@ -0,0 +1,298 @@
|
|||
# -*- 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 James Robson.
|
||||
# Portions created by James Robson are Copyright (c) 2009 James Robson.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Contributor(s): James Robson <arbingersys@gmail.com>
|
||||
|
||||
use strict;
|
||||
|
||||
package Bugzilla::Comment;
|
||||
|
||||
use base qw(Bugzilla::Object);
|
||||
|
||||
use Bugzilla::Attachment;
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::Error;
|
||||
use Bugzilla::User;
|
||||
use Bugzilla::Util;
|
||||
|
||||
###############################
|
||||
#### Initialization ####
|
||||
###############################
|
||||
|
||||
use constant DB_COLUMNS => qw(
|
||||
comment_id
|
||||
bug_id
|
||||
who
|
||||
bug_when
|
||||
work_time
|
||||
thetext
|
||||
isprivate
|
||||
already_wrapped
|
||||
type
|
||||
extra_data
|
||||
);
|
||||
|
||||
use constant UPDATE_COLUMNS => qw(
|
||||
type
|
||||
extra_data
|
||||
);
|
||||
|
||||
use constant DB_TABLE => 'longdescs';
|
||||
use constant ID_FIELD => 'comment_id';
|
||||
use constant LIST_ORDER => 'bug_when';
|
||||
|
||||
use constant VALIDATORS => {
|
||||
type => \&_check_type,
|
||||
};
|
||||
|
||||
use constant UPDATE_VALIDATORS => {
|
||||
extra_data => \&_check_extra_data,
|
||||
};
|
||||
|
||||
#########################
|
||||
# Database Manipulation #
|
||||
#########################
|
||||
|
||||
sub update {
|
||||
my $self = shift;
|
||||
my $changes = $self->SUPER::update(@_);
|
||||
$self->bug->_sync_fulltext();
|
||||
return $changes;
|
||||
}
|
||||
|
||||
# Speeds up displays of comment lists by loading all ->author objects
|
||||
# at once for a whole list.
|
||||
sub preload {
|
||||
my ($class, $comments) = @_;
|
||||
my %user_ids = map { $_->{who} => 1 } @$comments;
|
||||
my $users = Bugzilla::User->new_from_list([keys %user_ids]);
|
||||
my %user_map = map { $_->id => $_ } @$users;
|
||||
foreach my $comment (@$comments) {
|
||||
$comment->{author} = $user_map{$comment->{who}};
|
||||
}
|
||||
}
|
||||
|
||||
###############################
|
||||
#### Accessors ######
|
||||
###############################
|
||||
|
||||
sub already_wrapped { return $_[0]->{'already_wrapped'}; }
|
||||
sub body { return $_[0]->{'thetext'}; }
|
||||
sub bug_id { return $_[0]->{'bug_id'}; }
|
||||
sub creation_ts { return $_[0]->{'bug_when'}; }
|
||||
sub is_private { return $_[0]->{'isprivate'}; }
|
||||
sub work_time { return $_[0]->{'work_time'}; }
|
||||
sub type { return $_[0]->{'type'}; }
|
||||
sub extra_data { return $_[0]->{'extra_data'} }
|
||||
|
||||
sub bug {
|
||||
my $self = shift;
|
||||
require Bugzilla::Bug;
|
||||
$self->{bug} ||= new Bugzilla::Bug($self->bug_id);
|
||||
return $self->{bug};
|
||||
}
|
||||
|
||||
sub is_about_attachment {
|
||||
my ($self) = @_;
|
||||
return 1 if ($self->type == CMT_ATTACHMENT_CREATED
|
||||
or $self->type == CMT_ATTACHMENT_UPDATED);
|
||||
return 0;
|
||||
}
|
||||
|
||||
sub attachment {
|
||||
my ($self) = @_;
|
||||
return undef if not $self->is_about_attachment;
|
||||
$self->{attachment} ||= new Bugzilla::Attachment($self->extra_data);
|
||||
return $self->{attachment};
|
||||
}
|
||||
|
||||
sub author {
|
||||
my $self = shift;
|
||||
$self->{'author'} ||= new Bugzilla::User($self->{'who'});
|
||||
return $self->{'author'};
|
||||
}
|
||||
|
||||
sub body_full {
|
||||
my ($self, $params) = @_;
|
||||
$params ||= {};
|
||||
my $template = Bugzilla->template_inner;
|
||||
my $body;
|
||||
if ($self->type) {
|
||||
$template->process("bug/format_comment.txt.tmpl",
|
||||
{ comment => $self, %$params }, \$body)
|
||||
|| ThrowTemplateError($template->error());
|
||||
$body =~ s/^X//;
|
||||
}
|
||||
else {
|
||||
$body = $self->body;
|
||||
}
|
||||
if ($params->{wrap} and !$self->already_wrapped) {
|
||||
$body = wrap_comment($body);
|
||||
}
|
||||
return $body;
|
||||
}
|
||||
|
||||
############
|
||||
# Mutators #
|
||||
############
|
||||
|
||||
sub set_extra_data { $_[0]->set('extra_data', $_[1]); }
|
||||
|
||||
sub set_type {
|
||||
my ($self, $type, $extra_data) = @_;
|
||||
$self->set('type', $type);
|
||||
$self->set_extra_data($extra_data);
|
||||
}
|
||||
|
||||
##############
|
||||
# Validators #
|
||||
##############
|
||||
|
||||
sub _check_extra_data {
|
||||
my ($invocant, $extra_data, $type) = @_;
|
||||
$type = $invocant->type if ref $invocant;
|
||||
if ($type == CMT_NORMAL or $type == CMT_POPULAR_VOTES) {
|
||||
if (defined $extra_data) {
|
||||
ThrowCodeError('comment_extra_data_not_allowed',
|
||||
{ type => $type, extra_data => $extra_data });
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!defined $extra_data) {
|
||||
ThrowCodeError('comment_extra_data_required', { type => $type });
|
||||
}
|
||||
if ($type == CMT_MOVED_TO) {
|
||||
$extra_data = Bugzilla::User->check($extra_data)->login;
|
||||
}
|
||||
elsif ($type == CMT_ATTACHMENT_CREATED
|
||||
or $type == CMT_ATTACHMENT_UPDATED)
|
||||
{
|
||||
my $attachment = Bugzilla::Attachment->check({
|
||||
id => $extra_data });
|
||||
$extra_data = $attachment->id;
|
||||
}
|
||||
else {
|
||||
my $original = $extra_data;
|
||||
detaint_natural($extra_data)
|
||||
or ThrowCodeError('comment_extra_data_not_numeric',
|
||||
{ type => $type, extra_data => $original });
|
||||
}
|
||||
}
|
||||
|
||||
return $extra_data;
|
||||
}
|
||||
|
||||
sub _check_type {
|
||||
my ($invocant, $type) = @_;
|
||||
$type ||= CMT_NORMAL;
|
||||
my $original = $type;
|
||||
detaint_natural($type)
|
||||
or ThrowCodeError('comment_type_invalid', { type => $original });
|
||||
return $type;
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::Comment - A Comment for a given bug
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
use Bugzilla::Comment;
|
||||
|
||||
my $comment = Bugzilla::Comment->new($comment_id);
|
||||
my $comments = Bugzilla::Comment->new_from_list($comment_ids);
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
Bugzilla::Comment represents a comment attached to a bug.
|
||||
|
||||
This implements all standard C<Bugzilla::Object> methods. See
|
||||
L<Bugzilla::Object> for more details.
|
||||
|
||||
=head2 Accessors
|
||||
|
||||
=over
|
||||
|
||||
=item C<bug_id>
|
||||
|
||||
C<int> The ID of the bug to which the comment belongs.
|
||||
|
||||
=item C<creation_ts>
|
||||
|
||||
C<string> The comment creation timestamp.
|
||||
|
||||
=item C<body>
|
||||
|
||||
C<string> The body without any special additional text.
|
||||
|
||||
=item C<work_time>
|
||||
|
||||
C<string> Time spent as related to this comment.
|
||||
|
||||
=item C<is_private>
|
||||
|
||||
C<boolean> Comment is marked as private
|
||||
|
||||
=item C<already_wrapped>
|
||||
|
||||
If this comment is stored in the database word-wrapped, this will be C<1>.
|
||||
C<0> otherwise.
|
||||
|
||||
=item C<author>
|
||||
|
||||
L<Bugzilla::User> who created the comment.
|
||||
|
||||
=item C<body_full>
|
||||
|
||||
=over
|
||||
|
||||
=item B<Description>
|
||||
|
||||
C<string> Body of the comment, including any special text (such as
|
||||
"this bug was marked as a duplicate of...").
|
||||
|
||||
=item B<Params>
|
||||
|
||||
=over
|
||||
|
||||
=item C<is_bugmail>
|
||||
|
||||
C<boolean>. C<1> if this comment should be formatted specifically for
|
||||
bugmail.
|
||||
|
||||
=item C<wrap>
|
||||
|
||||
C<boolean>. C<1> if the comment should be returned word-wrapped.
|
||||
|
||||
=back
|
||||
|
||||
=item B<Returns>
|
||||
|
||||
A string, the full text of the comment as it would be displayed to an end-user.
|
||||
|
||||
=back
|
||||
|
||||
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
|
@ -65,6 +65,7 @@ use constant UPDATE_COLUMNS => qw(
|
|||
);
|
||||
|
||||
use constant VALIDATORS => {
|
||||
create_series => \&Bugzilla::Object::check_boolean,
|
||||
product => \&_check_product,
|
||||
initialowner => \&_check_initialowner,
|
||||
initialqacontact => \&_check_initialqacontact,
|
||||
|
@ -120,14 +121,15 @@ sub create {
|
|||
$class->check_required_create_fields(@_);
|
||||
my $params = $class->run_create_validators(@_);
|
||||
my $cc_list = delete $params->{initial_cc};
|
||||
my $create_series = delete $params->{create_series};
|
||||
|
||||
my $component = $class->insert_create_data($params);
|
||||
|
||||
# We still have to fill the component_cc table.
|
||||
$component->_update_cc_list($cc_list);
|
||||
$component->_update_cc_list($cc_list) if $cc_list;
|
||||
|
||||
# Create series for the new component.
|
||||
$component->_create_series();
|
||||
$component->_create_series() if $create_series;
|
||||
|
||||
$dbh->bz_commit_transaction();
|
||||
return $component;
|
||||
|
|
|
@ -35,6 +35,7 @@ use strict;
|
|||
use base qw(Exporter);
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::Hook;
|
||||
use Bugzilla::Install::Filesystem qw(fix_file_permissions);
|
||||
use Data::Dumper;
|
||||
use File::Temp;
|
||||
|
||||
|
@ -68,7 +69,7 @@ sub _load_params {
|
|||
}
|
||||
# This hook is also called in editparams.cgi. This call here is required
|
||||
# to make SetParam work.
|
||||
Bugzilla::Hook::process('config-modify_panels',
|
||||
Bugzilla::Hook::process('config_modify_panels',
|
||||
{ panels => \%hook_panels });
|
||||
}
|
||||
# END INIT CODE
|
||||
|
@ -84,7 +85,7 @@ sub param_panels {
|
|||
$param_panels->{$module} = "Bugzilla::Config::$module" unless $module eq 'Common';
|
||||
}
|
||||
# Now check for any hooked params
|
||||
Bugzilla::Hook::process('config-add_panels',
|
||||
Bugzilla::Hook::process('config_add_panels',
|
||||
{ panel_modules => $param_panels });
|
||||
return $param_panels;
|
||||
}
|
||||
|
@ -151,10 +152,6 @@ sub update_params {
|
|||
{
|
||||
$param->{'makeproductgroups'} = $param->{'usebuggroups'};
|
||||
}
|
||||
if (exists $param->{'usebuggroupsentry'}
|
||||
&& !exists $param->{'useentrygroupdefault'}) {
|
||||
$param->{'useentrygroupdefault'} = $param->{'usebuggroupsentry'};
|
||||
}
|
||||
|
||||
# Modularise auth code
|
||||
if (exists $param->{'useLDAP'} && !exists $param->{'loginmethod'}) {
|
||||
|
@ -196,6 +193,13 @@ sub update_params {
|
|||
$param->{'mail_delivery_method'} = $translation{$method};
|
||||
}
|
||||
|
||||
# Convert the old "ssl" parameter to the new "ssl_redirect" parameter.
|
||||
# Both "authenticated sessions" and "always" turn on "ssl_redirect"
|
||||
# when upgrading.
|
||||
if (exists $param->{'ssl'} and $param->{'ssl'} ne 'never') {
|
||||
$param->{'ssl_redirect'} = 1;
|
||||
}
|
||||
|
||||
# --- DEFAULTS FOR NEW PARAMS ---
|
||||
|
||||
_load_params unless %params;
|
||||
|
@ -203,7 +207,12 @@ sub update_params {
|
|||
my $name = $item->{'name'};
|
||||
unless (exists $param->{$name}) {
|
||||
print "New parameter: $name\n" unless $new_install;
|
||||
$param->{$name} = $answer->{$name} || $item->{'default'};
|
||||
if (exists $answer->{$name}) {
|
||||
$param->{$name} = $answer->{$name};
|
||||
}
|
||||
else {
|
||||
$param->{$name} = $item->{'default'};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -293,29 +302,13 @@ sub write_params {
|
|||
rename $tmpname, $param_file
|
||||
or die "Can't rename $tmpname to $param_file: $!";
|
||||
|
||||
ChmodDataFile($param_file, 0666);
|
||||
fix_file_permissions($param_file);
|
||||
|
||||
# And now we have to reset the params cache so that Bugzilla will re-read
|
||||
# them.
|
||||
delete Bugzilla->request_cache->{params};
|
||||
}
|
||||
|
||||
# Some files in the data directory must be world readable if and only if
|
||||
# we don't have a webserver group. Call this function to do this.
|
||||
# This will become a private function once all the datafile handling stuff
|
||||
# moves into this package
|
||||
|
||||
# This sub is not perldoc'd for that reason - noone should know about it
|
||||
sub ChmodDataFile {
|
||||
my ($file, $mask) = @_;
|
||||
my $perm = 0770;
|
||||
if ((stat(bz_locations()->{'datadir'}))[2] & 0002) {
|
||||
$perm = 0777;
|
||||
}
|
||||
$perm = $perm & $mask;
|
||||
chmod $perm,$file;
|
||||
}
|
||||
|
||||
sub read_param_file {
|
||||
my %params;
|
||||
my $datadir = bz_locations()->{'datadir'};
|
||||
|
|
|
@ -35,7 +35,7 @@ use strict;
|
|||
|
||||
use Bugzilla::Config::Common;
|
||||
|
||||
$Bugzilla::Config::Admin::sortkey = "01";
|
||||
our $sortkey = 200;
|
||||
|
||||
sub get_param_list {
|
||||
my $class = shift;
|
||||
|
|
|
@ -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 Netscape Communications
|
||||
# Corporation. Portions created by Netscape are
|
||||
# Copyright (C) 1998 Netscape Communications Corporation. All
|
||||
# Rights Reserved.
|
||||
#
|
||||
# Contributor(s): Terry Weissman <terry@mozilla.org>
|
||||
# Dawn Endico <endico@mozilla.org>
|
||||
# Dan Mosedale <dmose@mozilla.org>
|
||||
# Joe Robins <jmrobins@tgix.com>
|
||||
# Jacob Steenhagen <jake@bugzilla.org>
|
||||
# J. Paul Reed <preed@sigkill.com>
|
||||
# Bradley Baetz <bbaetz@student.usyd.edu.au>
|
||||
# Joseph Heenan <joseph@heenan.me.uk>
|
||||
# Erik Stambaugh <erik@dasbistro.com>
|
||||
# Frédéric Buclin <LpSolit@gmail.com>
|
||||
# Max Kanat-Alexander <mkanat@bugzilla.org>
|
||||
|
||||
package Bugzilla::Config::Advanced;
|
||||
use strict;
|
||||
|
||||
our $sortkey = 1700;
|
||||
|
||||
use constant get_param_list => (
|
||||
{
|
||||
name => 'cookiedomain',
|
||||
type => 't',
|
||||
default => ''
|
||||
},
|
||||
|
||||
{
|
||||
name => 'inbound_proxies',
|
||||
type => 't',
|
||||
default => ''
|
||||
},
|
||||
|
||||
{
|
||||
name => 'proxy_url',
|
||||
type => 't',
|
||||
default => ''
|
||||
},
|
||||
);
|
||||
|
||||
1;
|
|
@ -35,12 +35,12 @@ use strict;
|
|||
|
||||
use Bugzilla::Config::Common;
|
||||
|
||||
$Bugzilla::Config::Attachment::sortkey = "025";
|
||||
our $sortkey = 400;
|
||||
|
||||
sub get_param_list {
|
||||
my $class = shift;
|
||||
my @param_list = (
|
||||
{
|
||||
my $class = shift;
|
||||
my @param_list = (
|
||||
{
|
||||
name => 'allow_attachment_display',
|
||||
type => 'b',
|
||||
default => 0
|
||||
|
@ -54,65 +54,56 @@ sub get_param_list {
|
|||
},
|
||||
|
||||
{
|
||||
name => 'allow_attachment_deletion',
|
||||
type => 'b',
|
||||
default => 0
|
||||
},
|
||||
name => 'allow_attachment_deletion',
|
||||
type => 'b',
|
||||
default => 0
|
||||
},
|
||||
{
|
||||
name => 'allow_attach_url',
|
||||
type => 'b',
|
||||
default => 0
|
||||
},
|
||||
|
||||
{
|
||||
name => 'allow_attach_url',
|
||||
type => 'b',
|
||||
default => 0
|
||||
},
|
||||
{
|
||||
name => 'maxattachmentsize',
|
||||
type => 't',
|
||||
default => '1000',
|
||||
checker => \&check_maxattachmentsize
|
||||
},
|
||||
|
||||
{
|
||||
name => 'maxattachmentsize',
|
||||
type => 't',
|
||||
default => '1000',
|
||||
checker => \&check_maxattachmentsize
|
||||
},
|
||||
{
|
||||
name => 'inline_attachment_mime',
|
||||
type => 't',
|
||||
default => '^text/|^image/',
|
||||
},
|
||||
|
||||
# The maximum size (in bytes) for patches and non-patch attachments.
|
||||
# The default limit is 1000KB, which is 24KB less than mysql's default
|
||||
# maximum packet size (which determines how much data can be sent in a
|
||||
# single mysql packet and thus how much data can be inserted into the
|
||||
# database) to provide breathing space for the data in other fields of
|
||||
# the attachment record as well as any mysql packet overhead (I don't
|
||||
# know of any, but I suspect there may be some.)
|
||||
{
|
||||
name => 'mime_types_file',
|
||||
type => 't',
|
||||
default => '',
|
||||
},
|
||||
|
||||
{
|
||||
name => 'maxlocalattachment',
|
||||
type => 't',
|
||||
default => '0',
|
||||
checker => \&check_numeric
|
||||
},
|
||||
{
|
||||
name => 'force_attach_bigfile',
|
||||
type => 'b',
|
||||
default => 0,
|
||||
},
|
||||
|
||||
{
|
||||
name => 'convert_uncompressed_images',
|
||||
type => 'b',
|
||||
default => 0,
|
||||
checker => \&check_image_converter
|
||||
},
|
||||
# The maximum size (in bytes) for patches and non-patch attachments.
|
||||
# The default limit is 1000KB, which is 24KB less than mysql's default
|
||||
# maximum packet size (which determines how much data can be sent in a
|
||||
# single mysql packet and thus how much data can be inserted into the
|
||||
# database) to provide breathing space for the data in other fields of
|
||||
# the attachment record as well as any mysql packet overhead (I don't
|
||||
# know of any, but I suspect there may be some.)
|
||||
|
||||
{
|
||||
name => 'inline_attachment_mime',
|
||||
type => 't',
|
||||
default => '^text/|^image/',
|
||||
},
|
||||
|
||||
{
|
||||
name => 'mime_types_file',
|
||||
type => 't',
|
||||
default => '',
|
||||
},
|
||||
|
||||
{
|
||||
name => 'force_attach_bigfile',
|
||||
type => 'b',
|
||||
default => 0,
|
||||
},
|
||||
);
|
||||
return @param_list;
|
||||
{
|
||||
name => 'maxlocalattachment',
|
||||
type => 't',
|
||||
default => '0',
|
||||
checker => \&check_numeric
|
||||
} );
|
||||
return @param_list;
|
||||
}
|
||||
|
||||
1;
|
||||
|
|
|
@ -35,7 +35,7 @@ use strict;
|
|||
|
||||
use Bugzilla::Config::Common;
|
||||
|
||||
$Bugzilla::Config::Auth::sortkey = "02";
|
||||
our $sortkey = 300;
|
||||
|
||||
sub get_param_list {
|
||||
my $class = shift;
|
||||
|
@ -90,13 +90,6 @@ sub get_param_list {
|
|||
checker => \&check_multi
|
||||
},
|
||||
|
||||
{
|
||||
name => 'loginnetmask',
|
||||
type => 't',
|
||||
default => '0',
|
||||
checker => \&check_netmask
|
||||
},
|
||||
|
||||
{
|
||||
name => 'requirelogin',
|
||||
type => 'b',
|
||||
|
|
|
@ -36,7 +36,7 @@ use strict;
|
|||
use Bugzilla::Config::Common;
|
||||
use Bugzilla::Status;
|
||||
|
||||
$Bugzilla::Config::BugChange::sortkey = "03";
|
||||
our $sortkey = 500;
|
||||
|
||||
sub get_param_list {
|
||||
my $class = shift;
|
||||
|
|
|
@ -36,7 +36,7 @@ use strict;
|
|||
use Bugzilla::Config::Common;
|
||||
use Bugzilla::Field;
|
||||
|
||||
$Bugzilla::Config::BugFields::sortkey = "04";
|
||||
our $sortkey = 600;
|
||||
|
||||
sub get_param_list {
|
||||
my $class = shift;
|
||||
|
|
|
@ -35,7 +35,7 @@ use strict;
|
|||
|
||||
use Bugzilla::Config::Common;
|
||||
|
||||
$Bugzilla::Config::BugMove::sortkey = "05";
|
||||
our $sortkey = 700;
|
||||
|
||||
sub get_param_list {
|
||||
my $class = shift;
|
||||
|
|
|
@ -34,6 +34,7 @@ package Bugzilla::Config::Common;
|
|||
|
||||
use strict;
|
||||
|
||||
use Email::Address;
|
||||
use Socket;
|
||||
|
||||
use Bugzilla::Util;
|
||||
|
@ -47,10 +48,10 @@ use base qw(Exporter);
|
|||
qw(check_multi check_numeric check_regexp check_url check_group
|
||||
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_user_verify_class
|
||||
check_mail_delivery_method check_notification check_utf8
|
||||
check_bug_status check_smtp_auth check_theschwartz_available
|
||||
check_maxattachmentsize
|
||||
check_maxattachmentsize check_email
|
||||
);
|
||||
|
||||
# Checking functions for the various values
|
||||
|
@ -94,6 +95,14 @@ sub check_regexp {
|
|||
return $@;
|
||||
}
|
||||
|
||||
sub check_email {
|
||||
my ($value) = @_;
|
||||
if ($value !~ $Email::Address::mailbox) {
|
||||
return "must be a valid email address.";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
sub check_sslbase {
|
||||
my $url = shift;
|
||||
if ($url ne '') {
|
||||
|
@ -248,21 +257,6 @@ sub check_webdotbase {
|
|||
return "";
|
||||
}
|
||||
|
||||
sub check_netmask {
|
||||
my ($mask) = @_;
|
||||
my $res = check_numeric($mask);
|
||||
return $res if $res;
|
||||
if ($mask < 0 || $mask > 32) {
|
||||
return "an IPv4 netmask must be between 0 and 32 bits";
|
||||
}
|
||||
# Note that if we changed the netmask from anything apart from 32, then
|
||||
# existing logincookies which aren't for a single IP won't work
|
||||
# any more. We can't know which ones they are, though, so they'll just
|
||||
# take space until they're periodically cleared, later.
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
sub check_user_verify_class {
|
||||
# doeditparams traverses the list of params, and for each one it checks,
|
||||
# then updates. This means that if one param checker wants to look at
|
||||
|
@ -272,41 +266,39 @@ sub check_user_verify_class {
|
|||
# the login method as LDAP, we won't notice, but all logins will fail.
|
||||
# So don't do that.
|
||||
|
||||
my $params = Bugzilla->params;
|
||||
my ($list, $entry) = @_;
|
||||
$list || return 'You need to specify at least one authentication mechanism';
|
||||
for my $class (split /,\s*/, $list) {
|
||||
my $res = check_multi($class, $entry);
|
||||
return $res if $res;
|
||||
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"};
|
||||
return "RADIUS_secret is empty" unless Bugzilla->params->{"RADIUS_secret"};
|
||||
if (!Bugzilla->feature('auth_radius')) {
|
||||
return "RADIUS support is not available. Run checksetup.pl"
|
||||
. " for more details";
|
||||
}
|
||||
return "RADIUS servername (RADIUS_server) is missing"
|
||||
if !$params->{"RADIUS_server"};
|
||||
return "RADIUS_secret is empty" if !$params->{"RADIUS_secret"};
|
||||
}
|
||||
elsif ($class eq 'LDAP') {
|
||||
eval "require Net::LDAP";
|
||||
return "Error requiring Net::LDAP: '$@'" if $@;
|
||||
return "LDAP servername (LDAPserver) is missing" unless Bugzilla->params->{"LDAPserver"};
|
||||
return "LDAPBaseDN is empty" unless Bugzilla->params->{"LDAPBaseDN"};
|
||||
if (!Bugzilla->feature('auth_ldap')) {
|
||||
return "LDAP support is not available. Run checksetup.pl"
|
||||
. " for more details";
|
||||
}
|
||||
return "LDAP servername (LDAPserver) is missing"
|
||||
if !$params->{"LDAPserver"};
|
||||
return "LDAPBaseDN is empty" if !$params->{"LDAPBaseDN"};
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
sub check_image_converter {
|
||||
my ($value, $hash) = @_;
|
||||
if ($value == 1){
|
||||
eval "require Image::Magick";
|
||||
return "Error requiring Image::Magick: '$@'" if $@;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
sub check_mail_delivery_method {
|
||||
my $check = check_multi(@_);
|
||||
return $check if $check;
|
||||
my $mailer = shift;
|
||||
if ($mailer eq 'sendmail' && $^O =~ /MSWin32/i) {
|
||||
if ($mailer eq 'sendmail' and ON_WINDOWS) {
|
||||
# look for sendmail.exe
|
||||
return "Failed to locate " . SENDMAIL_EXE
|
||||
unless -e SENDMAIL_EXE;
|
||||
|
@ -342,20 +334,25 @@ sub check_notification {
|
|||
"about the next stable release, you should select " .
|
||||
"'latest_stable_release' instead";
|
||||
}
|
||||
if ($option ne 'disabled' && !Bugzilla->feature('updates')) {
|
||||
return "Some Perl modules are missing to get notifications about " .
|
||||
"new releases. See the output of checksetup.pl for more information";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
sub check_smtp_auth {
|
||||
my $username = shift;
|
||||
if ($username) {
|
||||
eval "require Authen::SASL";
|
||||
return "Error requiring Authen::SASL: '$@'" if $@;
|
||||
if ($username and !Bugzilla->feature('smtp_auth')) {
|
||||
return "SMTP Authentication is not available. Run checksetup.pl for"
|
||||
. " more details";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
sub check_theschwartz_available {
|
||||
if (!eval { require TheSchwartz; require Daemon::Generic; }) {
|
||||
my $use_queue = shift;
|
||||
if ($use_queue && !Bugzilla->feature('jobqueue')) {
|
||||
return "Using the job queue requires that you have certain Perl"
|
||||
. " modules installed. See the output of checksetup.pl"
|
||||
. " for more information";
|
||||
|
|
|
@ -35,17 +35,9 @@ use strict;
|
|||
|
||||
use Bugzilla::Config::Common;
|
||||
|
||||
$Bugzilla::Config::Core::sortkey = "00";
|
||||
|
||||
sub get_param_list {
|
||||
my $class = shift;
|
||||
my @param_list = (
|
||||
{
|
||||
name => 'maintainer',
|
||||
type => 't',
|
||||
default => 'THE MAINTAINER HAS NOT YET BEEN SET'
|
||||
},
|
||||
our $sortkey = 100;
|
||||
|
||||
use constant get_param_list => (
|
||||
{
|
||||
name => 'error_log',
|
||||
type => 't',
|
||||
|
@ -72,10 +64,9 @@ sub get_param_list {
|
|||
},
|
||||
|
||||
{
|
||||
name => 'docs_urlbase',
|
||||
type => 't',
|
||||
default => 'docs/%lang%/html/',
|
||||
checker => \&check_url
|
||||
name => 'ssl_redirect',
|
||||
type => 'b',
|
||||
default => 0
|
||||
},
|
||||
|
||||
{
|
||||
|
@ -85,59 +76,11 @@ sub get_param_list {
|
|||
checker => \&check_sslbase
|
||||
},
|
||||
|
||||
{
|
||||
name => 'ssl',
|
||||
type => 's',
|
||||
choices => ['never', 'authenticated sessions', 'always'],
|
||||
default => 'never'
|
||||
},
|
||||
|
||||
{
|
||||
name => 'cookiedomain',
|
||||
type => 't',
|
||||
default => ''
|
||||
},
|
||||
|
||||
{
|
||||
name => 'cookiepath',
|
||||
type => 't',
|
||||
default => '/'
|
||||
},
|
||||
|
||||
{
|
||||
name => 'utf8',
|
||||
type => 'b',
|
||||
default => '0',
|
||||
checker => \&check_utf8
|
||||
},
|
||||
|
||||
{
|
||||
name => 'shutdownhtml',
|
||||
type => 'l',
|
||||
default => ''
|
||||
},
|
||||
|
||||
{
|
||||
name => 'announcehtml',
|
||||
type => 'l',
|
||||
default => ''
|
||||
},
|
||||
|
||||
{
|
||||
name => 'proxy_url',
|
||||
type => 't',
|
||||
default => ''
|
||||
},
|
||||
|
||||
{
|
||||
name => 'upgrade_notification',
|
||||
type => 's',
|
||||
choices => ['development_snapshot', 'latest_stable_release',
|
||||
'stable_branch_release', 'disabled'],
|
||||
default => 'latest_stable_release',
|
||||
checker => \&check_notification
|
||||
} );
|
||||
return @param_list;
|
||||
}
|
||||
);
|
||||
|
||||
1;
|
||||
|
|
|
@ -35,7 +35,7 @@ use strict;
|
|||
|
||||
use Bugzilla::Config::Common;
|
||||
|
||||
$Bugzilla::Config::DependencyGraph::sortkey = "06";
|
||||
our $sortkey = 800;
|
||||
|
||||
sub get_param_list {
|
||||
my $class = shift;
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
# -*- Mode: perl; indent-tabs-mode: nil -*-
|
||||
#
|
||||
# The contents of this file are subject to the Mozilla Public
|
||||
# License Version 1.1 (the "License"); you may not use this file
|
||||
# except in compliance with the License. You may obtain a copy of
|
||||
# the License at http://www.mozilla.org/MPL/
|
||||
#
|
||||
# Software distributed under the License is distributed on an "AS
|
||||
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
|
||||
# implied. See the License for the specific language governing
|
||||
# rights and limitations under the License.
|
||||
#
|
||||
# The Original Code is the Bugzilla Bug Tracking System.
|
||||
#
|
||||
# The Initial Developer of the Original Code is Netscape Communications
|
||||
# Corporation. Portions created by Netscape are
|
||||
# Copyright (C) 1998 Netscape Communications Corporation. All
|
||||
# Rights Reserved.
|
||||
#
|
||||
# Contributor(s): Terry Weissman <terry@mozilla.org>
|
||||
# Dawn Endico <endico@mozilla.org>
|
||||
# Dan Mosedale <dmose@mozilla.org>
|
||||
# Joe Robins <jmrobins@tgix.com>
|
||||
# Jacob Steenhagen <jake@bugzilla.org>
|
||||
# J. Paul Reed <preed@sigkill.com>
|
||||
# Bradley Baetz <bbaetz@student.usyd.edu.au>
|
||||
# Joseph Heenan <joseph@heenan.me.uk>
|
||||
# Erik Stambaugh <erik@dasbistro.com>
|
||||
# Frédéric Buclin <LpSolit@gmail.com>
|
||||
# Max Kanat-Alexander <mkanat@bugzilla.org>
|
||||
|
||||
package Bugzilla::Config::General;
|
||||
use strict;
|
||||
use Bugzilla::Config::Common;
|
||||
|
||||
our $sortkey = 150;
|
||||
|
||||
use constant get_param_list => (
|
||||
{
|
||||
name => 'maintainer',
|
||||
type => 't',
|
||||
no_reset => '1',
|
||||
default => '',
|
||||
checker => \&check_email
|
||||
},
|
||||
|
||||
{
|
||||
name => 'docs_urlbase',
|
||||
type => 't',
|
||||
default => 'docs/%lang%/html/',
|
||||
checker => \&check_url
|
||||
},
|
||||
|
||||
{
|
||||
name => 'utf8',
|
||||
type => 'b',
|
||||
default => '0',
|
||||
checker => \&check_utf8
|
||||
},
|
||||
|
||||
{
|
||||
name => 'shutdownhtml',
|
||||
type => 'l',
|
||||
default => ''
|
||||
},
|
||||
|
||||
{
|
||||
name => 'announcehtml',
|
||||
type => 'l',
|
||||
default => ''
|
||||
},
|
||||
|
||||
{
|
||||
name => 'upgrade_notification',
|
||||
type => 's',
|
||||
choices => ['development_snapshot', 'latest_stable_release',
|
||||
'stable_branch_release', 'disabled'],
|
||||
default => 'latest_stable_release',
|
||||
checker => \&check_notification
|
||||
},
|
||||
);
|
||||
|
||||
1;
|
|
@ -36,7 +36,7 @@ use strict;
|
|||
use Bugzilla::Config::Common;
|
||||
use Bugzilla::Group;
|
||||
|
||||
$Bugzilla::Config::GroupSecurity::sortkey = "07";
|
||||
our $sortkey = 900;
|
||||
|
||||
sub get_param_list {
|
||||
my $class = shift;
|
||||
|
@ -48,12 +48,6 @@ sub get_param_list {
|
|||
default => 0
|
||||
},
|
||||
|
||||
{
|
||||
name => 'useentrygroupdefault',
|
||||
type => 'b',
|
||||
default => 0
|
||||
},
|
||||
|
||||
{
|
||||
name => 'chartgroup',
|
||||
type => 's',
|
||||
|
|
|
@ -35,7 +35,7 @@ use strict;
|
|||
|
||||
use Bugzilla::Config::Common;
|
||||
|
||||
$Bugzilla::Config::LDAP::sortkey = "09";
|
||||
our $sortkey = 1000;
|
||||
|
||||
sub get_param_list {
|
||||
my $class = shift;
|
||||
|
|
|
@ -36,7 +36,7 @@ use strict;
|
|||
use Bugzilla::Config::Common;
|
||||
use Email::Send;
|
||||
|
||||
$Bugzilla::Config::MTA::sortkey = "10";
|
||||
our $sortkey = 1200;
|
||||
|
||||
sub get_param_list {
|
||||
my $class = shift;
|
||||
|
|
|
@ -35,7 +35,7 @@ use strict;
|
|||
|
||||
use Bugzilla::Config::Common;
|
||||
|
||||
$Bugzilla::Config::PatchViewer::sortkey = "11";
|
||||
our $sortkey = 1300;
|
||||
|
||||
sub get_param_list {
|
||||
my $class = shift;
|
||||
|
|
|
@ -35,7 +35,7 @@ use strict;
|
|||
|
||||
use Bugzilla::Config::Common;
|
||||
|
||||
$Bugzilla::Config::Query::sortkey = "12";
|
||||
our $sortkey = 1400;
|
||||
|
||||
sub get_param_list {
|
||||
my $class = shift;
|
||||
|
@ -67,13 +67,6 @@ sub get_param_list {
|
|||
default => 'bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&emailassigned_to1=1&emailassigned_to2=1&emailreporter2=1&emailcc2=1&emailqa_contact2=1&order=Importance&long_desc_type=substring'
|
||||
},
|
||||
|
||||
{
|
||||
name => 'quicksearch_comment_cutoff',
|
||||
type => 't',
|
||||
default => '4',
|
||||
checker => \&check_numeric
|
||||
},
|
||||
|
||||
{
|
||||
name => 'specific_search_allow_empty_words',
|
||||
type => 'b',
|
||||
|
|
|
@ -25,7 +25,7 @@ use strict;
|
|||
|
||||
use Bugzilla::Config::Common;
|
||||
|
||||
$Bugzilla::Config::RADIUS::sortkey = "09";
|
||||
our $sortkey = 1100;
|
||||
|
||||
sub get_param_list {
|
||||
my $class = shift;
|
||||
|
|
|
@ -35,7 +35,7 @@ use strict;
|
|||
|
||||
use Bugzilla::Config::Common;
|
||||
|
||||
$Bugzilla::Config::ShadowDB::sortkey = "13";
|
||||
our $sortkey = 1500;
|
||||
|
||||
sub get_param_list {
|
||||
my $class = shift;
|
||||
|
|
|
@ -35,7 +35,7 @@ use strict;
|
|||
|
||||
use Bugzilla::Config::Common;
|
||||
|
||||
$Bugzilla::Config::UserMatch::sortkey = "14";
|
||||
our $sortkey = 1600;
|
||||
|
||||
sub get_param_list {
|
||||
my $class = shift;
|
||||
|
@ -46,13 +46,6 @@ sub get_param_list {
|
|||
default => '0'
|
||||
},
|
||||
|
||||
{
|
||||
name => 'usermatchmode',
|
||||
type => 's',
|
||||
choices => ['off', 'wildcard', 'search'],
|
||||
default => 'off'
|
||||
},
|
||||
|
||||
{
|
||||
name => 'maxusermatches',
|
||||
type => 't',
|
||||
|
|
|
@ -55,9 +55,9 @@ use File::Basename;
|
|||
AUTH_LOGINFAILED
|
||||
AUTH_DISABLED
|
||||
AUTH_NO_SUCH_USER
|
||||
AUTH_LOCKOUT
|
||||
|
||||
USER_PASSWORD_MIN_LENGTH
|
||||
USER_PASSWORD_MAX_LENGTH
|
||||
|
||||
LOGIN_OPTIONAL
|
||||
LOGIN_NORMAL
|
||||
|
@ -79,6 +79,7 @@ use File::Basename;
|
|||
|
||||
DEFAULT_COLUMN_LIST
|
||||
DEFAULT_QUERY_NAME
|
||||
DEFAULT_MILESTONE
|
||||
|
||||
QUERY_LIST
|
||||
LIST_OF_BUGS
|
||||
|
@ -92,6 +93,8 @@ use File::Basename;
|
|||
CMT_HAS_DUPE
|
||||
CMT_POPULAR_VOTES
|
||||
CMT_MOVED_TO
|
||||
CMT_ATTACHMENT_CREATED
|
||||
CMT_ATTACHMENT_UPDATED
|
||||
|
||||
THROW_ERROR
|
||||
|
||||
|
@ -127,14 +130,18 @@ use File::Basename;
|
|||
FIELD_TYPE_BUG_ID
|
||||
FIELD_TYPE_BUG_URLS
|
||||
|
||||
TIMETRACKING_FIELDS
|
||||
|
||||
USAGE_MODE_BROWSER
|
||||
USAGE_MODE_CMDLINE
|
||||
USAGE_MODE_WEBSERVICE
|
||||
USAGE_MODE_XMLRPC
|
||||
USAGE_MODE_EMAIL
|
||||
USAGE_MODE_JSON
|
||||
|
||||
ERROR_MODE_WEBPAGE
|
||||
ERROR_MODE_DIE
|
||||
ERROR_MODE_DIE_SOAP_FAULT
|
||||
ERROR_MODE_JSON_RPC
|
||||
ERROR_MODE_AJAX
|
||||
|
||||
INSTALLATION_MODE_INTERACTIVE
|
||||
|
@ -146,8 +153,11 @@ use File::Basename;
|
|||
|
||||
MAX_TOKEN_AGE
|
||||
MAX_LOGINCOOKIE_AGE
|
||||
MAX_LOGIN_ATTEMPTS
|
||||
LOGIN_LOCKOUT_INTERVAL
|
||||
|
||||
SAFE_PROTOCOLS
|
||||
LEGAL_CONTENT_TYPES
|
||||
|
||||
MIN_SMALLINT
|
||||
MAX_SMALLINT
|
||||
|
@ -172,7 +182,7 @@ use File::Basename;
|
|||
# CONSTANTS
|
||||
#
|
||||
# Bugzilla version
|
||||
use constant BUGZILLA_VERSION => "3.4.6";
|
||||
use constant BUGZILLA_VERSION => "3.6";
|
||||
|
||||
# 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
|
||||
|
@ -225,10 +235,10 @@ use constant AUTH_ERROR => 2;
|
|||
use constant AUTH_LOGINFAILED => 3;
|
||||
use constant AUTH_DISABLED => 4;
|
||||
use constant AUTH_NO_SUCH_USER => 5;
|
||||
use constant AUTH_LOCKOUT => 6;
|
||||
|
||||
# The minimum and maximum lengths a password must have.
|
||||
use constant USER_PASSWORD_MIN_LENGTH => 3;
|
||||
use constant USER_PASSWORD_MAX_LENGTH => 16;
|
||||
# The minimum length a password must have.
|
||||
use constant USER_PASSWORD_MIN_LENGTH => 6;
|
||||
|
||||
use constant LOGIN_OPTIONAL => 0;
|
||||
use constant LOGIN_NORMAL => 1;
|
||||
|
@ -238,18 +248,6 @@ use constant LOGOUT_ALL => 0;
|
|||
use constant LOGOUT_CURRENT => 1;
|
||||
use constant LOGOUT_KEEP_CURRENT => 2;
|
||||
|
||||
use constant contenttypes =>
|
||||
{
|
||||
"html"=> "text/html" ,
|
||||
"rdf" => "application/rdf+xml" ,
|
||||
"atom"=> "application/atom+xml" ,
|
||||
"xml" => "application/xml" ,
|
||||
"js" => "application/x-javascript" ,
|
||||
"csv" => "text/csv" ,
|
||||
"png" => "image/png" ,
|
||||
"ics" => "text/calendar" ,
|
||||
};
|
||||
|
||||
use constant GRANT_DIRECT => 0;
|
||||
use constant GRANT_REGEXP => 2;
|
||||
|
||||
|
@ -270,6 +268,9 @@ use constant DEFAULT_COLUMN_LIST => (
|
|||
# for the default settings.
|
||||
use constant DEFAULT_QUERY_NAME => '(Default query)';
|
||||
|
||||
# The default "defaultmilestone" created for products.
|
||||
use constant DEFAULT_MILESTONE => '---';
|
||||
|
||||
# The possible types for saved searches.
|
||||
use constant QUERY_LIST => 0;
|
||||
use constant LIST_OF_BUGS => 1;
|
||||
|
@ -286,6 +287,8 @@ use constant CMT_DUPE_OF => 1;
|
|||
use constant CMT_HAS_DUPE => 2;
|
||||
use constant CMT_POPULAR_VOTES => 3;
|
||||
use constant CMT_MOVED_TO => 4;
|
||||
use constant CMT_ATTACHMENT_CREATED => 5;
|
||||
use constant CMT_ATTACHMENT_UPDATED => 6;
|
||||
|
||||
# Determine whether a validation routine should return 0 or throw
|
||||
# an error when the validation fails.
|
||||
|
@ -370,28 +373,58 @@ use constant FIELD_TYPE_DATETIME => 5;
|
|||
use constant FIELD_TYPE_BUG_ID => 6;
|
||||
use constant FIELD_TYPE_BUG_URLS => 7;
|
||||
|
||||
# The fields from fielddefs that are blocked from non-timetracking users.
|
||||
# work_time is sometimes called actual_time.
|
||||
use constant TIMETRACKING_FIELDS =>
|
||||
qw(estimated_time remaining_time work_time actual_time
|
||||
percentage_complete deadline);
|
||||
|
||||
# The maximum number of days a token will remain valid.
|
||||
use constant MAX_TOKEN_AGE => 3;
|
||||
# How many days a logincookie will remain valid if not used.
|
||||
use constant MAX_LOGINCOOKIE_AGE => 30;
|
||||
|
||||
# Maximum failed logins to lock account for this IP
|
||||
use constant MAX_LOGIN_ATTEMPTS => 5;
|
||||
# If the maximum login attempts occur during this many minutes, the
|
||||
# account is locked.
|
||||
use constant LOGIN_LOCKOUT_INTERVAL => 30;
|
||||
|
||||
# Protocols which are considered as safe.
|
||||
use constant SAFE_PROTOCOLS => ('afs', 'cid', 'ftp', 'gopher', 'http', 'https',
|
||||
'irc', 'mid', 'news', 'nntp', 'prospero', 'telnet',
|
||||
'view-source', 'wais');
|
||||
|
||||
# Valid MIME types for attachments.
|
||||
use constant LEGAL_CONTENT_TYPES => ('application', 'audio', 'image', 'message',
|
||||
'model', 'multipart', 'text', 'video');
|
||||
|
||||
use constant contenttypes =>
|
||||
{
|
||||
"html"=> "text/html" ,
|
||||
"rdf" => "application/rdf+xml" ,
|
||||
"atom"=> "application/atom+xml" ,
|
||||
"xml" => "application/xml" ,
|
||||
"js" => "application/x-javascript" ,
|
||||
"csv" => "text/csv" ,
|
||||
"png" => "image/png" ,
|
||||
"ics" => "text/calendar" ,
|
||||
};
|
||||
|
||||
# Usage modes. Default USAGE_MODE_BROWSER. Use with Bugzilla->usage_mode.
|
||||
use constant USAGE_MODE_BROWSER => 0;
|
||||
use constant USAGE_MODE_CMDLINE => 1;
|
||||
use constant USAGE_MODE_WEBSERVICE => 2;
|
||||
use constant USAGE_MODE_XMLRPC => 2;
|
||||
use constant USAGE_MODE_EMAIL => 3;
|
||||
use constant USAGE_MODE_JSON => 4;
|
||||
|
||||
# Error modes. Default set by Bugzilla->usage_mode (so ERROR_MODE_WEBPAGE
|
||||
# usually). Use with Bugzilla->error_mode.
|
||||
use constant ERROR_MODE_WEBPAGE => 0;
|
||||
use constant ERROR_MODE_DIE => 1;
|
||||
use constant ERROR_MODE_DIE_SOAP_FAULT => 2;
|
||||
use constant ERROR_MODE_AJAX => 3;
|
||||
use constant ERROR_MODE_JSON_RPC => 3;
|
||||
use constant ERROR_MODE_AJAX => 4;
|
||||
|
||||
# The various modes that checksetup.pl can run in.
|
||||
use constant INSTALLATION_MODE_INTERACTIVE => 0;
|
||||
|
@ -425,13 +458,13 @@ use constant DB_MODULE => {
|
|||
name => 'Oracle'},
|
||||
};
|
||||
|
||||
# The user who should be considered "root" when we're giving
|
||||
# instructions to Bugzilla administrators.
|
||||
use constant ROOT_USER => $^O =~ /MSWin32/i ? 'Administrator' : 'root';
|
||||
|
||||
# True if we're on Win32.
|
||||
use constant ON_WINDOWS => ($^O =~ /MSWin32/i);
|
||||
|
||||
# The user who should be considered "root" when we're giving
|
||||
# instructions to Bugzilla administrators.
|
||||
use constant ROOT_USER => ON_WINDOWS ? 'Administrator' : 'root';
|
||||
|
||||
use constant MIN_SMALLINT => -32768;
|
||||
use constant MAX_SMALLINT => 32767;
|
||||
|
||||
|
|
|
@ -65,7 +65,7 @@ use constant ISOLATION_LEVEL => 'REPEATABLE READ';
|
|||
use constant ENUM_DEFAULTS => {
|
||||
bug_severity => ['blocker', 'critical', 'major', 'normal',
|
||||
'minor', 'trivial', 'enhancement'],
|
||||
priority => ["P1","P2","P3","P4","P5"],
|
||||
priority => ["Highest", "High", "Normal", "Low", "Lowest", "---"],
|
||||
op_sys => ["All","Windows","Mac OS","Linux","Other"],
|
||||
rep_platform => ["All","PC","Macintosh","Other"],
|
||||
bug_status => ["UNCONFIRMED","NEW","ASSIGNED","REOPENED","RESOLVED",
|
||||
|
@ -271,9 +271,9 @@ 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 bz_explain);
|
||||
our @_abstract_methods = qw(new sql_regexp sql_not_regexp sql_limit sql_to_days
|
||||
sql_date_format sql_interval bz_explain
|
||||
sql_group_concat);
|
||||
|
||||
# This overridden import method will check implementation of inherited classes
|
||||
# for missing implementation of abstract methods
|
||||
|
@ -286,7 +286,7 @@ sub import {
|
|||
# make sure all abstract methods are implemented
|
||||
foreach my $meth (@_abstract_methods) {
|
||||
$pkg->can($meth)
|
||||
or croak("Class $pkg does not define method $meth");
|
||||
or die("Class $pkg does not define method $meth");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -537,6 +537,13 @@ sub bz_alter_column {
|
|||
ThrowCodeError('column_not_null_no_default_alter',
|
||||
{ name => "$table.$name" }) if ($any_nulls);
|
||||
}
|
||||
# Preserve foreign key definitions in the Schema object when altering
|
||||
# types.
|
||||
if (defined $current_def->{REFERENCES}) {
|
||||
# Make sure we don't modify the caller's $new_def.
|
||||
$new_def = dclone($new_def);
|
||||
$new_def->{REFERENCES} = $current_def->{REFERENCES};
|
||||
}
|
||||
$self->bz_alter_column_raw($table, $name, $new_def, $current_def,
|
||||
$set_nulls_to);
|
||||
$self->_bz_real_schema->set_column($table, $name, $new_def);
|
||||
|
@ -689,7 +696,7 @@ sub bz_add_field_tables {
|
|||
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_schema->MULTI_SELECT_VALUE_TABLE);
|
||||
|
||||
$self->bz_add_fk($ms_table, 'bug_id', {TABLE => 'bugs',
|
||||
COLUMN => 'bug_id',
|
||||
|
@ -830,6 +837,14 @@ sub bz_drop_table {
|
|||
}
|
||||
}
|
||||
|
||||
sub bz_fk_info {
|
||||
my ($self, $table, $column) = @_;
|
||||
my $col_info = $self->bz_column_info($table, $column);
|
||||
return undef if !$col_info;
|
||||
my $fk = $col_info->{REFERENCES};
|
||||
return $fk;
|
||||
}
|
||||
|
||||
sub bz_rename_column {
|
||||
my ($self, $table, $old_name, $new_name) = @_;
|
||||
|
||||
|
@ -872,6 +887,16 @@ sub bz_rename_table {
|
|||
$self->_bz_store_real_schema;
|
||||
}
|
||||
|
||||
sub bz_set_next_serial_value {
|
||||
my ($self, $table, $column, $value) = @_;
|
||||
if (!$value) {
|
||||
$value = $self->selectrow_array("SELECT MAX($column) FROM $table") || 0;
|
||||
$value++;
|
||||
}
|
||||
my @sql = $self->_bz_real_schema->get_set_serial_sql($table, $column, $value);
|
||||
$self->do($_) foreach @sql;
|
||||
}
|
||||
|
||||
#####################################################################
|
||||
# Schema Information Methods
|
||||
#####################################################################
|
||||
|
|
|
@ -68,9 +68,13 @@ sub new {
|
|||
$dsn .= ";port=$port" if $port;
|
||||
$dsn .= ";mysql_socket=$sock" if $sock;
|
||||
|
||||
my $attrs = { mysql_enable_utf8 => Bugzilla->params->{'utf8'} };
|
||||
my %attrs = (
|
||||
mysql_enable_utf8 => Bugzilla->params->{'utf8'},
|
||||
# Needs to be explicitly specified for command-line processes.
|
||||
mysql_auto_reconnect => 1,
|
||||
);
|
||||
|
||||
my $self = $class->db_new($dsn, $user, $pass, $attrs);
|
||||
my $self = $class->db_new($dsn, $user, $pass, \%attrs);
|
||||
|
||||
# This makes sure that if the tables are encoded as UTF-8, we
|
||||
# return their data correctly.
|
||||
|
@ -165,8 +169,8 @@ sub sql_string_concat {
|
|||
sub sql_fulltext_search {
|
||||
my ($self, $column, $text) = @_;
|
||||
|
||||
# quote un-quoted compound words
|
||||
my @words = quotewords('[\s()]+', 'delimiters', $text);
|
||||
# quote un-quoted compound words
|
||||
my @words = quotewords('[\s()]+', 'delimiters', $text);
|
||||
if ($text =~ /(?:^|\W)[+\-<>~"()]/ || $text =~ /[()"*](?:$|\W)/)
|
||||
{
|
||||
# already a boolean mode search
|
||||
|
@ -324,15 +328,11 @@ EOT
|
|||
}
|
||||
|
||||
|
||||
# Figure out if any existing tables are of type ISAM and convert them
|
||||
# to type MyISAM if so. ISAM tables are deprecated in MySQL 3.23,
|
||||
# which Bugzilla now requires, and they don't support more than 16
|
||||
# indexes per table, which Bugzilla needs.
|
||||
my $table_status = $self->selectall_arrayref("SHOW TABLE STATUS");
|
||||
my %table_status = @{ $self->selectcol_arrayref("SHOW TABLE STATUS",
|
||||
{Columns=>[1,2]}) };
|
||||
my @isam_tables;
|
||||
foreach my $row (@$table_status) {
|
||||
my ($name, $type) = @$row;
|
||||
push(@isam_tables, $name) if (defined($type) && $type eq "ISAM");
|
||||
foreach my $name (keys %table_status) {
|
||||
push(@isam_tables, $name) if (defined($table_status{$name}) && $table_status{$name} eq "ISAM");
|
||||
}
|
||||
|
||||
if(scalar(@isam_tables)) {
|
||||
|
@ -354,7 +354,9 @@ EOT
|
|||
# We want to convert tables to InnoDB, but it's possible that they have
|
||||
# fulltext indexes on them, and conversion will fail unless we remove
|
||||
# the indexes.
|
||||
if (grep($_ eq 'bugs', @tables)) {
|
||||
if (grep($_ eq 'bugs', @tables)
|
||||
and !grep($_ eq 'bugs_fulltext', @tables))
|
||||
{
|
||||
if ($self->bz_index_info_real('bugs', 'short_desc')) {
|
||||
$self->bz_drop_index_raw('bugs', 'short_desc');
|
||||
}
|
||||
|
@ -363,7 +365,9 @@ EOT
|
|||
$sd_index_deleted = 1; # Used for later schema cleanup.
|
||||
}
|
||||
}
|
||||
if (grep($_ eq 'longdescs', @tables)) {
|
||||
if (grep($_ eq 'longdescs', @tables)
|
||||
and !grep($_ eq 'bugs_fulltext', @tables))
|
||||
{
|
||||
if ($self->bz_index_info_real('longdescs', 'thetext')) {
|
||||
$self->bz_drop_index_raw('longdescs', 'thetext');
|
||||
}
|
||||
|
@ -375,9 +379,9 @@ EOT
|
|||
|
||||
# Upgrade tables from MyISAM to InnoDB
|
||||
my @myisam_tables;
|
||||
foreach my $row (@$table_status) {
|
||||
my ($name, $type) = @$row;
|
||||
if (defined ($type) && $type =~ /^MYISAM$/i
|
||||
foreach my $name (keys %table_status) {
|
||||
if (defined($table_status{$name})
|
||||
&& $table_status{$name} =~ /^MYISAM$/i
|
||||
&& !grep($_ eq $name, Bugzilla::DB::Schema::Mysql::MYISAM_TABLES))
|
||||
{
|
||||
push(@myisam_tables, $name) ;
|
||||
|
@ -724,6 +728,7 @@ EOT
|
|||
foreach my $table ($self->bz_table_list_real) {
|
||||
my $info_sth = $self->prepare("SHOW FULL COLUMNS FROM $table");
|
||||
$info_sth->execute();
|
||||
my (@binary_sql, @utf8_sql);
|
||||
while (my $column = $info_sth->fetchrow_hashref) {
|
||||
# Our conversion code doesn't work on enum fields, but they
|
||||
# all go away later in checksetup anyway.
|
||||
|
@ -736,34 +741,13 @@ EOT
|
|||
{
|
||||
my $name = $column->{Field};
|
||||
|
||||
# The code below doesn't work on a field with a FULLTEXT
|
||||
# index. So we drop it, which we'd do later anyway.
|
||||
if ($table eq 'longdescs' && $name eq 'thetext') {
|
||||
$self->bz_drop_index('longdescs',
|
||||
'longdescs_thetext_idx');
|
||||
}
|
||||
if ($table eq 'bugs' && $name eq 'short_desc') {
|
||||
$self->bz_drop_index('bugs', 'bugs_short_desc_idx');
|
||||
}
|
||||
my %ft_indexes;
|
||||
if ($table eq 'bugs_fulltext') {
|
||||
%ft_indexes = $self->_bz_real_schema->get_indexes_on_column_abstract(
|
||||
'bugs_fulltext', $name);
|
||||
foreach my $index (keys %ft_indexes) {
|
||||
$self->bz_drop_index('bugs_fulltext', $index);
|
||||
}
|
||||
}
|
||||
if ($table eq 'test_runs' && $name eq 'summary') {
|
||||
$self->bz_drop_index('test_runs', 'test_runs_summary_idx');
|
||||
}
|
||||
print "$table.$name needs to be converted to UTF-8...\n";
|
||||
|
||||
my $dropped = $self->bz_drop_related_fks($table, $name);
|
||||
push(@dropped_fks, @$dropped);
|
||||
|
||||
print "Converting $table.$name to be stored as UTF-8...\n";
|
||||
my $col_info =
|
||||
$self->bz_column_info_real($table, $name);
|
||||
|
||||
# CHANGE COLUMN doesn't take PRIMARY KEY
|
||||
delete $col_info->{PRIMARYKEY};
|
||||
my $sql_def = $self->_bz_schema->get_type_ddl($col_info);
|
||||
|
@ -777,21 +761,41 @@ EOT
|
|||
my $type = $self->_bz_schema->convert_type($col_info->{TYPE});
|
||||
$binary =~ s/(\Q$type\E)/$1 CHARACTER SET binary/;
|
||||
$utf8 =~ s/(\Q$type\E)/$1 CHARACTER SET utf8/;
|
||||
$self->do("ALTER TABLE $table CHANGE COLUMN $name $name
|
||||
$binary");
|
||||
$self->do("ALTER TABLE $table CHANGE COLUMN $name $name
|
||||
$utf8");
|
||||
push(@binary_sql, "MODIFY COLUMN $name $binary");
|
||||
push(@utf8_sql, "MODIFY COLUMN $name $utf8");
|
||||
}
|
||||
} # foreach column
|
||||
|
||||
if ($table eq 'bugs_fulltext') {
|
||||
foreach my $index (keys %ft_indexes) {
|
||||
$self->bz_add_index('bugs_fulltext', $index,
|
||||
$ft_indexes{$index});
|
||||
}
|
||||
if (@binary_sql) {
|
||||
my %indexes = %{ $self->bz_table_indexes($table) };
|
||||
foreach my $index_name (keys %indexes) {
|
||||
my $index = $indexes{$index_name};
|
||||
if ($index->{TYPE} and $index->{TYPE} eq 'FULLTEXT') {
|
||||
$self->bz_drop_index($table, $index_name);
|
||||
}
|
||||
else {
|
||||
delete $indexes{$index_name};
|
||||
}
|
||||
if ($table eq 'test_runs' && $index_name eq 'summary') {
|
||||
$self->bz_drop_index('test_runs', 'test_runs_summary_idx');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$self->do("ALTER TABLE $table DEFAULT CHARACTER SET utf8");
|
||||
print "Converting the $table table to UTF-8...\n";
|
||||
my $bin = "ALTER TABLE $table " . join(', ', @binary_sql);
|
||||
my $utf = "ALTER TABLE $table " . join(', ', @utf8_sql,
|
||||
'DEFAULT CHARACTER SET utf8');
|
||||
$self->do($bin);
|
||||
$self->do($utf);
|
||||
|
||||
# Re-add any removed FULLTEXT indexes.
|
||||
foreach my $index (keys %indexes) {
|
||||
$self->bz_add_index($table, $index, $indexes{$index});
|
||||
}
|
||||
}
|
||||
else {
|
||||
$self->do("ALTER TABLE $table DEFAULT CHARACTER SET utf8");
|
||||
}
|
||||
|
||||
} # foreach my $table (@tables)
|
||||
|
||||
|
@ -832,22 +836,40 @@ sub _fix_defaults {
|
|||
# a default.
|
||||
return unless (defined $assi_default && $assi_default ne '');
|
||||
|
||||
my %fix_columns;
|
||||
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);
|
||||
my $abs_def = $self->bz_column_info($table, $column);
|
||||
# BLOB/TEXT columns never have defaults
|
||||
next if $abs_def->{TYPE} =~ /BLOB|TEXT/i;
|
||||
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";
|
||||
if ($raw_default eq '') {
|
||||
# Only (var)char columns can have empty strings as
|
||||
# defaults, so if we got an empty string for some
|
||||
# other default type, then it's bogus.
|
||||
next unless $abs_def->{TYPE} =~ /char/i;
|
||||
$raw_default = "''";
|
||||
}
|
||||
$fix_columns{$table} ||= [];
|
||||
push(@{ $fix_columns{$table} }, $column);
|
||||
print "$table.$column has incorrect DB default: $raw_default\n";
|
||||
}
|
||||
}
|
||||
} # foreach $column
|
||||
} # foreach $table
|
||||
|
||||
print "Fixing defaults...\n";
|
||||
foreach my $table (reverse sort keys %fix_columns) {
|
||||
my @alters = map("ALTER COLUMN $_ DROP DEFAULT",
|
||||
@{ $fix_columns{$table} });
|
||||
my $sql = "ALTER TABLE $table " . join(',', @alters);
|
||||
$self->do($sql);
|
||||
}
|
||||
}
|
||||
|
||||
# There is a bug in MySQL 4.1.0 - 4.1.15 that makes certain SELECT
|
||||
|
|
|
@ -115,6 +115,12 @@ sub bz_explain {
|
|||
return join("\n", @$explain);
|
||||
}
|
||||
|
||||
sub sql_group_concat {
|
||||
my ($self, $text, $separator) = @_;
|
||||
$separator ||= "','";
|
||||
return "group_concat(T_CLOB_DELIM($text, $separator))";
|
||||
}
|
||||
|
||||
sub sql_regexp {
|
||||
my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_;
|
||||
$real_pattern ||= $pattern;
|
||||
|
@ -272,6 +278,10 @@ sub _fix_hashref {
|
|||
sub adjust_statement {
|
||||
my ($sql) = @_;
|
||||
|
||||
if ($sql =~ /^CREATE OR REPLACE.*/i){
|
||||
return $sql;
|
||||
}
|
||||
|
||||
# We can't just assume any occurrence of "''" in $sql is an empty
|
||||
# string, since "''" can occur inside a string literal as a way of
|
||||
# escaping a single "'" in the literal. Therefore we must be trickier...
|
||||
|
@ -338,6 +348,10 @@ sub adjust_statement {
|
|||
# Oracle need no 'AS'
|
||||
$nonstring =~ s/\bAS\b//ig;
|
||||
|
||||
# Take the first 4000 chars for comparison
|
||||
$nonstring =~ s/\(\s*(longdescs_\d+\.thetext|attachdata_\d+\.thedata)/
|
||||
\(DBMS_LOB.SUBSTR\($1, 4000, 1\)/ig;
|
||||
|
||||
# Look for a LIMIT clause
|
||||
($limit) = ($nonstring =~ m(/\* LIMIT (\d*) \*/)o);
|
||||
|
||||
|
@ -529,6 +543,88 @@ sub bz_setup_database {
|
|||
. " RETURN DATE IS BEGIN RETURN SYSDATE; END;");
|
||||
$self->do("CREATE OR REPLACE FUNCTION CHAR_LENGTH(COLUMN_NAME VARCHAR2)"
|
||||
. " RETURN NUMBER IS BEGIN RETURN LENGTH(COLUMN_NAME); END;");
|
||||
|
||||
# Create types for group_concat
|
||||
my $t_clob_delim = $self->selectcol_arrayref("
|
||||
SELECT TYPE_NAME FROM USER_TYPES WHERE TYPE_NAME=?",
|
||||
undef, 'T_CLOB_DELIM');
|
||||
|
||||
if ( !@$t_clob_delim ) {
|
||||
$self->do("CREATE OR REPLACE TYPE T_CLOB_DELIM AS OBJECT "
|
||||
. "( p_CONTENT CLOB, p_DELIMITER VARCHAR2(256));");
|
||||
}
|
||||
|
||||
$self->do("CREATE OR REPLACE TYPE T_GROUP_CONCAT AS OBJECT
|
||||
( CLOB_CONTENT CLOB,
|
||||
DELIMITER VARCHAR2(256),
|
||||
STATIC FUNCTION ODCIAGGREGATEINITIALIZE(
|
||||
SCTX IN OUT NOCOPY T_GROUP_CONCAT)
|
||||
RETURN NUMBER,
|
||||
MEMBER FUNCTION ODCIAGGREGATEITERATE(
|
||||
SELF IN OUT NOCOPY T_GROUP_CONCAT,
|
||||
VALUE IN T_CLOB_DELIM)
|
||||
RETURN NUMBER,
|
||||
MEMBER FUNCTION ODCIAGGREGATETERMINATE(
|
||||
SELF IN T_GROUP_CONCAT,
|
||||
RETURNVALUE OUT NOCOPY CLOB,
|
||||
FLAGS IN NUMBER)
|
||||
RETURN NUMBER,
|
||||
MEMBER FUNCTION ODCIAGGREGATEMERGE(
|
||||
SELF IN OUT NOCOPY T_GROUP_CONCAT,
|
||||
CTX2 IN T_GROUP_CONCAT)
|
||||
RETURN NUMBER);");
|
||||
|
||||
$self->do("CREATE OR REPLACE TYPE BODY T_GROUP_CONCAT IS
|
||||
STATIC FUNCTION ODCIAGGREGATEINITIALIZE(
|
||||
SCTX IN OUT NOCOPY T_GROUP_CONCAT)
|
||||
RETURN NUMBER IS
|
||||
BEGIN
|
||||
SCTX := T_GROUP_CONCAT(EMPTY_CLOB(), NULL);
|
||||
DBMS_LOB.CREATETEMPORARY(SCTX.CLOB_CONTENT, TRUE);
|
||||
RETURN ODCICONST.SUCCESS;
|
||||
END;
|
||||
MEMBER FUNCTION ODCIAGGREGATEITERATE(
|
||||
SELF IN OUT NOCOPY T_GROUP_CONCAT,
|
||||
VALUE IN T_CLOB_DELIM)
|
||||
RETURN NUMBER IS
|
||||
BEGIN
|
||||
SELF.DELIMITER := VALUE.P_DELIMITER;
|
||||
DBMS_LOB.WRITEAPPEND(SELF.CLOB_CONTENT,
|
||||
LENGTH(SELF.DELIMITER),
|
||||
SELF.DELIMITER);
|
||||
DBMS_LOB.APPEND(SELF.CLOB_CONTENT, VALUE.P_CONTENT);
|
||||
|
||||
RETURN ODCICONST.SUCCESS;
|
||||
END;
|
||||
MEMBER FUNCTION ODCIAGGREGATETERMINATE(
|
||||
SELF IN T_GROUP_CONCAT,
|
||||
RETURNVALUE OUT NOCOPY CLOB,
|
||||
FLAGS IN NUMBER)
|
||||
RETURN NUMBER IS
|
||||
BEGIN
|
||||
RETURNVALUE := RTRIM(LTRIM(SELF.CLOB_CONTENT,
|
||||
SELF.DELIMITER),
|
||||
SELF.DELIMITER);
|
||||
RETURN ODCICONST.SUCCESS;
|
||||
END;
|
||||
MEMBER FUNCTION ODCIAGGREGATEMERGE(
|
||||
SELF IN OUT NOCOPY T_GROUP_CONCAT,
|
||||
CTX2 IN T_GROUP_CONCAT)
|
||||
RETURN NUMBER IS
|
||||
BEGIN
|
||||
DBMS_LOB.WRITEAPPEND(SELF.CLOB_CONTENT,
|
||||
LENGTH(SELF.DELIMITER),
|
||||
SELF.DELIMITER);
|
||||
DBMS_LOB.APPEND(SELF.CLOB_CONTENT, CTX2.CLOB_CONTENT);
|
||||
RETURN ODCICONST.SUCCESS;
|
||||
END;
|
||||
END;");
|
||||
|
||||
# Create user-defined aggregate function group_concat
|
||||
$self->do("CREATE OR REPLACE FUNCTION GROUP_CONCAT(P_INPUT T_CLOB_DELIM)
|
||||
RETURN CLOB
|
||||
DETERMINISTIC PARALLEL_ENABLE AGGREGATE USING T_GROUP_CONCAT;");
|
||||
|
||||
# Create a WORLD_LEXER named BZ_LEX for multilingual fulltext search
|
||||
my $lexer = $self->selectcol_arrayref(
|
||||
"SELECT pre_name FROM CTXSYS.CTX_PREFERENCES WHERE pre_name = ? AND
|
||||
|
@ -580,6 +676,14 @@ sub bz_setup_database {
|
|||
}
|
||||
}
|
||||
|
||||
# Drop the trigger which causes bug 541553
|
||||
my $trigger_name = "PRODUCTS_MILESTONEURL";
|
||||
my $exist_trigger = $self->selectcol_arrayref(
|
||||
"SELECT OBJECT_NAME FROM USER_OBJECTS
|
||||
WHERE OBJECT_NAME = ?", undef, $trigger_name);
|
||||
if(@$exist_trigger) {
|
||||
$self->do("DROP TRIGGER $trigger_name");
|
||||
}
|
||||
}
|
||||
|
||||
package Bugzilla::DB::Oracle::st;
|
||||
|
|
|
@ -94,6 +94,12 @@ sub bz_last_key {
|
|||
return $last_insert_id;
|
||||
}
|
||||
|
||||
sub sql_group_concat {
|
||||
my ($self, $text, $separator) = @_;
|
||||
$separator ||= "','";
|
||||
return "array_to_string(array_accum($text), $separator)";
|
||||
}
|
||||
|
||||
sub sql_istring {
|
||||
my ($self, $string) = @_;
|
||||
|
||||
|
@ -201,6 +207,20 @@ sub bz_setup_database {
|
|||
my $self = shift;
|
||||
$self->SUPER::bz_setup_database(@_);
|
||||
|
||||
# Custom Functions
|
||||
my $function = 'array_accum';
|
||||
my $array_accum = $self->selectrow_array(
|
||||
'SELECT 1 FROM pg_proc WHERE proname = ?', undef, $function);
|
||||
if (!$array_accum) {
|
||||
print "Creating function $function...\n";
|
||||
$self->do("CREATE AGGREGATE array_accum (
|
||||
SFUNC = array_append,
|
||||
BASETYPE = anyelement,
|
||||
STYPE = anyarray,
|
||||
INITCOND = '{}'
|
||||
)");
|
||||
}
|
||||
|
||||
# PostgreSQL doesn't like having *any* index on the thetext
|
||||
# field, because it can't have index data longer than 2770
|
||||
# characters on that field.
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
# Lance Larsh <lance.larsh@oracle.com>
|
||||
# Dennis Melentyev <dennis.melentyev@infopulse.com.ua>
|
||||
# Akamai Technologies <bugzilla-dev@akamai.com>
|
||||
# Elliotte Martin <emartin@everythingsolved.com>
|
||||
|
||||
package Bugzilla::DB::Schema;
|
||||
|
||||
|
@ -240,7 +241,9 @@ use constant ABSTRACT_SCHEMA => {
|
|||
FIELDS => [
|
||||
bug_id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
|
||||
PRIMARYKEY => 1},
|
||||
assigned_to => {TYPE => 'INT3', NOTNULL => 1},
|
||||
assigned_to => {TYPE => 'INT3', NOTNULL => 1,
|
||||
REFERENCES => {TABLE => 'profiles',
|
||||
COLUMN => 'userid'}},
|
||||
bug_file_loc => {TYPE => 'MEDIUMTEXT'},
|
||||
bug_severity => {TYPE => 'varchar(64)', NOTNULL => 1},
|
||||
bug_status => {TYPE => 'varchar(64)', NOTNULL => 1},
|
||||
|
@ -249,16 +252,24 @@ use constant ABSTRACT_SCHEMA => {
|
|||
short_desc => {TYPE => 'varchar(255)', NOTNULL => 1},
|
||||
op_sys => {TYPE => 'varchar(64)', NOTNULL => 1},
|
||||
priority => {TYPE => 'varchar(64)', NOTNULL => 1},
|
||||
product_id => {TYPE => 'INT2', NOTNULL => 1},
|
||||
product_id => {TYPE => 'INT2', NOTNULL => 1,
|
||||
REFERENCES => {TABLE => 'products',
|
||||
COLUMN => 'id'}},
|
||||
rep_platform => {TYPE => 'varchar(64)', NOTNULL => 1},
|
||||
reporter => {TYPE => 'INT3', NOTNULL => 1},
|
||||
reporter => {TYPE => 'INT3', NOTNULL => 1,
|
||||
REFERENCES => {TABLE => 'profiles',
|
||||
COLUMN => 'userid'}},
|
||||
version => {TYPE => 'varchar(64)', NOTNULL => 1},
|
||||
component_id => {TYPE => 'INT2', NOTNULL => 1},
|
||||
component_id => {TYPE => 'INT2', NOTNULL => 1,
|
||||
REFERENCES => {TABLE => 'components',
|
||||
COLUMN => 'id'}},
|
||||
resolution => {TYPE => 'varchar(64)',
|
||||
NOTNULL => 1, DEFAULT => "''"},
|
||||
target_milestone => {TYPE => 'varchar(20)',
|
||||
NOTNULL => 1, DEFAULT => "'---'"},
|
||||
qa_contact => {TYPE => 'INT3'},
|
||||
qa_contact => {TYPE => 'INT3',
|
||||
REERENCES => {TABLE => 'profiles',
|
||||
COLUMN => 'userid'}},
|
||||
status_whiteboard => {TYPE => 'MEDIUMTEXT', NOTNULL => 1,
|
||||
DEFAULT => "''"},
|
||||
votes => {TYPE => 'INT3', NOTNULL => 1,
|
||||
|
@ -273,9 +284,9 @@ use constant ABSTRACT_SCHEMA => {
|
|||
NOTNULL => 1, DEFAULT => 'TRUE'},
|
||||
cclist_accessible => {TYPE => 'BOOLEAN',
|
||||
NOTNULL => 1, DEFAULT => 'TRUE'},
|
||||
estimated_time => {TYPE => 'decimal(5,2)',
|
||||
estimated_time => {TYPE => 'decimal(7,2)',
|
||||
NOTNULL => 1, DEFAULT => '0'},
|
||||
remaining_time => {TYPE => 'decimal(5,2)',
|
||||
remaining_time => {TYPE => 'decimal(7,2)',
|
||||
NOTNULL => 1, DEFAULT => '0'},
|
||||
deadline => {TYPE => 'DATETIME'},
|
||||
alias => {TYPE => 'varchar(20)'},
|
||||
|
@ -341,7 +352,7 @@ use constant ABSTRACT_SCHEMA => {
|
|||
fieldid => {TYPE => 'INT3', NOTNULL => 1,
|
||||
REFERENCES => {TABLE => 'fielddefs',
|
||||
COLUMN => 'id'}},
|
||||
added => {TYPE => 'TINYTEXT'},
|
||||
added => {TYPE => 'varchar(255)'},
|
||||
removed => {TYPE => 'TINYTEXT'},
|
||||
],
|
||||
INDEXES => [
|
||||
|
@ -349,6 +360,7 @@ use constant ABSTRACT_SCHEMA => {
|
|||
bugs_activity_who_idx => ['who'],
|
||||
bugs_activity_bug_when_idx => ['bug_when'],
|
||||
bugs_activity_fieldid_idx => ['fieldid'],
|
||||
bugs_activity_added_idx => ['added'],
|
||||
],
|
||||
},
|
||||
|
||||
|
@ -374,10 +386,15 @@ use constant ABSTRACT_SCHEMA => {
|
|||
FIELDS => [
|
||||
comment_id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
|
||||
PRIMARYKEY => 1},
|
||||
bug_id => {TYPE => 'INT3', NOTNULL => 1},
|
||||
who => {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'}},
|
||||
bug_when => {TYPE => 'DATETIME', NOTNULL => 1},
|
||||
work_time => {TYPE => 'decimal(5,2)', NOTNULL => 1,
|
||||
work_time => {TYPE => 'decimal(7,2)', NOTNULL => 1,
|
||||
DEFAULT => '0'},
|
||||
thetext => {TYPE => 'LONGTEXT', NOTNULL => 1},
|
||||
isprivate => {TYPE => 'BOOLEAN', NOTNULL => 1,
|
||||
|
@ -592,10 +609,12 @@ use constant ABSTRACT_SCHEMA => {
|
|||
DEFAULT => '0'},
|
||||
grant_group_id => {TYPE => 'INT3',
|
||||
REFERENCES => {TABLE => 'groups',
|
||||
COLUMN => 'id'}},
|
||||
COLUMN => 'id',
|
||||
DELETE => 'SET NULL'}},
|
||||
request_group_id => {TYPE => 'INT3',
|
||||
REFERENCES => {TABLE => 'groups',
|
||||
COLUMN => 'id'}},
|
||||
COLUMN => 'id',
|
||||
DELETE => 'SET NULL'}},
|
||||
],
|
||||
},
|
||||
|
||||
|
@ -784,8 +803,14 @@ use constant ABSTRACT_SCHEMA => {
|
|||
status_workflow => {
|
||||
FIELDS => [
|
||||
# On bug creation, there is no old value.
|
||||
old_status => {TYPE => 'INT2'},
|
||||
new_status => {TYPE => 'INT2', NOTNULL => 1},
|
||||
old_status => {TYPE => 'INT2',
|
||||
REFERENCES => {TABLE => 'bug_status',
|
||||
COLUMN => 'id',
|
||||
DELETE => 'CASCADE'}},
|
||||
new_status => {TYPE => 'INT2', NOTNULL => 1,
|
||||
REFERENCES => {TABLE => 'bug_status',
|
||||
COLUMN => 'id',
|
||||
DELETE => 'CASCADE'}},
|
||||
require_comment => {TYPE => 'INT1', NOTNULL => 1, DEFAULT => 0},
|
||||
],
|
||||
INDEXES => [
|
||||
|
@ -946,7 +971,7 @@ use constant ABSTRACT_SCHEMA => {
|
|||
REFERENCES => {TABLE => 'profiles',
|
||||
COLUMN => 'userid',
|
||||
DELETE => 'CASCADE'}},
|
||||
ipaddr => {TYPE => 'varchar(40)', NOTNULL => 1},
|
||||
ipaddr => {TYPE => 'varchar(40)'},
|
||||
lastused => {TYPE => 'DATETIME', NOTNULL => 1},
|
||||
],
|
||||
INDEXES => [
|
||||
|
@ -954,6 +979,25 @@ use constant ABSTRACT_SCHEMA => {
|
|||
],
|
||||
},
|
||||
|
||||
login_failure => {
|
||||
FIELDS => [
|
||||
user_id => {TYPE => 'INT3', NOTNULL => 1,
|
||||
REFERENCES => {TABLE => 'profiles',
|
||||
COLUMN => 'userid',
|
||||
DELETE => 'CASCADE'}},
|
||||
login_time => {TYPE => 'DATETIME', NOTNULL => 1},
|
||||
ip_addr => {TYPE => 'varchar(40)', NOTNULL => 1},
|
||||
],
|
||||
INDEXES => [
|
||||
# We do lookups by every item in the table simultaneously, but
|
||||
# having an index with all three items would be the same size as
|
||||
# the table. So instead we have an index on just the smallest item,
|
||||
# to speed lookups.
|
||||
login_failure_user_id_idx => ['user_id'],
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
# "tokens" stores the tokens users receive when a password or email
|
||||
# change is requested. Tokens provide an extra measure of security
|
||||
# for these changes.
|
||||
|
@ -1004,10 +1048,12 @@ use constant ABSTRACT_SCHEMA => {
|
|||
REFERENCES => {TABLE => 'products',
|
||||
COLUMN => 'id',
|
||||
DELETE => 'CASCADE'}},
|
||||
entry => {TYPE => 'BOOLEAN', NOTNULL => 1},
|
||||
entry => {TYPE => 'BOOLEAN', NOTNULL => 1,
|
||||
DEFAULT => 'FALSE'},
|
||||
membercontrol => {TYPE => 'BOOLEAN', NOTNULL => 1},
|
||||
othercontrol => {TYPE => 'BOOLEAN', NOTNULL => 1},
|
||||
canedit => {TYPE => 'BOOLEAN', NOTNULL => 1},
|
||||
canedit => {TYPE => 'BOOLEAN', NOTNULL => 1,
|
||||
DEFAULT => 'FALSE'},
|
||||
editcomponents => {TYPE => 'BOOLEAN', NOTNULL => 1,
|
||||
DEFAULT => 'FALSE'},
|
||||
editbugs => {TYPE => 'BOOLEAN', NOTNULL => 1,
|
||||
|
@ -1159,12 +1205,13 @@ use constant ABSTRACT_SCHEMA => {
|
|||
PRIMARYKEY => 1},
|
||||
name => {TYPE => 'varchar(64)', NOTNULL => 1},
|
||||
classification_id => {TYPE => 'INT2', NOTNULL => 1,
|
||||
DEFAULT => '1'},
|
||||
DEFAULT => '1',
|
||||
REFERENCES => {TABLE => 'classifications',
|
||||
COLUMN => 'id',
|
||||
DELETE => 'CASCADE'}},
|
||||
description => {TYPE => 'MEDIUMTEXT'},
|
||||
milestoneurl => {TYPE => 'TINYTEXT', NOTNULL => 1,
|
||||
DEFAULT => "''"},
|
||||
disallownew => {TYPE => 'BOOLEAN', NOTNULL => 1,
|
||||
DEFAULT => 0},
|
||||
isactive => {TYPE => 'BOOLEAN', NOTNULL => 1,
|
||||
DEFAULT => 1},
|
||||
votesperuser => {TYPE => 'INT2', NOTNULL => 1,
|
||||
DEFAULT => 0},
|
||||
maxvotesperbug => {TYPE => 'INT2', NOTNULL => 1,
|
||||
|
@ -1173,6 +1220,8 @@ use constant ABSTRACT_SCHEMA => {
|
|||
DEFAULT => 0},
|
||||
defaultmilestone => {TYPE => 'varchar(20)',
|
||||
NOTNULL => 1, DEFAULT => "'---'"},
|
||||
allows_unconfirmed => {TYPE => 'BOOLEAN', NOTNULL => 1,
|
||||
DEFAULT => 'FALSE'},
|
||||
],
|
||||
INDEXES => [
|
||||
products_name_idx => {FIELDS => ['name'],
|
||||
|
@ -1215,7 +1264,7 @@ use constant ABSTRACT_SCHEMA => {
|
|||
creator => {TYPE => 'INT3',
|
||||
REFERENCES => {TABLE => 'profiles',
|
||||
COLUMN => 'userid',
|
||||
DELETE => 'SET NULL'}},
|
||||
DELETE => 'CASCADE'}},
|
||||
category => {TYPE => 'INT2', NOTNULL => 1,
|
||||
REFERENCES => {TABLE => 'series_categories',
|
||||
COLUMN => 'id',
|
||||
|
@ -1226,7 +1275,6 @@ use constant ABSTRACT_SCHEMA => {
|
|||
DELETE => 'CASCADE'}},
|
||||
name => {TYPE => 'varchar(64)', NOTNULL => 1},
|
||||
frequency => {TYPE => 'INT2', NOTNULL => 1},
|
||||
last_viewed => {TYPE => 'DATETIME'},
|
||||
query => {TYPE => 'MEDIUMTEXT', NOTNULL => 1},
|
||||
is_public => {TYPE => 'BOOLEAN', NOTNULL => 1,
|
||||
DEFAULT => 'FALSE'},
|
||||
|
@ -1321,6 +1369,8 @@ use constant ABSTRACT_SCHEMA => {
|
|||
DELETE => 'CASCADE'}},
|
||||
subject => {TYPE => 'varchar(128)'},
|
||||
body => {TYPE => 'MEDIUMTEXT'},
|
||||
mailifnobugs => {TYPE => 'BOOLEAN', NOTNULL => 1,
|
||||
DEFAULT => 'FALSE'},
|
||||
],
|
||||
},
|
||||
|
||||
|
@ -1388,7 +1438,10 @@ use constant ABSTRACT_SCHEMA => {
|
|||
REFERENCES => {TABLE => 'profiles',
|
||||
COLUMN => 'userid',
|
||||
DELETE => 'CASCADE'}},
|
||||
setting_name => {TYPE => 'varchar(32)', NOTNULL => 1},
|
||||
setting_name => {TYPE => 'varchar(32)', NOTNULL => 1,
|
||||
REFERENCES => {TABLE => 'setting',
|
||||
COLUMN => 'name',
|
||||
DELETE => 'CASCADE'}},
|
||||
setting_value => {TYPE => 'varchar(32)', NOTNULL => 1},
|
||||
],
|
||||
INDEXES => [
|
||||
|
@ -1443,13 +1496,13 @@ use constant ABSTRACT_SCHEMA => {
|
|||
},
|
||||
|
||||
ts_note => {
|
||||
FIELDS => [
|
||||
FIELDS => [
|
||||
# This is a BIGINT in standard TheSchwartz schemas.
|
||||
jobid => {TYPE => 'INT4', NOTNULL => 1},
|
||||
notekey => {TYPE => 'varchar(255)'},
|
||||
value => {TYPE => 'LONGBLOB'},
|
||||
],
|
||||
INDEXES => [
|
||||
],
|
||||
INDEXES => [
|
||||
ts_note_jobid_idx => {FIELDS => [qw(jobid notekey)],
|
||||
TYPE => 'UNIQUE'},
|
||||
],
|
||||
|
@ -1477,7 +1530,7 @@ use constant ABSTRACT_SCHEMA => {
|
|||
status => {TYPE => 'INT2'},
|
||||
completion_time => {TYPE => 'INT4'},
|
||||
delete_after => {TYPE => 'INT4'},
|
||||
],
|
||||
],
|
||||
INDEXES => [
|
||||
ts_exitstatus_funcid_idx => ['funcid'],
|
||||
ts_exitstatus_delete_after_idx => ['delete_after'],
|
||||
|
@ -1507,7 +1560,6 @@ use constant MULTI_SELECT_VALUE_TABLE => {
|
|||
],
|
||||
};
|
||||
|
||||
|
||||
#--------------------------------------------------------------------------
|
||||
|
||||
=head1 METHODS
|
||||
|
@ -1599,7 +1651,7 @@ sub _initialize {
|
|||
if exists $abstract_schema->{$table};
|
||||
}
|
||||
unlock_keys(%$abstract_schema);
|
||||
Bugzilla::Hook::process('db_schema-abstract_schema',
|
||||
Bugzilla::Hook::process('db_schema_abstract_schema',
|
||||
{ schema => $abstract_schema });
|
||||
unlock_hash(%$abstract_schema);
|
||||
}
|
||||
|
|
|
@ -178,13 +178,35 @@ sub get_alter_column_ddl {
|
|||
delete $new_def_copy{PRIMARYKEY};
|
||||
}
|
||||
|
||||
my $new_ddl = $self->get_type_ddl(\%new_def_copy);
|
||||
my @statements;
|
||||
|
||||
push(@statements, "UPDATE $table SET $column = $set_nulls_to
|
||||
WHERE $column IS NULL") if defined $set_nulls_to;
|
||||
push(@statements, "ALTER TABLE $table CHANGE COLUMN
|
||||
|
||||
# Calling SET DEFAULT or DROP DEFAULT is *way* faster than calling
|
||||
# CHANGE COLUMN, so just do that if we're just changing the default.
|
||||
my %old_defaultless = %$old_def;
|
||||
my %new_defaultless = %$new_def;
|
||||
delete $old_defaultless{DEFAULT};
|
||||
delete $new_defaultless{DEFAULT};
|
||||
if (!$self->columns_equal($old_def, $new_def)
|
||||
&& $self->columns_equal(\%new_defaultless, \%old_defaultless))
|
||||
{
|
||||
if (!defined $new_def->{DEFAULT}) {
|
||||
push(@statements,
|
||||
"ALTER TABLE $table ALTER COLUMN $column DROP DEFAULT");
|
||||
}
|
||||
else {
|
||||
push(@statements, "ALTER TABLE $table ALTER COLUMN $column
|
||||
SET DEFAULT " . $new_def->{DEFAULT});
|
||||
}
|
||||
}
|
||||
else {
|
||||
my $new_ddl = $self->get_type_ddl(\%new_def_copy);
|
||||
push(@statements, "ALTER TABLE $table CHANGE COLUMN
|
||||
$column $column $new_ddl");
|
||||
}
|
||||
|
||||
if ($old_def->{PRIMARYKEY} && !$new_def->{PRIMARYKEY}) {
|
||||
# Dropping a PRIMARY KEY needs an explicit DROP PRIMARY KEY
|
||||
push(@statements, "ALTER TABLE $table DROP PRIMARY KEY");
|
||||
|
@ -241,6 +263,11 @@ sub get_rename_indexes_ddl {
|
|||
return ($sql);
|
||||
}
|
||||
|
||||
sub get_set_serial_sql {
|
||||
my ($self, $table, $column, $value) = @_;
|
||||
return ("ALTER TABLE $table AUTO_INCREMENT = $value");
|
||||
}
|
||||
|
||||
# Converts a DBI column_info output to an abstract column definition.
|
||||
# Expects to only be called by Bugzila::DB::Mysql::_bz_build_schema_from_disk,
|
||||
# although there's a chance that it will also work properly if called
|
||||
|
|
|
@ -145,6 +145,9 @@ sub get_fk_ddl {
|
|||
my $to_column = $references->{COLUMN} || confess "No column in reference";
|
||||
my $fk_name = $self->_get_fk_name($table, $column, $references);
|
||||
|
||||
# 'ON DELETE RESTRICT' is enabled by default
|
||||
$delete = "" if ( defined $delete && $delete =~ /RESTRICT/i);
|
||||
|
||||
my $fk_string = "\n CONSTRAINT $fk_name FOREIGN KEY ($column)\n"
|
||||
. " REFERENCES $to_table($to_column)\n";
|
||||
|
||||
|
@ -400,4 +403,29 @@ sub _get_create_seq_ddl {
|
|||
return @ddl;
|
||||
}
|
||||
|
||||
sub get_set_serial_sql {
|
||||
my ($self, $table, $column, $value) = @_;
|
||||
my @sql;
|
||||
my $seq_name = "${table}_${column}_SEQ";
|
||||
push(@sql, "DROP SEQUENCE ${seq_name}");
|
||||
push(@sql, $self->_get_create_seq_ddl($table, $column, $value));
|
||||
return @sql;
|
||||
}
|
||||
|
||||
sub get_drop_column_ddl {
|
||||
my $self = shift;
|
||||
my ($table, $column) = @_;
|
||||
my @sql;
|
||||
push(@sql, $self->SUPER::get_drop_column_ddl(@_));
|
||||
my $dbh=Bugzilla->dbh;
|
||||
my $trigger_name = uc($table . "_" . $column);
|
||||
my $exist_trigger = $dbh->selectcol_arrayref(
|
||||
"SELECT OBJECT_NAME FROM USER_OBJECTS
|
||||
WHERE OBJECT_NAME = ?", undef, $trigger_name);
|
||||
if(@$exist_trigger) {
|
||||
push(@sql, "DROP TRIGGER $trigger_name");
|
||||
}
|
||||
return @sql;
|
||||
}
|
||||
|
||||
1;
|
||||
|
|
|
@ -119,6 +119,12 @@ sub get_rename_table_sql {
|
|||
return ("ALTER TABLE $old_name RENAME TO $new_name");
|
||||
}
|
||||
|
||||
sub get_set_serial_sql {
|
||||
my ($self, $table, $column, $value) = @_;
|
||||
return ("SELECT setval('${table}_${column}_seq', $value, false)
|
||||
FROM $table");
|
||||
}
|
||||
|
||||
sub _get_alter_type_sql {
|
||||
my ($self, $table, $column, $new_def, $old_def) = @_;
|
||||
my @statements;
|
||||
|
|
|
@ -66,7 +66,7 @@ sub _error_message
|
|||
my $mesg = '';
|
||||
$mesg .= "[$$] " . time2str("%D %H:%M:%S ", time());
|
||||
$mesg .= uc($type)." $error ";
|
||||
$mesg .= "$ENV{REMOTE_ADDR}" if $ENV{REMOTE_ADDR};
|
||||
$mesg .= remote_ip();
|
||||
if (Bugzilla->user)
|
||||
{
|
||||
$mesg .= ' ' . Bugzilla->user->login;
|
||||
|
@ -161,19 +161,37 @@ sub _throw_error
|
|||
print Bugzilla->cgi->header();
|
||||
print $message;
|
||||
}
|
||||
elsif ($mode == ERROR_MODE_DIE_SOAP_FAULT)
|
||||
elsif ($mode == ERROR_MODE_DIE_SOAP_FAULT || Bugzilla->error_mode == ERROR_MODE_JSON_RPC)
|
||||
{
|
||||
# Clone the hash so we aren't modifying the constant.
|
||||
my %error_map = %{ WS_ERROR_CODE() };
|
||||
require Bugzilla::Hook;
|
||||
Bugzilla::Hook::process('webservice-error_codes',
|
||||
Bugzilla::Hook::process('webservice_error_codes',
|
||||
{ error_map => \%error_map });
|
||||
my $code = $error_map{$error};
|
||||
if (!$code) {
|
||||
$code = ERROR_UNKNOWN_FATAL if $type eq 'code';
|
||||
$code = ERROR_UNKNOWN_TRANSIENT if $type eq 'user';
|
||||
}
|
||||
die bless { message => SOAP::Fault->faultcode($code)->faultstring($message) };
|
||||
if (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT) {
|
||||
die bless { message => SOAP::Fault->faultcode($code)->faultstring($message) };
|
||||
}
|
||||
else {
|
||||
my $server = Bugzilla->_json_server;
|
||||
# Technically JSON-RPC isn't allowed to have error numbers
|
||||
# higher than 999, but we do this to avoid conflicts with
|
||||
# the internal JSON::RPC error codes.
|
||||
$server->raise_error(code => 100000 + $code,
|
||||
message => $message,
|
||||
id => $server->{_bz_request_id},
|
||||
version => $server->version);
|
||||
# Most JSON-RPC Throw*Error calls happen within an eval inside
|
||||
# of JSON::RPC. So, in that circumstance, instead of exiting,
|
||||
# we die with no message. JSON::RPC checks raise_error before
|
||||
# it checks $@, so it returns the proper error.
|
||||
die if _in_eval();
|
||||
$server->response($server->error_response_header);
|
||||
}
|
||||
}
|
||||
elsif ($mode == ERROR_MODE_AJAX)
|
||||
{
|
||||
|
|
|
@ -0,0 +1,222 @@
|
|||
# -*- 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 Developers are Copyright (C) 2009 the
|
||||
# Initial Developer. All Rights Reserved.
|
||||
#
|
||||
# Contributor(s):
|
||||
# Max Kanat-Alexander <mkanat@bugzilla.org>
|
||||
|
||||
package Bugzilla::Extension;
|
||||
|
||||
use strict;
|
||||
|
||||
# Don't use any more Bugzilla modules here as Bugzilla::Extension
|
||||
# could be used outside of normal running Bugzilla installation
|
||||
# (i.e. in checksetup.pl)
|
||||
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::Util;
|
||||
use Bugzilla::Hook;
|
||||
|
||||
use Cwd qw(abs_path);
|
||||
use File::Basename;
|
||||
use File::Spec::Functions;
|
||||
|
||||
use base 'Exporter';
|
||||
our @EXPORT = qw(extension_info required_modules optional_modules extension_version extension_include extension_template_dir extension_code_dir set_hook);
|
||||
|
||||
my $extensions = {
|
||||
# name => {
|
||||
# required_modules => [],
|
||||
# optional_modules => [],
|
||||
# version => '',
|
||||
# loaded => boolean,
|
||||
# inc => [ 'path1', 'path2' ],
|
||||
# }
|
||||
};
|
||||
|
||||
# List all available extension names
|
||||
sub available
|
||||
{
|
||||
my $dir = bz_locations()->{extensionsdir};
|
||||
my @extension_items = glob(catfile($dir, '*'));
|
||||
my @r;
|
||||
foreach my $item (@extension_items)
|
||||
{
|
||||
my $basename = basename($item);
|
||||
# Skip CVS directories and any hidden files/dirs.
|
||||
next if $basename eq 'CVS' or $basename =~ /^\./;
|
||||
if (-d $item)
|
||||
{
|
||||
if (!-e catfile($item, "disabled"))
|
||||
{
|
||||
trick_taint($basename);
|
||||
push @r, $basename;
|
||||
}
|
||||
}
|
||||
}
|
||||
return @r;
|
||||
}
|
||||
|
||||
# List all loaded extensions
|
||||
sub loaded
|
||||
{
|
||||
return grep { $extensions->{$_}->{loaded} } keys %$extensions;
|
||||
}
|
||||
|
||||
# Get extensions information hashref
|
||||
sub extension_info
|
||||
{
|
||||
shift if $_[0] eq __PACKAGE__ || ref $_[0];
|
||||
my ($name) = @_;
|
||||
return $extensions->{$name};
|
||||
}
|
||||
|
||||
# Getters/setters for REQUIRED_MODULES, OPTIONAL_MODULES and version
|
||||
sub required_modules { setter('required_modules', @_) }
|
||||
sub optional_modules { setter('optional_modules', @_) }
|
||||
sub extension_version { setter('version', @_) }
|
||||
|
||||
# Getter/setter for extension code directory (for old extension system)
|
||||
sub extension_code_dir
|
||||
{
|
||||
my ($name, $new) = @_;
|
||||
my $old = setter('code_dir', $name, $new);
|
||||
return $old || catfile(bz_locations()->{extensionsdir}, $name, 'code');
|
||||
}
|
||||
|
||||
# Getter/setter for extension template directory
|
||||
sub extension_template_dir
|
||||
{
|
||||
my ($name, $new) = @_;
|
||||
my $old = setter('template_dir', $name, $new);
|
||||
return $old || catfile(bz_locations()->{extensionsdir}, $name, 'template');
|
||||
}
|
||||
|
||||
# Getter/setter for extension include path (@INC)
|
||||
sub extension_include
|
||||
{
|
||||
my ($name, $new) = @_;
|
||||
if ($new)
|
||||
{
|
||||
if (ref $new && $new !~ /ARRAY/)
|
||||
{
|
||||
die __PACKAGE__."::extension_include('$name', '$new'): second argument should be an arrayref";
|
||||
}
|
||||
$new = [ $new ] if !ref $new;
|
||||
$new = [ map { abs_path($_) } @$new ];
|
||||
trick_taint($_) for @$new;
|
||||
}
|
||||
my $old = setter('inc', $name, $new);
|
||||
# update @INC
|
||||
my $oh = { map { $_ => 1 } @$old };
|
||||
for (my $i = $#INC; $i >= 0; $i--)
|
||||
{
|
||||
splice @INC, $i, 1 if $oh->{$INC[$i]};
|
||||
}
|
||||
unshift @INC, @$new if $new;
|
||||
return $old;
|
||||
}
|
||||
|
||||
# Generic getter/setter
|
||||
sub setter
|
||||
{
|
||||
my ($key, $name, $value) = @_;
|
||||
$extensions->{$name} ||= {};
|
||||
my $old = $extensions->{$name}->{$key};
|
||||
$extensions->{$name}->{$key} = $value if defined $value;
|
||||
return $old;
|
||||
}
|
||||
|
||||
# Load all available extensions
|
||||
sub load_all
|
||||
{
|
||||
shift if $_[0] && ($_[0] eq __PACKAGE__ || ref $_[0]);
|
||||
foreach (available())
|
||||
{
|
||||
load($_);
|
||||
}
|
||||
}
|
||||
|
||||
# Load one extension
|
||||
sub load
|
||||
{
|
||||
my ($name) = @_;
|
||||
if ($extensions->{$name} && $extensions->{$name}->{loaded})
|
||||
{
|
||||
# Extension is already loaded
|
||||
return;
|
||||
}
|
||||
|
||||
my $dir = bz_locations()->{extensionsdir};
|
||||
# Add default include path
|
||||
extension_include($name, catfile($dir, $name, 'lib'));
|
||||
|
||||
# Load main extension file
|
||||
my $file = catfile($dir, $name, "$name.pl");
|
||||
if (-e $file)
|
||||
{
|
||||
trick_taint($file);
|
||||
require $file;
|
||||
}
|
||||
|
||||
# Support for old extension system
|
||||
my $code_dir = extension_code_dir($name);
|
||||
if (-d $code_dir)
|
||||
{
|
||||
my @hooks = glob(catfile($code_dir, '*.pl'));
|
||||
my ($hook, $hook_sub);
|
||||
foreach my $filename (@hooks)
|
||||
{
|
||||
trick_taint($filename);
|
||||
$hook = basename($filename);
|
||||
$hook =~ s/\.pl$//so;
|
||||
if (!-r $filename)
|
||||
{
|
||||
warn __PACKAGE__."::load(): can't read $filename, skipping";
|
||||
next;
|
||||
}
|
||||
set_hook($name, $hook, { type => 'file', filename => $filename });
|
||||
}
|
||||
}
|
||||
|
||||
$extensions->{$name}->{loaded} = 1;
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::Extension - Base class for Bugzilla Extensions.
|
||||
|
||||
=head1 BUGZILLA::EXTENSION CLASS METHODS
|
||||
|
||||
These are used internally by Bugzilla to load and set up extensions.
|
||||
If you are an extension author, you don't need to care about these.
|
||||
|
||||
=head2 C<load>
|
||||
|
||||
Takes two arguments, the path to F<Extension.pm> and the path to F<Config.pm>,
|
||||
for an extension. Loads the extension's code packages into memory using
|
||||
C<require>, does some sanity-checking on the extension, and returns the
|
||||
package name of the loaded extension.
|
||||
|
||||
=head2 C<load_all>
|
||||
|
||||
Calls L</load> for every enabled extension installed into Bugzilla,
|
||||
and returns an arrayref of all the package names that were loaded.
|
|
@ -15,6 +15,7 @@
|
|||
# Contributor(s): Dan Mosedale <dmose@mozilla.org>
|
||||
# Frédéric Buclin <LpSolit@gmail.com>
|
||||
# Myk Melez <myk@mozilla.org>
|
||||
# Greg Hendricks <ghendricks@novell.com>
|
||||
|
||||
=head1 NAME
|
||||
|
||||
|
@ -106,14 +107,14 @@ use constant DB_COLUMNS => qw(
|
|||
use constant REQUIRED_CREATE_FIELDS => qw(name description);
|
||||
|
||||
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,
|
||||
sortkey => \&_check_sortkey,
|
||||
type => \&_check_type,
|
||||
custom => \&_check_custom,
|
||||
description => \&_check_description,
|
||||
enter_bug => \&_check_enter_bug,
|
||||
buglist => \&Bugzilla::Object::check_boolean,
|
||||
mailhead => \&_check_mailhead,
|
||||
obsolete => \&_check_obsolete,
|
||||
sortkey => \&_check_sortkey,
|
||||
type => \&_check_type,
|
||||
visibility_field_id => \&_check_visibility_field_id,
|
||||
};
|
||||
|
||||
|
@ -217,7 +218,7 @@ use constant DEFAULT_FIELDS => (
|
|||
{name => 'deadline', desc => 'Deadline',
|
||||
in_new_bugmail => 1, buglist => 1},
|
||||
{name => 'commenter', desc => 'Commenter'},
|
||||
{name => 'flagtypes.name', desc => 'Flag'},
|
||||
{name => 'flagtypes.name', desc => 'Flags', buglist => 1},
|
||||
{name => 'requestees.login_name', desc => 'Flag Requestee'},
|
||||
{name => 'setters.login_name', desc => 'Flag Setter'},
|
||||
{name => 'work_time', desc => 'Hours Worked', buglist => 1},
|
||||
|
@ -731,16 +732,14 @@ sub remove_from_db {
|
|||
$bugs_query = "SELECT COUNT(*) FROM bug_$name";
|
||||
}
|
||||
else {
|
||||
$bugs_query = "SELECT COUNT(*) FROM bugs WHERE $name IS NOT NULL
|
||||
AND $name != ''";
|
||||
$bugs_query = "SELECT COUNT(*) FROM bugs WHERE $name IS NOT NULL";
|
||||
if ($self->type != FIELD_TYPE_BUG_ID && $self->type != FIELD_TYPE_DATETIME) {
|
||||
$bugs_query .= " AND $name != ''";
|
||||
}
|
||||
# Ignore the default single select value
|
||||
if ($self->type == FIELD_TYPE_SINGLE_SELECT) {
|
||||
$bugs_query .= " AND $name != '---'";
|
||||
}
|
||||
# Ignore blank dates.
|
||||
if ($self->type == FIELD_TYPE_DATETIME) {
|
||||
$bugs_query .= " AND $name != '00-00-00 00:00:00'";
|
||||
}
|
||||
}
|
||||
|
||||
my $has_bugs = $dbh->selectrow_array($bugs_query);
|
||||
|
@ -845,6 +844,11 @@ sub run_create_validators {
|
|||
}
|
||||
|
||||
my $type = $params->{type} || 0;
|
||||
|
||||
if ($params->{custom} && !$type) {
|
||||
ThrowCodeError('field_type_not_specified');
|
||||
}
|
||||
|
||||
$params->{value_field_id} =
|
||||
$class->_check_value_field_id($params->{value_field_id},
|
||||
($type == FIELD_TYPE_SINGLE_SELECT
|
||||
|
@ -1032,8 +1036,14 @@ sub check_field {
|
|||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
# If $legalsRef is undefined, we use the default valid values.
|
||||
# Valid values for this check are all possible values.
|
||||
# Using get_legal_values would only return active values, but since
|
||||
# some bugs may have inactive values set, we want to check them too.
|
||||
unless (defined $legalsRef) {
|
||||
$legalsRef = get_legal_field_values($name);
|
||||
$legalsRef = Bugzilla::Field->new({name => $name})->legal_values;
|
||||
my @values = map($_->name, @$legalsRef);
|
||||
$legalsRef = \@values;
|
||||
|
||||
}
|
||||
|
||||
if (!defined($value)
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
# The Original Code is the Bugzilla Bug Tracking System.
|
||||
#
|
||||
# Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org>
|
||||
# Greg Hendricks <ghendricks@novell.com>
|
||||
# Vitaliy Filippov <vitalif@mail.ru>
|
||||
|
||||
use strict;
|
||||
|
@ -45,11 +46,13 @@ use constant DB_COLUMNS => qw(
|
|||
id
|
||||
value
|
||||
sortkey
|
||||
isactive
|
||||
);
|
||||
|
||||
use constant UPDATE_COLUMNS => qw(
|
||||
value
|
||||
sortkey
|
||||
isactive
|
||||
);
|
||||
|
||||
use constant NAME_FIELD => 'value';
|
||||
|
@ -60,6 +63,7 @@ use constant REQUIRED_CREATE_FIELDS => qw(value);
|
|||
use constant VALIDATORS => {
|
||||
value => \&_check_value,
|
||||
sortkey => \&_check_sortkey,
|
||||
isactive => \&Bugzilla::Object::check_boolean,
|
||||
};
|
||||
|
||||
use constant CLASS_MAP => {
|
||||
|
@ -214,7 +218,8 @@ sub _check_if_controller {
|
|||
# Accessors #
|
||||
#############
|
||||
|
||||
sub sortkey { return $_[0]->{'sortkey'}; }
|
||||
sub is_active { return $_[0]->{'isactive'}; }
|
||||
sub sortkey { return $_[0]->{'sortkey'}; }
|
||||
|
||||
sub bug_count {
|
||||
my $self = shift;
|
||||
|
@ -303,7 +308,7 @@ sub controlled_values
|
|||
{
|
||||
my $type = Bugzilla::Field::Choice->type($field);
|
||||
$f = $type->match({ id => $f });
|
||||
}
|
||||
}
|
||||
$controlled_values->{$field->name} = $f;
|
||||
}
|
||||
$self->{controlled_values} = $controlled_values;
|
||||
|
@ -317,7 +322,7 @@ sub controlled_plus_generic
|
|||
my $controlled_values;
|
||||
unless ($controlled_values = $self->{controlled_plus_generic})
|
||||
{
|
||||
my $fields = $self->field->controls_values_of;
|
||||
my $fields = $self->field->controls_values_of;
|
||||
foreach my $field (@$fields)
|
||||
{
|
||||
my $f = Bugzilla->dbh->selectcol_arrayref(
|
||||
|
@ -368,8 +373,9 @@ sub has_visibility_value
|
|||
# Mutators #
|
||||
############
|
||||
|
||||
sub set_name { $_[0]->set('value', $_[1]); }
|
||||
sub set_sortkey { $_[0]->set('sortkey', $_[1]); }
|
||||
sub set_is_active { $_[0]->set('isactive', $_[1]); }
|
||||
sub set_name { $_[0]->set('value', $_[1]); }
|
||||
sub set_sortkey { $_[0]->set('sortkey', $_[1]); }
|
||||
|
||||
sub set_visibility_values
|
||||
{
|
||||
|
|
1333
Bugzilla/Flag.pm
1333
Bugzilla/Flag.pm
File diff suppressed because it is too large
Load Diff
|
@ -84,21 +84,49 @@ sub user_regexp { return $_[0]->{'userregexp'}; }
|
|||
sub is_active { return $_[0]->{'isactive'}; }
|
||||
sub icon_url { return $_[0]->{'icon_url'}; }
|
||||
|
||||
sub bugs {
|
||||
my $self = shift;
|
||||
return $self->{bugs} if exists $self->{bugs};
|
||||
my $bug_ids = Bugzilla->dbh->selectcol_arrayref(
|
||||
'SELECT bug_id FROM bug_group_map WHERE group_id = ?',
|
||||
undef, $self->id);
|
||||
require Bugzilla::Bug;
|
||||
$self->{bugs} = Bugzilla::Bug->new_from_list($bug_ids);
|
||||
return $self->{bugs};
|
||||
}
|
||||
|
||||
sub members_direct {
|
||||
my ($self) = @_;
|
||||
return $self->{members_direct} if defined $self->{members_direct};
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $user_ids = $dbh->selectcol_arrayref(
|
||||
"SELECT user_group_map.user_id
|
||||
FROM user_group_map
|
||||
WHERE user_group_map.group_id = ?
|
||||
AND grant_type = " . GRANT_DIRECT . "
|
||||
AND isbless = 0", undef, $self->id);
|
||||
require Bugzilla::User;
|
||||
$self->{members_direct} = Bugzilla::User->new_from_list($user_ids);
|
||||
$self->{members_direct} ||= $self->_get_members(GRANT_DIRECT);
|
||||
return $self->{members_direct};
|
||||
}
|
||||
|
||||
sub members_non_inherited {
|
||||
my ($self) = @_;
|
||||
$self->{members_non_inherited} ||= $self->_get_members();
|
||||
return $self->{members_non_inherited};
|
||||
}
|
||||
|
||||
# A helper for members_direct and members_non_inherited
|
||||
sub _get_members {
|
||||
my ($self, $grant_type) = @_;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $grant_clause = $grant_type ? "AND grant_type = $grant_type" : "";
|
||||
my $user_ids = $dbh->selectcol_arrayref(
|
||||
"SELECT DISTINCT user_id
|
||||
FROM user_group_map
|
||||
WHERE isbless = 0 $grant_clause AND group_id = ?", undef, $self->id);
|
||||
require Bugzilla::User;
|
||||
return Bugzilla::User->new_from_list($user_ids);
|
||||
}
|
||||
|
||||
sub flag_types {
|
||||
my $self = shift;
|
||||
require Bugzilla::FlagType;
|
||||
$self->{flag_types} ||= Bugzilla::FlagType::match({ group => $self->id });
|
||||
return $self->{flag_types};
|
||||
}
|
||||
|
||||
sub grant_direct {
|
||||
my ($self, $type) = @_;
|
||||
$self->{grant_direct} ||= {};
|
||||
|
@ -131,6 +159,30 @@ sub granted_by_direct {
|
|||
return $self->{granted_by_direct}->{$type};
|
||||
}
|
||||
|
||||
sub products {
|
||||
my $self = shift;
|
||||
return $self->{products} if exists $self->{products};
|
||||
my $product_data = Bugzilla->dbh->selectall_arrayref(
|
||||
'SELECT product_id, entry, membercontrol, othercontrol,
|
||||
canedit, editcomponents, editbugs, canconfirm
|
||||
FROM group_control_map WHERE group_id = ?', {Slice=>{}},
|
||||
$self->id);
|
||||
my @ids = map { $_->{product_id} } @$product_data;
|
||||
require Bugzilla::Product;
|
||||
my $products = Bugzilla::Product->new_from_list(\@ids);
|
||||
my %data_map = map { $_->{product_id} => $_ } @$product_data;
|
||||
my @retval;
|
||||
foreach my $product (@$products) {
|
||||
# Data doesn't need to contain product_id--we already have
|
||||
# the product object.
|
||||
delete $data_map{$product->id}->{product_id};
|
||||
push(@retval, { controls => $data_map{$product->id},
|
||||
product => $product });
|
||||
}
|
||||
$self->{products} = \@retval;
|
||||
return $self->{products};
|
||||
}
|
||||
|
||||
###############################
|
||||
#### Methods ####
|
||||
###############################
|
||||
|
@ -143,6 +195,8 @@ sub set_icon_url { $_[0]->set('icon_url', $_[1]); }
|
|||
|
||||
sub update {
|
||||
my $self = shift;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
$dbh->bz_start_transaction();
|
||||
my $changes = $self->SUPER::update(@_);
|
||||
|
||||
if (exists $changes->{name}) {
|
||||
|
@ -162,9 +216,76 @@ sub update {
|
|||
&& $changes->{isactive}->[1]);
|
||||
|
||||
$self->_rederive_regexp() if exists $changes->{userregexp};
|
||||
|
||||
Bugzilla::Hook::process('group_end_of_update',
|
||||
{ group => $self, changes => $changes });
|
||||
$dbh->bz_commit_transaction();
|
||||
return $changes;
|
||||
}
|
||||
|
||||
sub check_remove {
|
||||
my ($self, $params) = @_;
|
||||
|
||||
# System groups cannot be deleted!
|
||||
if (!$self->is_bug_group) {
|
||||
ThrowUserError("system_group_not_deletable", { name => $self->name });
|
||||
}
|
||||
|
||||
# Groups having a special role cannot be deleted.
|
||||
my @special_groups;
|
||||
foreach my $special_group (GROUP_PARAMS) {
|
||||
if ($self->name eq Bugzilla->params->{$special_group}) {
|
||||
push(@special_groups, $special_group);
|
||||
}
|
||||
}
|
||||
if (scalar(@special_groups)) {
|
||||
ThrowUserError('group_has_special_role',
|
||||
{ name => $self->name,
|
||||
groups => \@special_groups });
|
||||
}
|
||||
|
||||
return if $params->{'test_only'};
|
||||
|
||||
my $cantdelete = 0;
|
||||
|
||||
my $users = $self->members_non_inherited;
|
||||
if (scalar(@$users) && !$params->{'remove_from_users'}) {
|
||||
$cantdelete = 1;
|
||||
}
|
||||
|
||||
my $bugs = $self->bugs;
|
||||
if (scalar(@$bugs) && !$params->{'remove_from_bugs'}) {
|
||||
$cantdelete = 1;
|
||||
}
|
||||
|
||||
my $products = $self->products;
|
||||
if (scalar(@$products) && !$params->{'remove_from_products'}) {
|
||||
$cantdelete = 1;
|
||||
}
|
||||
|
||||
my $flag_types = $self->flag_types;
|
||||
if (scalar(@$flag_types) && !$params->{'remove_from_flags'}) {
|
||||
$cantdelete = 1;
|
||||
}
|
||||
|
||||
ThrowUserError('group_cannot_delete', { group => $self }) if $cantdelete;
|
||||
}
|
||||
|
||||
sub remove_from_db {
|
||||
my $self = shift;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
$self->check_remove(@_);
|
||||
$dbh->bz_start_transaction();
|
||||
Bugzilla::Hook::process('group_before_delete', { group => $self });
|
||||
$dbh->do('DELETE FROM whine_schedules
|
||||
WHERE mailto_type = ? AND mailto = ?',
|
||||
undef, MAILTO_GROUP, $self->id);
|
||||
# All the other tables will be handled by foreign keys when we
|
||||
# drop the main "groups" row.
|
||||
$self->SUPER::remove_from_db(@_);
|
||||
$dbh->bz_commit_transaction();
|
||||
}
|
||||
|
||||
# Add missing entries in bug_group_map for bugs created while
|
||||
# a mandatory group was disabled and which is now enabled again.
|
||||
sub _enforce_mandatory {
|
||||
|
@ -224,20 +345,6 @@ sub _rederive_regexp {
|
|||
}
|
||||
}
|
||||
|
||||
sub members_non_inherited {
|
||||
my ($self) = @_;
|
||||
return $self->{members_non_inherited}
|
||||
if exists $self->{members_non_inherited};
|
||||
|
||||
my $member_ids = Bugzilla->dbh->selectcol_arrayref(
|
||||
'SELECT DISTINCT user_id FROM user_group_map
|
||||
WHERE isbless = 0 AND group_id = ?',
|
||||
undef, $self->id) || [];
|
||||
require Bugzilla::User;
|
||||
$self->{members_non_inherited} = Bugzilla::User->new_from_list($member_ids);
|
||||
return $self->{members_non_inherited};
|
||||
}
|
||||
|
||||
sub flatten_group_membership {
|
||||
my ($self, @groups) = @_;
|
||||
|
||||
|
@ -277,6 +384,8 @@ sub create {
|
|||
print get_text('install_group_create', { name => $params->{name} }) . "\n"
|
||||
if Bugzilla->usage_mode == USAGE_MODE_CMDLINE;
|
||||
|
||||
$dbh->bz_start_transaction();
|
||||
|
||||
my $group = $class->SUPER::create(@_);
|
||||
|
||||
# Since we created a new group, give the "admin" group all privileges
|
||||
|
@ -294,6 +403,9 @@ sub create {
|
|||
}
|
||||
|
||||
$group->_rederive_regexp() if $group->user_regexp;
|
||||
|
||||
Bugzilla::Hook::process('group_end_of_create', { group => $group });
|
||||
$dbh->bz_commit_transaction();
|
||||
return $group;
|
||||
}
|
||||
|
||||
|
@ -414,6 +526,53 @@ be a member of this group.
|
|||
|
||||
=over
|
||||
|
||||
=item C<check_remove>
|
||||
|
||||
=over
|
||||
|
||||
=item B<Description>
|
||||
|
||||
Determines whether it's OK to remove this group from the database, and
|
||||
throws an error if it's not OK.
|
||||
|
||||
=item B<Params>
|
||||
|
||||
=over
|
||||
|
||||
=item C<test_only>
|
||||
|
||||
C<boolean> If you want to only check if the group can be deleted I<at all>,
|
||||
under any circumstances, specify C<test_only> to just do the most basic tests
|
||||
(the other parameters will be ignored in this situation, as those tests won't
|
||||
be run).
|
||||
|
||||
=item C<remove_from_users>
|
||||
|
||||
C<boolean> True if it would be OK to remove all users who are in this group
|
||||
from this group.
|
||||
|
||||
=item C<remove_from_bugs>
|
||||
|
||||
C<boolean> True if it would be OK to remove all bugs that are in this group
|
||||
from this group.
|
||||
|
||||
=item C<remove_from_flags>
|
||||
|
||||
C<boolean> True if it would be OK to stop all flagtypes that reference
|
||||
this group from referencing this group (e.g., as their grantgroup or
|
||||
requestgroup).
|
||||
|
||||
=item C<remove_from_products>
|
||||
|
||||
C<boolean> True if it would be OK to remove this group from all group controls
|
||||
on products.
|
||||
|
||||
=back
|
||||
|
||||
=item B<Returns> (nothing)
|
||||
|
||||
=back
|
||||
|
||||
=item C<members_non_inherited>
|
||||
|
||||
Returns an arrayref of L<Bugzilla::User> objects representing people who are
|
||||
|
|
751
Bugzilla/Hook.pm
751
Bugzilla/Hook.pm
|
@ -18,69 +18,112 @@
|
|||
# Rights Reserved.
|
||||
#
|
||||
# Contributor(s): Zach Lipton <zach@zachlipton.com>
|
||||
#
|
||||
# Max Kanat-Alexander <mkanat@bugzilla.org>
|
||||
|
||||
package Bugzilla::Hook;
|
||||
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::Util;
|
||||
use Bugzilla::Error;
|
||||
|
||||
use strict;
|
||||
no strict 'subs';
|
||||
use Bugzilla::Util;
|
||||
use base 'Exporter';
|
||||
our @EXPORT = qw(set_hook run_hooks);
|
||||
|
||||
sub process {
|
||||
my ($name, $args) = @_;
|
||||
my %hooks;
|
||||
my @hook_stack;
|
||||
my %hook_hash;
|
||||
|
||||
# get a list of all extensions
|
||||
my @extensions = glob(bz_locations()->{'extensionsdir'} . "/*");
|
||||
|
||||
# check each extension to see if it uses the hook
|
||||
# if so, invoke the extension source file:
|
||||
foreach my $extension (@extensions) {
|
||||
# all of these variables come directly from code or directory names.
|
||||
# If there's malicious data here, we have much bigger issues to
|
||||
# worry about, so we can safely detaint them:
|
||||
trick_taint($extension);
|
||||
# Skip CVS directories and any hidden files/dirs.
|
||||
next if $extension =~ m{/CVS$} || $extension =~ m{/\.[^/]+$};
|
||||
next if -e "$extension/disabled";
|
||||
if (-e $extension.'/code/'.$name.'.pl') {
|
||||
Bugzilla->hook_args($args);
|
||||
# Allow extensions to load their own libraries.
|
||||
local @INC = ("$extension/lib", @INC);
|
||||
do($extension.'/code/'.$name.'.pl');
|
||||
if ($@)
|
||||
{
|
||||
$@->throw if ref($@) && $@->isa('Bugzilla::Error');
|
||||
ThrowCodeError('extension_invalid',
|
||||
{ errstr => $@, name => $name, extension => $extension });
|
||||
}
|
||||
# Flush stored data.
|
||||
Bugzilla->hook_args({});
|
||||
}
|
||||
}
|
||||
# Set extension hook or hooks
|
||||
sub set_hook
|
||||
{
|
||||
my ($extension, $hook, $callable) = @_;
|
||||
$hook =~ tr/-/_/;
|
||||
$hooks{$hook}{$extension} = $callable;
|
||||
}
|
||||
|
||||
sub enabled_plugins {
|
||||
my $extdir = bz_locations()->{'extensionsdir'};
|
||||
my @extensions = glob("$extdir/*");
|
||||
my %enabled;
|
||||
foreach my $extension (@extensions) {
|
||||
trick_taint($extension);
|
||||
my $extname = $extension;
|
||||
$extname =~ s{^\Q$extdir\E/}{};
|
||||
next if $extname eq 'CVS' || $extname =~ /^\./;
|
||||
next if -e "$extension/disabled";
|
||||
# Allow extensions to load their own libraries.
|
||||
local @INC = ("$extension/lib", @INC);
|
||||
$enabled{$extname} = do("$extension/info.pl");
|
||||
ThrowCodeError('extension_invalid',
|
||||
{ errstr => $@, name => 'version',
|
||||
extension => $extension }) if $@;
|
||||
# An alias
|
||||
sub run_hooks
|
||||
{
|
||||
goto &process;
|
||||
}
|
||||
|
||||
# Process all hooks with name $name
|
||||
sub process
|
||||
{
|
||||
my ($name, $args) = @_;
|
||||
$name =~ tr/-/_/;
|
||||
|
||||
push @hook_stack, $name;
|
||||
$hook_hash{$name}++;
|
||||
|
||||
my @process = values %{$hooks{$name}};
|
||||
for my $f (@process)
|
||||
{
|
||||
if (!defined $f)
|
||||
{
|
||||
next;
|
||||
}
|
||||
elsif (ref $f eq 'ARRAY')
|
||||
{
|
||||
push @process, @$f;
|
||||
next;
|
||||
}
|
||||
elsif (ref $f eq 'CODE')
|
||||
{
|
||||
# Fall through if()
|
||||
}
|
||||
elsif (!ref $f && $f =~ /^(.*)::[^:]*$/)
|
||||
{
|
||||
my $pk = $1;
|
||||
if ($pk)
|
||||
{
|
||||
eval { require $pk };
|
||||
if ($@)
|
||||
{
|
||||
warn "Error autoloading hook package $pk: $@";
|
||||
}
|
||||
}
|
||||
}
|
||||
elsif (ref $f eq 'HASH' && $f->{type} eq 'file' &&
|
||||
open my $fd, $f->{filename})
|
||||
{
|
||||
# Slurp file content into $hook_sub
|
||||
my $sub;
|
||||
{
|
||||
local $/ = undef;
|
||||
$sub = <$fd>;
|
||||
trick_taint($sub);
|
||||
}
|
||||
close $fd;
|
||||
$sub =~ s/Bugzilla->hook_args/\$args/gso;
|
||||
my $pk = $f->{filename};
|
||||
$pk =~ s/\W+/_/gso;
|
||||
$pk = "Bugzilla::Hook::$pk";
|
||||
$sub = eval "package $pk; sub { my (\$args) = \@_; $sub; return 1; };";
|
||||
if ($@)
|
||||
{
|
||||
warn __PACKAGE__."::load(): error during loading $f->{filename} into a subroutine (note that Bugzilla->hook_args was replaced by \$args): $@";
|
||||
next;
|
||||
}
|
||||
$f = $sub;
|
||||
}
|
||||
else
|
||||
{
|
||||
die "Don't know what to do with hook callable \"$f\". Is it really callable?";
|
||||
}
|
||||
# OK, call the function!
|
||||
# When a hook returns TRUE, other hooks are also called
|
||||
# When a hook returns FALSE, hook processing is stopped
|
||||
&$f($args) || last;
|
||||
}
|
||||
|
||||
return \%enabled;
|
||||
$hook_hash{$name}--;
|
||||
pop @hook_stack;
|
||||
}
|
||||
|
||||
sub in
|
||||
{
|
||||
my $hook_name = shift;
|
||||
return $hook_hash{$hook_name} || 0;
|
||||
}
|
||||
|
||||
1;
|
||||
|
@ -105,36 +148,19 @@ hooks. When a piece of standard Bugzilla code wants to allow an extension
|
|||
to perform additional functions, it uses Bugzilla::Hook's L</process>
|
||||
subroutine to invoke any extension code if installed.
|
||||
|
||||
There is a sample extension in F<extensions/example/> that demonstrates
|
||||
most of the things described in this document, as well as many of the
|
||||
hooks available.
|
||||
The implementation of extensions is described in L<Bugzilla::Extension>.
|
||||
|
||||
There is sample code for every hook in the Example extension, located in
|
||||
F<extensions/Example/Extension.pm>.
|
||||
|
||||
=head2 How Hooks Work
|
||||
|
||||
When a hook named C<HOOK_NAME> is run, Bugzilla will attempt to invoke any
|
||||
source files named F<extensions/*/code/HOOK_NAME.pl>.
|
||||
When a hook named C<HOOK_NAME> is run, Bugzilla looks through all
|
||||
enabled L<extensions|Bugzilla::Extension> for extensions that implement
|
||||
a subroutined named C<HOOK_NAME>.
|
||||
|
||||
So, for example, if your extension is called "testopia", and you
|
||||
want to have code run during the L</install-update_db> hook, you
|
||||
would have a file called F<extensions/testopia/code/install-update_db.pl>
|
||||
that contained perl code to run during that hook.
|
||||
|
||||
=head2 Arguments Passed to Hooks
|
||||
|
||||
Some L<hooks|/HOOKS> have params that are passed to them.
|
||||
|
||||
These params are accessible through L<Bugzilla/hook_args>.
|
||||
That returns a hashref. Very frequently, if you want your
|
||||
hook to do anything, you have to modify these variables.
|
||||
|
||||
=head2 Versioning Extensions
|
||||
|
||||
Every extension must have a file in its root called F<info.pl>.
|
||||
This file must return a hash when called with C<do>.
|
||||
The hash must contain a 'version' key with the current version of the
|
||||
extension. Extension authors can also add any extra infomration to this hash if
|
||||
required, by adding a new key beginning with x_ which will not be used the
|
||||
core Bugzilla code.
|
||||
See L<Bugzilla::Extension> for more details about how an extension
|
||||
can run code during a hook.
|
||||
|
||||
=head1 SUBROUTINES
|
||||
|
||||
|
@ -174,7 +200,25 @@ 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
|
||||
=head2 attachment_process_data
|
||||
|
||||
This happens at the very beginning process of the attachment creation.
|
||||
You can edit the attachment content itself as well as all attributes
|
||||
of the attachment, before they are validated and inserted into the DB.
|
||||
|
||||
Params:
|
||||
|
||||
=over
|
||||
|
||||
=item C<data> - A reference pointing either to the content of the file
|
||||
being uploaded or pointing to the filehandle associated with the file.
|
||||
|
||||
=item C<attributes> - A hashref whose keys are the same as
|
||||
L<Bugzilla::Attachment/create>. The data it contains hasn't been checked yet.
|
||||
|
||||
=back
|
||||
|
||||
=head2 auth_login_methods
|
||||
|
||||
This allows you to add new login types to Bugzilla.
|
||||
(See L<Bugzilla::Auth::Login>.)
|
||||
|
@ -205,16 +249,16 @@ login methods that weren't passed to L<Bugzilla::Auth/login>.)
|
|||
|
||||
=back
|
||||
|
||||
=head2 auth-verify_methods
|
||||
=head2 auth_verify_methods
|
||||
|
||||
This works just like L</auth-login_methods> except it's for
|
||||
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>.
|
||||
takes a C<modules> parameter, just like L</auth_login_methods>.
|
||||
|
||||
=head2 bug-columns
|
||||
=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
|
||||
object. Note that you will also need to use the L</bug_fields> hook in
|
||||
conjunction with this hook to make this work.
|
||||
|
||||
Params:
|
||||
|
@ -226,7 +270,7 @@ your column name(s) onto the array.
|
|||
|
||||
=back
|
||||
|
||||
=head2 bug-end_of_create
|
||||
=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.
|
||||
|
@ -242,7 +286,22 @@ values.
|
|||
|
||||
=back
|
||||
|
||||
=head2 bug-end_of_update
|
||||
=head2 bug_end_of_create_validators
|
||||
|
||||
This happens during L<Bugzilla::Bug/create>, after all parameters have
|
||||
been validated, but before anything has been inserted into the database.
|
||||
|
||||
Params:
|
||||
|
||||
=over
|
||||
|
||||
=item C<params>
|
||||
|
||||
A hashref. The validated parameters passed to C<create>.
|
||||
|
||||
=back
|
||||
|
||||
=head2 bug_end_of_update
|
||||
|
||||
This happens at the end of L<Bugzilla::Bug/update>, after all other changes are
|
||||
made to the database. This generally occurs inside a database transaction.
|
||||
|
@ -251,23 +310,32 @@ Params:
|
|||
|
||||
=over
|
||||
|
||||
=item C<bug> - The changed bug object, with all fields set to their updated
|
||||
values.
|
||||
=item C<bug>
|
||||
|
||||
=item C<timestamp> - The timestamp used for all updates in this transaction.
|
||||
The changed bug object, with all fields set to their updated values.
|
||||
|
||||
=item C<changes> - The hash of changed fields.
|
||||
C<$changes-E<gt>{field} = [old, new]>
|
||||
=item C<old_bug>
|
||||
|
||||
A bug object pulled from the database before the fields were set to
|
||||
their updated values (so it has the old values available for each field).
|
||||
|
||||
=item C<timestamp>
|
||||
|
||||
The timestamp used for all updates in this transaction.
|
||||
|
||||
=item C<changes>
|
||||
|
||||
The hash of changed fields. C<< $changes->{field} = [old, new] >>
|
||||
|
||||
=back
|
||||
|
||||
=head2 bug-fields
|
||||
=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>.
|
||||
Note: You should add here the names of any fields you added in L</bug_columns>.
|
||||
|
||||
Params:
|
||||
|
||||
|
@ -278,7 +346,72 @@ your column name(s) onto the array.
|
|||
|
||||
=back
|
||||
|
||||
=head2 buglist-columns
|
||||
=head2 bug_format_comment
|
||||
|
||||
Allows you to do custom parsing on comments before they are displayed. You do
|
||||
this by returning two regular expressions: one that matches the section you
|
||||
want to replace, and then another that says what you want to replace that
|
||||
match with.
|
||||
|
||||
The matching and replacement will be run with the C</g> switch on the regex.
|
||||
|
||||
Params:
|
||||
|
||||
=over
|
||||
|
||||
=item C<regexes>
|
||||
|
||||
An arrayref of hashrefs.
|
||||
|
||||
You should push a hashref containing two keys (C<match> and C<replace>)
|
||||
in to this array. C<match> is the regular expression that matches the
|
||||
text you want to replace, C<replace> is what you want to replace that
|
||||
text with. (This gets passed into a regular expression like
|
||||
C<s/$match/$replace/>.)
|
||||
|
||||
Instead of specifying a regular expression for C<replace> you can also
|
||||
return a coderef (a reference to a subroutine). If you want to use
|
||||
backreferences (using C<$1>, C<$2>, etc. in your C<replace>), you have to use
|
||||
this method--it won't work if you specify C<$1>, C<$2> in a regular expression
|
||||
for C<replace>. Your subroutine will get a hashref as its only argument. This
|
||||
hashref contains a single key, C<matches>. C<matches> is an arrayref that
|
||||
contains C<$1>, C<$2>, C<$3>, etc. in order, up to C<$10>. Your subroutine
|
||||
should return what you want to replace the full C<match> with. (See the code
|
||||
example for this hook if you want to see how this actually all works in code.
|
||||
It's simpler than it sounds.)
|
||||
|
||||
B<You are responsible for HTML-escaping your returned data.> Failing to
|
||||
do so could open a security hole in Bugzilla.
|
||||
|
||||
=item C<text>
|
||||
|
||||
A B<reference> to the exact text that you are parsing.
|
||||
|
||||
Generally you should not modify this yourself. Instead you should be
|
||||
returning regular expressions using the C<regexes> array.
|
||||
|
||||
The text has already been word-wrapped, but has not been parsed in any way
|
||||
otherwise. (So, for example, it is not HTML-escaped. You get "&", not
|
||||
"&".)
|
||||
|
||||
=item C<bug>
|
||||
|
||||
The L<Bugzilla::Bug> object that this comment is on. Sometimes this is
|
||||
C<undef>, meaning that we are parsing text that is not on a bug.
|
||||
|
||||
=item C<comment>
|
||||
|
||||
A hashref representing the comment you are about to parse, including
|
||||
all of the fields that comments contain when they are returned by
|
||||
by L<Bugzilla::Bug/longdescs>.
|
||||
|
||||
Sometimes this is C<undef>, meaning that we are parsing text that is
|
||||
not a bug comment (but could still be some other part of a bug, like
|
||||
the summary line).
|
||||
|
||||
=back
|
||||
|
||||
=head2 buglist_columns
|
||||
|
||||
This happens in buglist.cgi after the standard columns have been defined and
|
||||
right before the display column determination. It gives you the opportunity
|
||||
|
@ -306,7 +439,49 @@ The definition is structured as:
|
|||
|
||||
=back
|
||||
|
||||
=head2 colchange-columns
|
||||
=head2 bugmail_recipients
|
||||
|
||||
This allows you to modify the list of users who are going to be receiving
|
||||
a particular bugmail. It also allows you to specify why they are receiving
|
||||
the bugmail.
|
||||
|
||||
Users' bugmail preferences will be applied to any users that you add
|
||||
to the list. (So, for example, if you add somebody as though they were
|
||||
a CC on the bug, and their preferences state that they don't get email
|
||||
when they are a CC, they won't get email.)
|
||||
|
||||
This hook is called before watchers or globalwatchers are added to the
|
||||
recipient list.
|
||||
|
||||
Params:
|
||||
|
||||
=over
|
||||
|
||||
=item C<bug>
|
||||
|
||||
The L<Bugzilla::Bug> that bugmail is being sent about.
|
||||
|
||||
=item C<recipients>
|
||||
|
||||
This is a hashref. The keys are numeric user ids from the C<profiles>
|
||||
table in the database, for each user who should be receiving this bugmail.
|
||||
The values are hashrefs. The keys in I<these> hashrefs correspond to
|
||||
the "relationship" that the user has to the bug they're being emailed
|
||||
about, and the value should always be C<1>. The "relationships"
|
||||
are described by the various C<REL_> constants in L<Bugzilla::Constants>.
|
||||
|
||||
Here's an example of adding userid C<123> to the recipient list
|
||||
as though he were on the CC list:
|
||||
|
||||
$recipients->{123}->{+REL_CC} = 1
|
||||
|
||||
(We use C<+> in front of C<REL_CC> so that Perl interprets it as a constant
|
||||
instead of as a string.)
|
||||
|
||||
=back
|
||||
|
||||
|
||||
=head2 colchange_columns
|
||||
|
||||
This happens in F<colchange.cgi> right after the list of possible display
|
||||
columns have been defined and gives you the opportunity to add additional
|
||||
|
@ -317,12 +492,11 @@ Params:
|
|||
=over
|
||||
|
||||
=item C<columns> - An arrayref containing an array of column IDs. Any IDs
|
||||
added by this hook must have been defined in the the buglist-columns hook.
|
||||
See L</buglist-columns>.
|
||||
added by this hook must have been defined in the the L</buglist_columns> hook.
|
||||
|
||||
=back
|
||||
|
||||
=head2 config-add_panels
|
||||
=head2 config_add_panels
|
||||
|
||||
If you want to add new panels to the Parameters administrative interface,
|
||||
this is where you do it.
|
||||
|
@ -343,7 +517,7 @@ extension.)
|
|||
|
||||
=back
|
||||
|
||||
=head2 config-modify_panels
|
||||
=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
|
||||
|
@ -363,11 +537,13 @@ 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.
|
||||
L</config_add_panels> if you want to add new panels.
|
||||
|
||||
=back
|
||||
|
||||
=head2 enter_bug-entrydefaultvars
|
||||
=head2 enter_bug_entrydefaultvars
|
||||
|
||||
B<DEPRECATED> - Use L</template_before_process> instead.
|
||||
|
||||
This happens right before the template is loaded on enter_bug.cgi.
|
||||
|
||||
|
@ -379,9 +555,9 @@ Params:
|
|||
|
||||
=back
|
||||
|
||||
=head2 flag-end_of_update
|
||||
=head2 flag_end_of_update
|
||||
|
||||
This happens at the end of L<Bugzilla::Flag/process>, after all other changes
|
||||
This happens at the end of L<Bugzilla::Flag/update_flags>, after all other changes
|
||||
are made to the database and after emails are sent. It gives you a before/after
|
||||
snapshot of flags so you can react to specific flag changes.
|
||||
This generally occurs inside a database transaction.
|
||||
|
@ -393,7 +569,7 @@ Params:
|
|||
|
||||
=over
|
||||
|
||||
=item C<bug> - The changed bug object.
|
||||
=item C<object> - The changed bug or attachment object.
|
||||
|
||||
=item C<timestamp> - The timestamp used for all updates in this transaction.
|
||||
|
||||
|
@ -405,7 +581,53 @@ changed flags, and search for a specific condition like C<added eq 'review-'>.
|
|||
|
||||
=back
|
||||
|
||||
=head2 install-before_final_checks
|
||||
=head2 group_before_delete
|
||||
|
||||
This happens in L<Bugzilla::Group/remove_from_db>, after we've confirmed
|
||||
that the group can be deleted, but before any rows have actually
|
||||
been removed from the database. This occurs inside a database
|
||||
transaction.
|
||||
|
||||
Params:
|
||||
|
||||
=over
|
||||
|
||||
=item C<group> - The L<Bugzilla::Group> being deleted.
|
||||
|
||||
=back
|
||||
|
||||
=head2 group_end_of_create
|
||||
|
||||
This happens at the end of L<Bugzilla::Group/create>, after all other
|
||||
changes are made to the database. This occurs inside a database transaction.
|
||||
|
||||
Params:
|
||||
|
||||
=over
|
||||
|
||||
=item C<group> - The changed L<Bugzilla::Group> object, with all fields set
|
||||
to their updated values.
|
||||
|
||||
=back
|
||||
|
||||
=head2 group_end_of_update
|
||||
|
||||
This happens at the end of L<Bugzilla::Group/update>, after all other
|
||||
changes are made to the database. This occurs inside a database transaction.
|
||||
|
||||
Params:
|
||||
|
||||
=over
|
||||
|
||||
=item C<group> - The changed L<Bugzilla::Group> object, with all fields set
|
||||
to their updated values.
|
||||
|
||||
=item C<changes> - The hash of changed fields.
|
||||
C<< $changes->{$field} = [$old, $new] >>
|
||||
|
||||
=back
|
||||
|
||||
=head2 install_before_final_checks
|
||||
|
||||
Allows execution of custom code before the final checks are done in
|
||||
checksetup.pl.
|
||||
|
@ -420,44 +642,20 @@ A flag that indicates whether or not checksetup is running in silent mode.
|
|||
|
||||
=back
|
||||
|
||||
=head2 install-requirements
|
||||
|
||||
Because of the way Bugzilla installation works, there can't be a normal
|
||||
hook during the time that F<checksetup.pl> checks what modules are
|
||||
installed. (C<Bugzilla::Hook> needs to have those modules installed--it's
|
||||
a chicken-and-egg problem.)
|
||||
|
||||
So instead of the way hooks normally work, this hook just looks for two
|
||||
subroutines (or constants, since all constants are just subroutines) in
|
||||
your file, called C<OPTIONAL_MODULES> and C<REQUIRED_MODULES>,
|
||||
which should return arrayrefs in the same format as C<OPTIONAL_MODULES> and
|
||||
C<REQUIRED_MODULES> in L<Bugzilla::Install::Requirements>.
|
||||
|
||||
These subroutines will be passed an arrayref that contains the current
|
||||
Bugzilla requirements of the same type, in case you want to modify
|
||||
Bugzilla's requirements somehow. (Probably the most common would be to
|
||||
alter a version number or the "feature" element of C<OPTIONAL_MODULES>.)
|
||||
|
||||
F<checksetup.pl> will add these requirements to its own.
|
||||
|
||||
Please remember--if you put something in C<REQUIRED_MODULES>, then
|
||||
F<checksetup.pl> B<cannot complete> unless the user has that module
|
||||
installed! So use C<OPTIONAL_MODULES> whenever you can.
|
||||
|
||||
=head2 install-update_db
|
||||
=head2 install_update_db
|
||||
|
||||
This happens at the very end of all the tables being updated
|
||||
during an installation or upgrade. If you need to modify your custom
|
||||
schema, do it here. No params are passed.
|
||||
|
||||
=head2 db_schema-abstract_schema
|
||||
=head2 db_schema_abstract_schema
|
||||
|
||||
This allows you to add tables to Bugzilla. Note that we recommend that you
|
||||
prefix the names of your tables with some word, so that they don't conflict
|
||||
with any future Bugzilla tables.
|
||||
|
||||
If you wish to add new I<columns> to existing Bugzilla tables, do that
|
||||
in L</install-update_db>.
|
||||
in L</install_update_db>.
|
||||
|
||||
Params:
|
||||
|
||||
|
@ -470,7 +668,7 @@ database when run.
|
|||
|
||||
=back
|
||||
|
||||
=head2 mailer-before_send
|
||||
=head2 mailer_before_send
|
||||
|
||||
Called right before L<Bugzilla::Mailer> sends a message to the MTA.
|
||||
|
||||
|
@ -485,7 +683,173 @@ L<Email::Send/new>.
|
|||
|
||||
=back
|
||||
|
||||
=head2 page-before_template
|
||||
=head2 object_before_create
|
||||
|
||||
This happens at the beginning of L<Bugzilla::Object/create>.
|
||||
|
||||
Params:
|
||||
|
||||
=over
|
||||
|
||||
=item C<class>
|
||||
|
||||
The name of the class that C<create> was called on. You can check this
|
||||
like C<< if ($class->isa('Some::Class')) >> in your code, to perform specific
|
||||
tasks before C<create> for only certain classes.
|
||||
|
||||
=item C<params>
|
||||
|
||||
A hashref. The set of named parameters passed to C<create>.
|
||||
|
||||
=back
|
||||
|
||||
|
||||
=head2 object_before_delete
|
||||
|
||||
This happens in L<Bugzilla::Object/remove_from_db>, after we've confirmed
|
||||
that the object can be deleted, but before any rows have actually
|
||||
been removed from the database. This sometimes occurs inside a database
|
||||
transaction.
|
||||
|
||||
Params:
|
||||
|
||||
=over
|
||||
|
||||
=item C<object> - The L<Bugzilla::Object> being deleted. You will probably
|
||||
want to check its type like C<< $object->isa('Some::Class') >> before doing
|
||||
anything with it.
|
||||
|
||||
=back
|
||||
|
||||
|
||||
=head2 object_before_set
|
||||
|
||||
Called during L<Bugzilla::Object/set>, before any actual work is done.
|
||||
You can use this to perform actions before a value is changed for
|
||||
specific fields on certain types of objects.
|
||||
|
||||
Params:
|
||||
|
||||
=over
|
||||
|
||||
=item C<object>
|
||||
|
||||
The object that C<set> was called on. You will probably want to
|
||||
do something like C<< if ($object->isa('Some::Class')) >> in your code to
|
||||
limit your changes to only certain subclasses of Bugzilla::Object.
|
||||
|
||||
=item C<field>
|
||||
|
||||
The name of the field being updated in the object.
|
||||
|
||||
=item C<value>
|
||||
|
||||
The value being set on the object.
|
||||
|
||||
=back
|
||||
|
||||
=head2 object_end_of_create_validators
|
||||
|
||||
Called at the end of L<Bugzilla::Object/run_create_validators>. You can
|
||||
use this to run additional validation when creating an object.
|
||||
|
||||
If a subclass has overridden C<run_create_validators>, then this usually
|
||||
happens I<before> the subclass does its custom validation.
|
||||
|
||||
Params:
|
||||
|
||||
=over
|
||||
|
||||
=item C<class>
|
||||
|
||||
The name of the class that C<create> was called on. You can check this
|
||||
like C<< if ($class->isa('Some::Class')) >> in your code, to perform specific
|
||||
tasks for only certain classes.
|
||||
|
||||
=item C<params>
|
||||
|
||||
A hashref. The set of named parameters passed to C<create>, modified and
|
||||
validated by the C<VALIDATORS> specified for the object.
|
||||
|
||||
=back
|
||||
|
||||
|
||||
=head2 object_end_of_set
|
||||
|
||||
Called during L<Bugzilla::Object/set>, after all the code of the function
|
||||
has completed (so the value has been validated and the field has been set
|
||||
to the new value). You can use this to perform actions after a value is
|
||||
changed for specific fields on certain types of objects.
|
||||
|
||||
The new value is not specifically passed to this hook because you can
|
||||
get it as C<< $object->{$field} >>.
|
||||
|
||||
Params:
|
||||
|
||||
=over
|
||||
|
||||
=item C<object>
|
||||
|
||||
The object that C<set> was called on. You will probably want to
|
||||
do something like C<< if ($object->isa('Some::Class')) >> in your code to
|
||||
limit your changes to only certain subclasses of Bugzilla::Object.
|
||||
|
||||
=item C<field>
|
||||
|
||||
The name of the field that was updated in the object.
|
||||
|
||||
=back
|
||||
|
||||
|
||||
=head2 object_end_of_set_all
|
||||
|
||||
This happens at the end of L<Bugzilla::Object/set_all>. This is a
|
||||
good place to call custom set_ functions on objects, or to make changes
|
||||
to an object before C<update()> is called.
|
||||
|
||||
Params:
|
||||
|
||||
=over
|
||||
|
||||
=item C<object>
|
||||
|
||||
The L<Bugzilla::Object> which is being updated. You will probably want to
|
||||
do something like C<< if ($object->isa('Some::Class')) >> in your code to
|
||||
limit your changes to only certain subclasses of Bugzilla::Object.
|
||||
|
||||
=item C<params>
|
||||
|
||||
A hashref. The set of named parameters passed to C<set_all>.
|
||||
|
||||
=back
|
||||
|
||||
=head2 object_end_of_update
|
||||
|
||||
Called during L<Bugzilla::Object/update>, after changes are made
|
||||
to the database, but while still inside a transaction.
|
||||
|
||||
Params:
|
||||
|
||||
=over
|
||||
|
||||
=item C<object>
|
||||
|
||||
The object that C<update> was called on. You will probably want to
|
||||
do something like C<< if ($object->isa('Some::Class')) >> in your code to
|
||||
limit your changes to only certain subclasses of Bugzilla::Object.
|
||||
|
||||
=item C<old_object>
|
||||
|
||||
The object as it was before it was updated.
|
||||
|
||||
=item C<changes>
|
||||
|
||||
The fields that have been changed, in the same format that
|
||||
L<Bugzilla::Object/update> returns.
|
||||
|
||||
=back
|
||||
|
||||
=head2 page_before_template
|
||||
|
||||
This is a simple way to add your own pages to Bugzilla. This hooks C<page.cgi>,
|
||||
which loads templates from F<template/en/default/pages>. For example,
|
||||
|
@ -512,7 +876,9 @@ your template.
|
|||
|
||||
=back
|
||||
|
||||
=head2 product-confirm_delete
|
||||
=head2 product_confirm_delete
|
||||
|
||||
B<DEPRECATED> - Use L</template_before_process> instead.
|
||||
|
||||
Called before displaying the confirmation message when deleting a product.
|
||||
|
||||
|
@ -524,6 +890,115 @@ Params:
|
|||
|
||||
=back
|
||||
|
||||
=head2 sanitycheck_check
|
||||
|
||||
This hook allows for extra sanity checks to be added, for use by
|
||||
F<sanitycheck.cgi>.
|
||||
|
||||
Params:
|
||||
|
||||
=over
|
||||
|
||||
=item C<status> - a CODEREF that allows status messages to be displayed
|
||||
to the user. (F<sanitycheck.cgi>'s C<Status>)
|
||||
|
||||
=back
|
||||
|
||||
=head2 product_end_of_create
|
||||
|
||||
Called right after a new product has been created, allowing additional
|
||||
changes to be made to the new product's attributes. This occurs inside of
|
||||
a database transaction, so if the hook throws an error all previous
|
||||
changes will be rolled back including the creation of the new product.
|
||||
|
||||
Params:
|
||||
|
||||
=over
|
||||
|
||||
=item C<product> - The new L<Bugzilla::Product> object that was just created.
|
||||
|
||||
=back
|
||||
|
||||
=head2 sanitycheck_repair
|
||||
|
||||
This hook allows for extra sanity check repairs to be made, for use by
|
||||
F<sanitycheck.cgi>.
|
||||
|
||||
Params:
|
||||
|
||||
=over
|
||||
|
||||
=item C<status> - a CODEREF that allows status messages to be displayed
|
||||
to the user. (F<sanitycheck.cgi>'s C<Status>)
|
||||
|
||||
=back
|
||||
|
||||
=head2 template_before_create
|
||||
|
||||
This hook allows you to modify the configuration of L<Bugzilla::Template>
|
||||
objects before they are created. For example, you could add a new
|
||||
global template variable this way.
|
||||
|
||||
Params:
|
||||
|
||||
=over
|
||||
|
||||
=item C<config>
|
||||
|
||||
A hashref--the configuration that will be passed to L<Template/new>.
|
||||
See L<http://template-toolkit.org/docs/modules/Template.html#section_CONFIGURATION_SUMMARY>
|
||||
for information on how this configuration variable is structured (or just
|
||||
look at the code for C<create> in L<Bugzilla::Template>.)
|
||||
|
||||
=back
|
||||
|
||||
=head2 template_before_process
|
||||
|
||||
This hook is called any time Bugzilla processes a template file, including
|
||||
calls to C<< $template->process >>, C<PROCESS> statements in templates,
|
||||
and C<INCLUDE> statements in templates. It is not called when templates
|
||||
process a C<BLOCK>, only when they process a file.
|
||||
|
||||
This hook allows you to define additional variables that will be available to
|
||||
the template being processed, or to modify the variables that are currently
|
||||
in the template. It works exactly as though you inserted code to modify
|
||||
template variables at the top of a template.
|
||||
|
||||
You probably want to restrict this hook to operating only if a certain
|
||||
file is being processed (which is why you get a C<file> argument
|
||||
below). Otherwise, modifying the C<vars> argument will affect every single
|
||||
template in Bugzilla.
|
||||
|
||||
B<Note:> This hook is not called if you are already in this hook.
|
||||
(That is, it won't call itself recursively.) This prevents infinite
|
||||
recursion in situations where this hook needs to process a template
|
||||
(such as if this hook throws an error).
|
||||
|
||||
Params:
|
||||
|
||||
=over
|
||||
|
||||
=item C<vars>
|
||||
|
||||
This is the entire set of variables that the current template can see.
|
||||
Technically, this is a L<Template::Stash> object, but you can just
|
||||
use it like a hashref if you want.
|
||||
|
||||
=item C<file>
|
||||
|
||||
The name of the template file being processed. This is relative to the
|
||||
main template directory for the language (i.e. for
|
||||
F<template/en/default/bug/show.html.tmpl>, this variable will contain
|
||||
C<bug/show.html.tmpl>).
|
||||
|
||||
=item C<context>
|
||||
|
||||
A L<Template::Context> object. Usually you will not have to use this, but
|
||||
if you need information about the template itself (other than just its
|
||||
name), you can get it from here.
|
||||
|
||||
=back
|
||||
|
||||
=head2 webservice
|
||||
|
||||
This hook allows you to add your own modules to the WebService. (See
|
||||
|
@ -557,7 +1032,7 @@ plugins).
|
|||
|
||||
=back
|
||||
|
||||
=head2 webservice-error_codes
|
||||
=head2 webservice_error_codes
|
||||
|
||||
If your webservice extension throws custom errors, you can set numeric
|
||||
codes for those errors here.
|
||||
|
@ -575,3 +1050,7 @@ A hash that maps the names of errors (like C<invalid_param>) to numbers.
|
|||
See L<Bugzilla::WebService::Constants/WS_ERROR_CODE> for an example.
|
||||
|
||||
=back
|
||||
|
||||
=head1 SEE ALSO
|
||||
|
||||
L<Bugzilla::Extension>
|
||||
|
|
|
@ -26,6 +26,8 @@ package Bugzilla::Install;
|
|||
|
||||
use strict;
|
||||
|
||||
use Bugzilla::Component;
|
||||
use Bugzilla::Config qw(:admin);
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::Error;
|
||||
use Bugzilla::Group;
|
||||
|
@ -134,7 +136,10 @@ use constant DEFAULT_PRODUCT => {
|
|||
name => 'TestProduct',
|
||||
description => 'This is a test product.'
|
||||
. ' This ought to be blown away and replaced with real stuff in a'
|
||||
. ' finished installation of bugzilla.'
|
||||
. ' finished installation of bugzilla.',
|
||||
version => Bugzilla::Version::DEFAULT_VERSION,
|
||||
classification => 'Unclassified',
|
||||
defaultmilestone => DEFAULT_MILESTONE,
|
||||
};
|
||||
|
||||
use constant DEFAULT_COMPONENT => {
|
||||
|
@ -189,88 +194,43 @@ sub update_system_groups {
|
|||
$dbh->do('INSERT INTO group_group_map (grantor_id, member_id)
|
||||
VALUES (?,?)', undef, $sudo_protect->id, $sudo->id);
|
||||
}
|
||||
}
|
||||
|
||||
# Re-evaluate all regexps, to keep them up-to-date.
|
||||
my $sth = $dbh->prepare(
|
||||
"SELECT profiles.userid, profiles.login_name, groups.id,
|
||||
groups.userregexp, user_group_map.group_id
|
||||
FROM (profiles CROSS JOIN groups)
|
||||
LEFT JOIN user_group_map
|
||||
ON user_group_map.user_id = profiles.userid
|
||||
AND user_group_map.group_id = groups.id
|
||||
AND user_group_map.grant_type = ?
|
||||
WHERE userregexp != '' OR user_group_map.group_id IS NOT NULL");
|
||||
sub create_default_classification {
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
my $sth_add = $dbh->prepare(
|
||||
"INSERT INTO user_group_map (user_id, group_id, isbless, grant_type)
|
||||
VALUES (?, ?, 0, " . GRANT_REGEXP . ")");
|
||||
|
||||
my $sth_del = $dbh->prepare(
|
||||
"DELETE FROM user_group_map
|
||||
WHERE user_id = ? AND group_id = ? AND isbless = 0
|
||||
AND grant_type = " . GRANT_REGEXP);
|
||||
|
||||
$sth->execute(GRANT_REGEXP);
|
||||
while (my ($uid, $login, $gid, $rexp, $present) = $sth->fetchrow_array()) {
|
||||
if ($login =~ m/$rexp/i) {
|
||||
$sth_add->execute($uid, $gid) unless $present;
|
||||
} else {
|
||||
$sth_del->execute($uid, $gid) if $present;
|
||||
}
|
||||
# Make the default Classification if it doesn't already exist.
|
||||
if (!$dbh->selectrow_array('SELECT 1 FROM classifications')) {
|
||||
print get_text('install_default_classification',
|
||||
{ name => DEFAULT_CLASSIFICATION->{name} }) . "\n";
|
||||
Bugzilla::Classification->create(DEFAULT_CLASSIFICATION);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
# This function should be called only after creating the admin user.
|
||||
sub create_default_product {
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
# Make the default Classification if it doesn't already exist.
|
||||
if (!$dbh->selectrow_array('SELECT 1 FROM classifications')) {
|
||||
my $class = DEFAULT_CLASSIFICATION;
|
||||
print get_text('install_default_classification',
|
||||
{ name => $class->{name} }) . "\n";
|
||||
$dbh->do('INSERT INTO classifications (name, description)
|
||||
VALUES (?, ?)',
|
||||
undef, $class->{name}, $class->{description});
|
||||
}
|
||||
|
||||
# And same for the default product/component.
|
||||
if (!$dbh->selectrow_array('SELECT 1 FROM products')) {
|
||||
my $default_prod = DEFAULT_PRODUCT;
|
||||
print get_text('install_default_product',
|
||||
{ name => $default_prod->{name} }) . "\n";
|
||||
{ name => DEFAULT_PRODUCT->{name} }) . "\n";
|
||||
|
||||
$dbh->do(q{INSERT INTO products (name, description)
|
||||
VALUES (?,?)},
|
||||
undef, $default_prod->{name}, $default_prod->{description});
|
||||
my $product = Bugzilla::Product->create(DEFAULT_PRODUCT);
|
||||
|
||||
my $product = new Bugzilla::Product({name => $default_prod->{name}});
|
||||
|
||||
# The default version.
|
||||
Bugzilla::Version::create(Bugzilla::Version::DEFAULT_VERSION, $product);
|
||||
|
||||
# And we automatically insert the default milestone.
|
||||
$dbh->do(q{INSERT INTO milestones (product_id, value, sortkey)
|
||||
SELECT id, defaultmilestone, 0
|
||||
FROM products});
|
||||
|
||||
# Get the user who will be the owner of the Product.
|
||||
# We pick the admin with the lowest id, or we insert
|
||||
# an invalid "0" into the database, just so that we can
|
||||
# create the component.
|
||||
# Get the user who will be the owner of the Component.
|
||||
# We pick the admin with the lowest id, which is probably the
|
||||
# admin checksetup.pl just created.
|
||||
my $admin_group = new Bugzilla::Group({name => 'admin'});
|
||||
my ($admin_id) = $dbh->selectrow_array(
|
||||
'SELECT user_id FROM user_group_map WHERE group_id = ?
|
||||
ORDER BY user_id ' . $dbh->sql_limit(1),
|
||||
undef, $admin_group->id) || 0;
|
||||
undef, $admin_group->id);
|
||||
my $admin = Bugzilla::User->new($admin_id);
|
||||
|
||||
my $default_comp = DEFAULT_COMPONENT;
|
||||
|
||||
$dbh->do("INSERT INTO components (name, product_id, description,
|
||||
initialowner)
|
||||
VALUES (?, ?, ?, ?)", undef, $default_comp->{name},
|
||||
$product->id, $default_comp->{description}, $admin_id);
|
||||
Bugzilla::Component->create({
|
||||
%{ DEFAULT_COMPONENT() }, product => $product,
|
||||
initialowner => $admin->login });
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -359,6 +319,12 @@ sub make_admin {
|
|||
$group_insert->execute($user->id, $editusers->id, 0, GRANT_DIRECT);
|
||||
};
|
||||
|
||||
# If there is no maintainer set, make this user the maintainer.
|
||||
if (!Bugzilla->params->{'maintainer'}) {
|
||||
SetParam('maintainer', $user->email);
|
||||
write_params();
|
||||
}
|
||||
|
||||
print "\n", get_text('install_admin_created', { user => $user }), "\n";
|
||||
}
|
||||
|
||||
|
@ -450,9 +416,14 @@ Params: none
|
|||
|
||||
Returns: nothing.
|
||||
|
||||
=item C<create_default_classification>
|
||||
|
||||
Creates the default "Unclassified" L<Classification|Bugzilla::Classification>
|
||||
if it doesn't already exist
|
||||
|
||||
=item C<create_default_product()>
|
||||
|
||||
Description: Creates the default product and classification if
|
||||
Description: Creates the default product and component if
|
||||
they don't exist.
|
||||
|
||||
Params: none
|
||||
|
|
|
@ -25,6 +25,7 @@ use Encode;
|
|||
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::Hook;
|
||||
use Bugzilla::Install ();
|
||||
use Bugzilla::Install::Util qw(indicate_progress install_string);
|
||||
use Bugzilla::Util;
|
||||
use Bugzilla::Series;
|
||||
|
@ -415,12 +416,13 @@ sub update_table_definitions {
|
|||
_fix_attachments_submitter_id_idx();
|
||||
_copy_attachments_thedata_to_attach_data();
|
||||
_fix_broken_all_closed_series();
|
||||
|
||||
# 2005-08-14 bugreport@peshkin.net -- Bug 304583
|
||||
# Get rid of leftover DERIVED group permissions
|
||||
use constant GRANT_DERIVED => 1;
|
||||
$dbh->do("DELETE FROM user_group_map WHERE grant_type = " . GRANT_DERIVED);
|
||||
|
||||
_rederive_regex_groups();
|
||||
|
||||
# PUBLIC is a reserved word in Oracle.
|
||||
$dbh->bz_rename_column('series', 'public', 'is_public');
|
||||
|
||||
|
@ -458,10 +460,14 @@ sub update_table_definitions {
|
|||
_move_data_nomail_into_db();
|
||||
|
||||
# The products table lacked sensible defaults.
|
||||
$dbh->bz_alter_column('products', 'milestoneurl',
|
||||
{TYPE => 'TINYTEXT', NOTNULL => 1, DEFAULT => "''"});
|
||||
$dbh->bz_alter_column('products', 'disallownew',
|
||||
{TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 0});
|
||||
if ($dbh->bz_column_info('products', 'milestoneurl')) {
|
||||
$dbh->bz_alter_column('products', 'milestoneurl',
|
||||
{TYPE => 'TINYTEXT', NOTNULL => 1, DEFAULT => "''"});
|
||||
}
|
||||
if ($dbh->bz_column_info('products', 'disallownew')){
|
||||
$dbh->bz_alter_column('products', 'disallownew',
|
||||
{TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 0});
|
||||
}
|
||||
$dbh->bz_alter_column('products', 'votesperuser',
|
||||
{TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0});
|
||||
$dbh->bz_alter_column('products', 'votestoconfirm',
|
||||
|
@ -556,11 +562,52 @@ sub update_table_definitions {
|
|||
# 2009-03-02 arbingersys@gmail.com - Bug 423613
|
||||
_add_extern_id_index();
|
||||
|
||||
# 2009-03-31 LpSolit@gmail.com - Bug 478972
|
||||
$dbh->bz_alter_column('group_control_map', 'entry',
|
||||
{TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'});
|
||||
$dbh->bz_alter_column('group_control_map', 'canedit',
|
||||
{TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'});
|
||||
|
||||
# 2009-01-16 oreomike@gmail.com - Bug 302420
|
||||
$dbh->bz_add_column('whine_events', 'mailifnobugs',
|
||||
{ TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'});
|
||||
|
||||
_convert_disallownew_to_isactive();
|
||||
|
||||
$dbh->bz_alter_column('bugs_activity', 'added',
|
||||
{ TYPE => 'varchar(255)' });
|
||||
$dbh->bz_add_index('bugs_activity', 'bugs_activity_added_idx', ['added']);
|
||||
|
||||
# 2009-09-28 LpSolit@gmail.com - Bug 519032
|
||||
$dbh->bz_drop_column('series', 'last_viewed');
|
||||
|
||||
# 2009-09-28 LpSolit@gmail.com - Bug 399073
|
||||
_fix_logincookies_ipaddr();
|
||||
|
||||
# 2009-11-01 LpSolit@gmail.com - Bug 525025
|
||||
_fix_invalid_custom_field_names();
|
||||
|
||||
_set_attachment_comment_types();
|
||||
|
||||
$dbh->bz_drop_column('products', 'milestoneurl');
|
||||
|
||||
_add_allows_unconfirmed_to_product_table();
|
||||
_convert_flagtypes_fks_to_set_null();
|
||||
_fix_decimal_types();
|
||||
_fix_series_creator_fk();
|
||||
|
||||
################################################################
|
||||
# New --TABLE-- changes should go *** A B O V E *** this point #
|
||||
################################################################
|
||||
|
||||
Bugzilla::Hook::process('install-update_db');
|
||||
Bugzilla::Hook::process('install_update_db');
|
||||
|
||||
# We do this here because otherwise the foreign key from
|
||||
# products.classification_id to classifications.id will fail
|
||||
# (because products.classification_id defaults to "1", so on upgraded
|
||||
# installations it's already been set before the first Classification
|
||||
# exists).
|
||||
Bugzilla::Install::create_default_classification();
|
||||
|
||||
$dbh->bz_setup_foreign_keys();
|
||||
}
|
||||
|
@ -579,10 +626,11 @@ sub _update_pre_checksetup_bugzillas {
|
|||
$dbh->bz_add_column('bugs', 'qa_contact', {TYPE => 'INT3'});
|
||||
$dbh->bz_add_column('bugs', 'status_whiteboard',
|
||||
{TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"});
|
||||
$dbh->bz_add_column('products', 'disallownew',
|
||||
{TYPE => 'BOOLEAN', NOTNULL => 1}, 0);
|
||||
$dbh->bz_add_column('products', 'milestoneurl',
|
||||
{TYPE => 'TINYTEXT', NOTNULL => 1}, '');
|
||||
if (!$dbh->bz_column_info('products', 'isactive')){
|
||||
$dbh->bz_add_column('products', 'disallownew',
|
||||
{TYPE => 'BOOLEAN', NOTNULL => 1}, 0);
|
||||
}
|
||||
|
||||
$dbh->bz_add_column('components', 'initialqacontact',
|
||||
{TYPE => 'TINYTEXT'});
|
||||
$dbh->bz_add_column('components', 'description',
|
||||
|
@ -1215,7 +1263,7 @@ sub _use_ip_instead_of_hostname_in_logincookies {
|
|||
# Now update the logincookies schema
|
||||
$dbh->bz_drop_column("logincookies", "hostname");
|
||||
$dbh->bz_add_column("logincookies", "ipaddr",
|
||||
{TYPE => 'varchar(40)', NOTNULL => 1}, '');
|
||||
{TYPE => 'varchar(40)'});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1838,7 +1886,6 @@ sub _setup_usebuggroups_backward_compatibility {
|
|||
#
|
||||
# If group_control_map is empty, backward-compatibility
|
||||
# usebuggroups-equivalent records should be created.
|
||||
my $entry = Bugzilla->params->{'useentrygroupdefault'};
|
||||
my ($maps_exist) = $dbh->selectrow_array(
|
||||
"SELECT DISTINCT 1 FROM group_control_map");
|
||||
if (!$maps_exist) {
|
||||
|
@ -1855,11 +1902,9 @@ sub _setup_usebuggroups_backward_compatibility {
|
|||
if ($groupname eq $productname) {
|
||||
# Product and group have same name.
|
||||
$dbh->do("INSERT INTO group_control_map " .
|
||||
"(group_id, product_id, entry, membercontrol, " .
|
||||
"othercontrol, canedit) " .
|
||||
"VALUES ($groupid, $productid, $entry, " .
|
||||
CONTROLMAPDEFAULT . ", " .
|
||||
CONTROLMAPNA . ", 0)");
|
||||
"(group_id, product_id, membercontrol, othercontrol) " .
|
||||
"VALUES (?, ?, ?, ?)", undef,
|
||||
($groupid, $productid, CONTROLMAPDEFAULT, CONTROLMAPNA));
|
||||
} else {
|
||||
# See if this group is a product group at all.
|
||||
my $sth2 = $dbh->prepare("SELECT id FROM products
|
||||
|
@ -1870,11 +1915,9 @@ sub _setup_usebuggroups_backward_compatibility {
|
|||
# If there is no product with the same name as this
|
||||
# group, then it is permitted for all products.
|
||||
$dbh->do("INSERT INTO group_control_map " .
|
||||
"(group_id, product_id, entry, membercontrol, " .
|
||||
"othercontrol, canedit) " .
|
||||
"VALUES ($groupid, $productid, 0, " .
|
||||
CONTROLMAPSHOWN . ", " .
|
||||
CONTROLMAPNA . ", 0)");
|
||||
"(group_id, product_id, membercontrol, othercontrol) " .
|
||||
"VALUES (?, ?, ?, ?)", undef,
|
||||
($groupid, $productid, CONTROLMAPSHOWN, CONTROLMAPNA));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2199,17 +2242,9 @@ sub _clone_email_event {
|
|||
my ($source, $target) = @_;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
my $sth1 = $dbh->prepare("SELECT user_id, relationship FROM email_setting
|
||||
WHERE event = $source");
|
||||
my $sth2 = $dbh->prepare("INSERT into email_setting " .
|
||||
"(user_id, relationship, event) VALUES (" .
|
||||
"?, ?, $target)");
|
||||
|
||||
$sth1->execute();
|
||||
|
||||
while (my ($userid, $relationship) = $sth1->fetchrow_array()) {
|
||||
$sth2->execute($userid, $relationship);
|
||||
}
|
||||
$dbh->do("INSERT INTO email_setting (user_id, relationship, event)
|
||||
SELECT user_id, relationship, $target FROM email_setting
|
||||
WHERE event = $source");
|
||||
}
|
||||
|
||||
sub _migrate_email_prefs_to_new_table {
|
||||
|
@ -2325,10 +2360,11 @@ sub _initialize_dependency_tree_changes_email_pref {
|
|||
|
||||
foreach my $desc (keys %events) {
|
||||
my $event = $events{$desc};
|
||||
my $sth = $dbh->prepare("SELECT COUNT(*) FROM email_setting
|
||||
WHERE event = $event");
|
||||
$sth->execute();
|
||||
if (!($sth->fetchrow_arrayref()->[0])) {
|
||||
my $have_events = $dbh->selectrow_array(
|
||||
"SELECT 1 FROM email_setting WHERE event = $event "
|
||||
. $dbh->sql_limit(1));
|
||||
|
||||
if (!$have_events) {
|
||||
# No settings in the table yet, so we assume that this is the
|
||||
# first time it's being set.
|
||||
print "Initializing \"$desc\" email_setting ...\n";
|
||||
|
@ -2679,6 +2715,54 @@ EOT
|
|||
} # if (@$broken_nonopen_series)
|
||||
}
|
||||
|
||||
# This needs to happen at two times: when we upgrade from 2.16 (thus creating
|
||||
# user_group_map), and when we kill derived gruops in the DB.
|
||||
sub _rederive_regex_groups {
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
my $regex_groups_exist = $dbh->selectrow_array(
|
||||
"SELECT 1 FROM groups WHERE userregexp = '' " . $dbh->sql_limit(1));
|
||||
return if !$regex_groups_exist;
|
||||
|
||||
my $regex_derivations = $dbh->selectrow_array(
|
||||
'SELECT 1 FROM user_group_map WHERE grant_type = ' . GRANT_REGEXP
|
||||
. ' ' . $dbh->sql_limit(1));
|
||||
return if $regex_derivations;
|
||||
|
||||
print "Deriving regex group memberships...\n";
|
||||
|
||||
# Re-evaluate all regexps, to keep them up-to-date.
|
||||
my $sth = $dbh->prepare(
|
||||
"SELECT profiles.userid, profiles.login_name, groups.id,
|
||||
groups.userregexp, user_group_map.group_id
|
||||
FROM (profiles CROSS JOIN groups)
|
||||
LEFT JOIN user_group_map
|
||||
ON user_group_map.user_id = profiles.userid
|
||||
AND user_group_map.group_id = groups.id
|
||||
AND user_group_map.grant_type = ?
|
||||
WHERE userregexp != '' OR user_group_map.group_id IS NOT NULL");
|
||||
|
||||
my $sth_add = $dbh->prepare(
|
||||
"INSERT INTO user_group_map (user_id, group_id, isbless, grant_type)
|
||||
VALUES (?, ?, 0, " . GRANT_REGEXP . ")");
|
||||
|
||||
my $sth_del = $dbh->prepare(
|
||||
"DELETE FROM user_group_map
|
||||
WHERE user_id = ? AND group_id = ? AND isbless = 0
|
||||
AND grant_type = " . GRANT_REGEXP);
|
||||
|
||||
$sth->execute(GRANT_REGEXP);
|
||||
while (my ($uid, $login, $gid, $rexp, $present) =
|
||||
$sth->fetchrow_array())
|
||||
{
|
||||
if ($login =~ m/$rexp/i) {
|
||||
$sth_add->execute($uid, $gid) unless $present;
|
||||
} else {
|
||||
$sth_del->execute($uid, $gid) if $present;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sub _clean_control_characters_from_short_desc {
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
|
@ -2826,13 +2910,13 @@ sub _move_data_nomail_into_db {
|
|||
SET disable_mail = 1
|
||||
WHERE userid = ?');
|
||||
foreach my $user_to_check (keys %nomail) {
|
||||
my $uid;
|
||||
if ($uid = Bugzilla::User::login_to_id($user_to_check)) {
|
||||
my $user = new Bugzilla::User($uid);
|
||||
print "\tDisabling email for user ", $user->email, "\n";
|
||||
$query->execute($user->id);
|
||||
delete $nomail{$user->email};
|
||||
}
|
||||
my $uid = $dbh->selectrow_array(
|
||||
'SELECT userid FROM profiles WHERE login_name = ?',
|
||||
undef, $user_to_check);
|
||||
next if !$uid;
|
||||
print "\tDisabling email for user $user_to_check\n";
|
||||
$query->execute($uid);
|
||||
delete $nomail{$user_to_check};
|
||||
}
|
||||
|
||||
# If there are any nomail entries remaining, move them to nomail.bad
|
||||
|
@ -2929,11 +3013,8 @@ sub _initialize_workflow {
|
|||
# and mark these statuses as 'closed', even if some of these statuses are
|
||||
# expected to be open statuses. Bug statuses we have no information about
|
||||
# are left as 'open'.
|
||||
my @closed_statuses =
|
||||
@{$dbh->selectcol_arrayref('SELECT DISTINCT bug_status FROM bugs
|
||||
WHERE resolution != ?', undef, '')};
|
||||
|
||||
# Append the default list of closed statuses *unless* we detect at least
|
||||
#
|
||||
# We append the default list of closed statuses *unless* we detect at least
|
||||
# one closed state in the DB (i.e. with is_open = 0). This would mean that
|
||||
# the DB has already been updated at least once and maybe the admin decided
|
||||
# that e.g. 'RESOLVED' is now an open state, in which case we don't want to
|
||||
|
@ -2944,6 +3025,9 @@ sub _initialize_workflow {
|
|||
WHERE is_open = 0');
|
||||
|
||||
if (!$num_closed_states) {
|
||||
my @closed_statuses =
|
||||
@{$dbh->selectcol_arrayref('SELECT DISTINCT bug_status FROM bugs
|
||||
WHERE resolution != ?', undef, '')};
|
||||
@closed_statuses =
|
||||
map {$dbh->quote($_)} (@closed_statuses, qw(RESOLVED VERIFIED CLOSED));
|
||||
|
||||
|
@ -3151,26 +3235,33 @@ sub _add_foreign_keys_to_multiselects {
|
|||
}
|
||||
}
|
||||
|
||||
# This subroutine is used in multiple places (for times when we update
|
||||
# the text of comments), so it takes an argument, $bug_ids, which causes
|
||||
# it to update bugs_fulltext for those bug_ids instead of populating the
|
||||
# whole table.
|
||||
sub _populate_bugs_fulltext
|
||||
{
|
||||
my $bug_ids = shift;
|
||||
$bug_ids = undef if $bug_ids && !@$bug_ids;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $fulltext = $dbh->selectrow_array
|
||||
("SELECT 1 FROM bugs_fulltext ".$dbh->sql_limit(1));
|
||||
# We only populate the table if it's empty...
|
||||
if (!$fulltext)
|
||||
# We only populate the table if it's empty or if we've been given a
|
||||
# set of bug ids.
|
||||
if ($bug_ids || !$fulltext)
|
||||
{
|
||||
# ... and if there are bugs in the bugs table.
|
||||
my @bug_ids = @{ $dbh->selectcol_arrayref("SELECT bug_id FROM bugs") };
|
||||
return if !@bug_ids;
|
||||
$bug_ids ||= $dbh->selectcol_arrayref("SELECT bug_id FROM bugs");
|
||||
return if !$bug_ids;
|
||||
|
||||
# Bug 46221 - Russian Stemming in Bugzilla fulltext search
|
||||
# We can't use GROUP_CONCAT because we need to stem each word
|
||||
# And there could be tons of bugs, so we'll use N-bug portions
|
||||
print "Populating bugs_fulltext... (this can take a long time.)\n";
|
||||
my ($portion, $done, $total) = (256, 0, scalar @bug_ids);
|
||||
my ($portion, $done, $total) = (256, 0, scalar @$bug_ids);
|
||||
my ($short, $all, $nopriv, $wh, $rows);
|
||||
my ($sth, $sthn) = (undef, 0);
|
||||
while (my @ids = splice @bug_ids, 0, $portion)
|
||||
while (my @ids = splice @$bug_ids, 0, $portion)
|
||||
{
|
||||
$rows = {};
|
||||
$wh = "bug_id IN (" . join(",", ("?") x @ids) . ")";
|
||||
|
@ -3195,7 +3286,7 @@ sub _populate_bugs_fulltext
|
|||
$sthn = @ids;
|
||||
$sth = $dbh->prepare(
|
||||
"INSERT INTO bugs_fulltext (bug_id, short_desc, comments, comments_noprivate)" .
|
||||
" VALUES " . join(",", ("(?,?,?,?)") x @ids)
|
||||
" VALUES " . join(",", ("(?,?,?,?)") x @ids) . " ON UPDATE SET bug_id=bug_id"
|
||||
);
|
||||
}
|
||||
$sth->execute(map { ($_, @{$rows->{$_}}) } @ids);
|
||||
|
@ -3226,6 +3317,155 @@ sub _add_extern_id_index {
|
|||
}
|
||||
}
|
||||
|
||||
sub _convert_disallownew_to_isactive {
|
||||
my $dbh = Bugzilla->dbh;
|
||||
if ($dbh->bz_column_info('products', 'disallownew')){
|
||||
$dbh->bz_add_column('products', 'isactive',
|
||||
{ TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'});
|
||||
|
||||
# isactive is the boolean reverse of disallownew.
|
||||
$dbh->do('UPDATE products SET isactive = 0 WHERE disallownew = 1');
|
||||
$dbh->do('UPDATE products SET isactive = 1 WHERE disallownew = 0');
|
||||
|
||||
$dbh->bz_drop_column('products','disallownew');
|
||||
}
|
||||
}
|
||||
|
||||
sub _fix_logincookies_ipaddr {
|
||||
my $dbh = Bugzilla->dbh;
|
||||
return if !$dbh->bz_column_info('logincookies', 'ipaddr')->{NOTNULL};
|
||||
|
||||
$dbh->bz_alter_column('logincookies', 'ipaddr', {TYPE => 'varchar(40)'});
|
||||
$dbh->do('UPDATE logincookies SET ipaddr = NULL WHERE ipaddr = ?',
|
||||
undef, '0.0.0.0');
|
||||
}
|
||||
|
||||
sub _fix_invalid_custom_field_names {
|
||||
my @fields = Bugzilla->get_fields({ custom => 1 });
|
||||
|
||||
foreach my $field (@fields) {
|
||||
next if $field->name =~ /^[a-zA-Z0-9_]+$/;
|
||||
# The field name is illegal and can break the DB. Kill the field!
|
||||
$field->set_obsolete(1);
|
||||
eval { $field->remove_from_db(); };
|
||||
print "Removing custom field '" . $field->name . "' (illegal name)... ";
|
||||
print $@ ? "failed\n$@\n" : "succeeded\n";
|
||||
}
|
||||
}
|
||||
|
||||
sub _set_attachment_comment_type {
|
||||
my ($type, $string) = @_;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
# We check if there are any comments of this type already, first,
|
||||
# because this is faster than a full LIKE search on the comments,
|
||||
# and currently this will run every time we run checksetup.
|
||||
my $test = $dbh->selectrow_array(
|
||||
"SELECT 1 FROM longdescs WHERE type = $type " . $dbh->sql_limit(1));
|
||||
return [] if $test;
|
||||
my %comments = @{ $dbh->selectcol_arrayref(
|
||||
"SELECT comment_id, thetext FROM longdescs
|
||||
WHERE thetext LIKE '$string%'",
|
||||
{Columns=>[1,2]}) };
|
||||
my @comment_ids = keys %comments;
|
||||
return [] if !scalar @comment_ids;
|
||||
my $what = "update";
|
||||
if ($type == CMT_ATTACHMENT_CREATED) {
|
||||
$what = "creation";
|
||||
}
|
||||
print "Setting the type field on attachment $what comments...\n";
|
||||
my $sth = $dbh->prepare(
|
||||
'UPDATE longdescs SET thetext = ?, type = ?, extra_data = ?
|
||||
WHERE comment_id = ?');
|
||||
my $count = 0;
|
||||
my $total = scalar @comment_ids;
|
||||
foreach my $id (@comment_ids) {
|
||||
$count++;
|
||||
my $text = $comments{$id};
|
||||
next if $text !~ /^\Q$string\E(\d+)/;
|
||||
my $attachment_id = $1;
|
||||
my @lines = split("\n", $text);
|
||||
if ($type == CMT_ATTACHMENT_CREATED) {
|
||||
# Now we have to remove the text up until we find a line that's
|
||||
# just a single newline, because the old "Created an attachment"
|
||||
# text included the attachment description underneath it, and in
|
||||
# Bugzillas before 2.20, that could be wrapped into multiple lines,
|
||||
# in the database.
|
||||
while (1) {
|
||||
my $line = shift @lines;
|
||||
last if (!defined $line or trim($line) eq '');
|
||||
}
|
||||
}
|
||||
else {
|
||||
# However, the "From update of attachment" line is always just
|
||||
# one line--the first line of the comment.
|
||||
shift @lines;
|
||||
}
|
||||
$text = join("\n", @lines);
|
||||
$sth->execute($text, $type, $attachment_id, $id);
|
||||
indicate_progress({ total => $total, current => $count,
|
||||
every => 25 });
|
||||
}
|
||||
return \@comment_ids;
|
||||
}
|
||||
|
||||
sub _set_attachment_comment_types {
|
||||
my $dbh = Bugzilla->dbh;
|
||||
$dbh->bz_start_transaction();
|
||||
my $created_ids = _set_attachment_comment_type(
|
||||
CMT_ATTACHMENT_CREATED, 'Created an attachment (id=');
|
||||
my $updated_ids = _set_attachment_comment_type(
|
||||
CMT_ATTACHMENT_UPDATED, '(From update of attachment ');
|
||||
$dbh->bz_commit_transaction();
|
||||
return unless (@$created_ids or @$updated_ids);
|
||||
|
||||
my @comment_ids = (@$created_ids, @$updated_ids);
|
||||
|
||||
my $bug_ids = $dbh->selectcol_arrayref(
|
||||
'SELECT DISTINCT bug_id FROM longdescs WHERE '
|
||||
. $dbh->sql_in('comment_id', \@comment_ids));
|
||||
_populate_bugs_fulltext($bug_ids);
|
||||
}
|
||||
|
||||
sub _add_allows_unconfirmed_to_product_table {
|
||||
my $dbh = Bugzilla->dbh;
|
||||
if (!$dbh->bz_column_info('products', 'allows_unconfirmed')) {
|
||||
$dbh->bz_add_column('products', 'allows_unconfirmed',
|
||||
{ TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE' });
|
||||
$dbh->do('UPDATE products SET allows_unconfirmed = 1
|
||||
WHERE votestoconfirm > 0');
|
||||
}
|
||||
}
|
||||
|
||||
sub _convert_flagtypes_fks_to_set_null {
|
||||
my $dbh = Bugzilla->dbh;
|
||||
foreach my $column (qw(request_group_id grant_group_id)) {
|
||||
my $fk = $dbh->bz_fk_info('flagtypes', $column);
|
||||
if ($fk and !defined $fk->{DELETE}) {
|
||||
# checksetup will re-create the FK with the appropriate definition
|
||||
# at the end of its table upgrades, so we just drop it here.
|
||||
$dbh->bz_drop_fk('flagtypes', $column);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sub _fix_decimal_types {
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $type = {TYPE => 'decimal(7,2)', NOTNULL => 1, DEFAULT => '0'};
|
||||
$dbh->bz_alter_column('bugs', 'estimated_time', $type);
|
||||
$dbh->bz_alter_column('bugs', 'remaining_time', $type);
|
||||
$dbh->bz_alter_column('longdescs', 'work_time', $type);
|
||||
}
|
||||
|
||||
sub _fix_series_creator_fk {
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $fk = $dbh->bz_fk_info('series', 'creator');
|
||||
# Change the FK from SET NULL to CASCADE. (It will be re-created
|
||||
# automatically at the end of all DB changes.)
|
||||
if ($fk and $fk->{DELETE} eq 'SET NULL') {
|
||||
$dbh->bz_drop_fk('series', 'creator');
|
||||
}
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
|
|
@ -30,6 +30,7 @@ use strict;
|
|||
use Bugzilla::Constants;
|
||||
use Bugzilla::Error;
|
||||
use Bugzilla::Install::Localconfig;
|
||||
use Bugzilla::Install::Util qw(install_string);
|
||||
use Bugzilla::Util;
|
||||
|
||||
use File::Find;
|
||||
|
@ -44,6 +45,7 @@ our @EXPORT = qw(
|
|||
update_filesystem
|
||||
create_htaccess
|
||||
fix_all_file_permissions
|
||||
fix_file_permissions
|
||||
);
|
||||
|
||||
use constant HT_DEFAULT_DENY => <<EOT;
|
||||
|
@ -58,10 +60,10 @@ EOT
|
|||
# a perldoc. However, look at the various hashes defined inside this
|
||||
# function to understand what it returns. (There are comments throughout.)
|
||||
#
|
||||
# The rationale for the file permissions is that the web server generally
|
||||
# runs as apache, so the cgi scripts should not be writable for apache,
|
||||
# otherwise someone may find it possible to change the cgis when exploiting
|
||||
# some security flaw somewhere (not necessarily in Bugzilla!)
|
||||
# The rationale for the file permissions is that there is a group the
|
||||
# web server executes the scripts as, so the cgi scripts should not be writable
|
||||
# by this group. Otherwise someone may find it possible to change the cgis
|
||||
# when exploiting some security flaw somewhere (not necessarily in Bugzilla!)
|
||||
sub FILESYSTEM {
|
||||
my $datadir = bz_locations()->{'datadir'};
|
||||
my $attachdir = bz_locations()->{'attachdir'};
|
||||
|
@ -74,6 +76,7 @@ sub FILESYSTEM {
|
|||
my $localconfig = bz_locations()->{'localconfig'};
|
||||
|
||||
my $ws_group = Bugzilla->localconfig->{'webservergroup'};
|
||||
my $use_suexec = Bugzilla->localconfig->{'use_suexec'};
|
||||
|
||||
# The set of permissions that we use:
|
||||
|
||||
|
@ -83,7 +86,7 @@ sub FILESYSTEM {
|
|||
# Executable by the owner only.
|
||||
my $owner_executable = 0700;
|
||||
# Readable by the web server.
|
||||
my $ws_readable = $ws_group ? 0640 : 0644;
|
||||
my $ws_readable = ($ws_group && !$use_suexec) ? 0640 : 0644;
|
||||
# Readable by the owner only.
|
||||
my $owner_readable = 0600;
|
||||
# Writeable by the web server.
|
||||
|
@ -91,7 +94,7 @@ sub FILESYSTEM {
|
|||
|
||||
# DIRECTORIES
|
||||
# Readable by the web server.
|
||||
my $ws_dir_readable = $ws_group ? 0750 : 0755;
|
||||
my $ws_dir_readable = ($ws_group && !$use_suexec) ? 0750 : 0755;
|
||||
# Readable only by the owner.
|
||||
my $owner_dir_readable = 0700;
|
||||
# Writeable by the web server.
|
||||
|
@ -124,6 +127,7 @@ sub FILESYSTEM {
|
|||
'email_in.pl' => { perms => $ws_executable },
|
||||
'sanitycheck.pl' => { perms => $ws_executable },
|
||||
'jobqueue.pl' => { perms => $owner_executable },
|
||||
'migrate.pl' => { perms => $owner_executable },
|
||||
'install-module.pl' => { perms => $owner_executable },
|
||||
|
||||
"$localconfig.old" => { perms => $owner_readable },
|
||||
|
@ -134,9 +138,9 @@ sub FILESYSTEM {
|
|||
'docs/style.css' => { perms => $ws_readable },
|
||||
'docs/*/rel_notes.txt' => { perms => $ws_readable },
|
||||
'docs/*/README.docs' => { perms => $owner_readable },
|
||||
"$datadir/bugzilla-update.xml" => { perms => $ws_writeable },
|
||||
"$datadir/params" => { perms => $ws_writeable },
|
||||
"$datadir/old-params.txt" => { perms => $owner_readable },
|
||||
"$extensionsdir/create.pl" => { perms => $owner_executable },
|
||||
);
|
||||
|
||||
# Directories that we want to set the perms on, but not
|
||||
|
@ -165,8 +169,6 @@ sub FILESYSTEM {
|
|||
# Readable directories
|
||||
"$datadir/mining" => { files => $ws_readable,
|
||||
dirs => $ws_dir_readable },
|
||||
"$datadir/duplicates" => { files => $ws_readable,
|
||||
dirs => $ws_dir_readable },
|
||||
"$libdir/Bugzilla" => { files => $ws_readable,
|
||||
dirs => $ws_dir_readable },
|
||||
$extlib => { files => $ws_readable,
|
||||
|
@ -212,7 +214,7 @@ sub FILESYSTEM {
|
|||
my %create_dirs = (
|
||||
$datadir => $ws_dir_full_control,
|
||||
"$datadir/mining" => $ws_dir_readable,
|
||||
"$datadir/duplicates" => $ws_dir_readable,
|
||||
"$datadir/extensions" => $ws_dir_readable,
|
||||
$attachdir => $ws_dir_writeable,
|
||||
$extensionsdir => $ws_dir_readable,
|
||||
graphs => $ws_dir_writeable,
|
||||
|
@ -224,6 +226,8 @@ sub FILESYSTEM {
|
|||
# The name of each file, pointing at its default permissions and
|
||||
# default contents.
|
||||
my %create_files = (
|
||||
"$datadir/extensions/additional" => { perms => $ws_readable,
|
||||
contents => '' },
|
||||
# We create this file so that it always has the right owner
|
||||
# and permissions. Otherwise, the webserver creates it as
|
||||
# owned by itself, which can cause problems if jobqueue.pl
|
||||
|
@ -356,10 +360,10 @@ sub update_filesystem {
|
|||
foreach my $dir (sort keys %dirs) {
|
||||
unless (-d $dir) {
|
||||
print "Creating $dir directory...\n";
|
||||
mkdir $dir || die $!;
|
||||
mkdir $dir or die "mkdir $dir failed: $!";
|
||||
# For some reason, passing in the permissions to "mkdir"
|
||||
# doesn't work right, but doing a "chmod" does.
|
||||
chmod $dirs{$dir}, $dir || die $!;
|
||||
chmod $dirs{$dir}, $dir or warn "Cannot chmod $dir: $!";
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -411,6 +415,11 @@ EOT
|
|||
unlink "$datadir/duplicates.rdf";
|
||||
unlink "$datadir/duplicates-old.rdf";
|
||||
}
|
||||
|
||||
if (-e "$datadir/duplicates") {
|
||||
print "Removing duplicates directory...\n";
|
||||
rmtree("$datadir/duplicates");
|
||||
}
|
||||
}
|
||||
|
||||
# A simple helper for creating "empty" CSS files.
|
||||
|
@ -573,12 +582,20 @@ sub _update_old_charts {
|
|||
}
|
||||
}
|
||||
|
||||
sub fix_file_permissions {
|
||||
my ($file) = @_;
|
||||
return if ON_WINDOWS;
|
||||
my $perms = FILESYSTEM()->{all_files}->{$file}->{perms};
|
||||
# Note that _get_owner_and_group is always silent here.
|
||||
my ($owner_id, $group_id) = _get_owner_and_group();
|
||||
_fix_perms($file, $owner_id, $group_id, $perms);
|
||||
}
|
||||
|
||||
sub fix_all_file_permissions {
|
||||
my ($output) = @_;
|
||||
|
||||
my $ws_group = Bugzilla->localconfig->{'webservergroup'};
|
||||
my $group_id = _check_web_server_group($ws_group, $output);
|
||||
# _get_owner_and_group also checks that the webservergroup is valid.
|
||||
my ($owner_id, $group_id) = _get_owner_and_group($output);
|
||||
|
||||
return if ON_WINDOWS;
|
||||
|
||||
|
@ -589,9 +606,6 @@ sub fix_all_file_permissions {
|
|||
|
||||
print get_text('install_file_perms_fix') . "\n" if $output;
|
||||
|
||||
my $owner_id = POSIX::getuid();
|
||||
$group_id = POSIX::getgid() unless defined $group_id;
|
||||
|
||||
foreach my $dir (sort keys %dirs) {
|
||||
next unless -d $dir;
|
||||
_fix_perms($dir, $owner_id, $group_id, $dirs{$dir});
|
||||
|
@ -619,6 +633,16 @@ sub fix_all_file_permissions {
|
|||
_fix_cvs_dirs($owner_id, '.');
|
||||
}
|
||||
|
||||
sub _get_owner_and_group {
|
||||
my ($output) = @_;
|
||||
my $group_id = _check_web_server_group($output);
|
||||
return () if ON_WINDOWS;
|
||||
|
||||
my $owner_id = POSIX::getuid();
|
||||
$group_id = POSIX::getgid() unless defined $group_id;
|
||||
return ($owner_id, $group_id);
|
||||
}
|
||||
|
||||
# A helper for fix_all_file_permissions
|
||||
sub _fix_cvs_dirs {
|
||||
my ($owner_id, $dir) = @_;
|
||||
|
@ -640,10 +664,16 @@ sub _fix_cvs_dirs {
|
|||
sub _fix_perms {
|
||||
my ($name, $owner, $group, $perms) = @_;
|
||||
#printf ("Changing $name to %o\n", $perms);
|
||||
chown $owner, $group, $name
|
||||
|| warn "Failed to change ownership of $name: $!";
|
||||
|
||||
# The webserver should never try to chown files.
|
||||
if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
|
||||
chown $owner, $group, $name
|
||||
or warn install_string('chown_failed', { path => $name,
|
||||
error => $! }) . "\n";
|
||||
}
|
||||
chmod $perms, $name
|
||||
|| warn "Failed to change permissions of $name: $!";
|
||||
or warn install_string('chmod_failed', { path => $name,
|
||||
error => $! }) . "\n";
|
||||
}
|
||||
|
||||
sub _fix_perms_recursively {
|
||||
|
@ -664,8 +694,9 @@ sub _fix_perms_recursively {
|
|||
}
|
||||
|
||||
sub _check_web_server_group {
|
||||
my ($group, $output) = @_;
|
||||
my ($output) = @_;
|
||||
|
||||
my $group = Bugzilla->localconfig->{'webservergroup'};
|
||||
my $filename = bz_locations()->{'localconfig'};
|
||||
my $group_id;
|
||||
|
||||
|
@ -749,4 +780,10 @@ Params: C<$output> - C<true> if you want this function to print
|
|||
|
||||
Returns: nothing
|
||||
|
||||
=item C<fix_file_permissions>
|
||||
|
||||
Given the name of a file, its permissions will be fixed according to
|
||||
how they are supposed to be set in Bugzilla's current configuration.
|
||||
If it fails to set the permissions, a warning will be printed to STDERR.
|
||||
|
||||
=back
|
||||
|
|
|
@ -67,9 +67,11 @@ EOT
|
|||
{
|
||||
name => 'webservergroup',
|
||||
default => ON_WINDOWS ? '' : 'apache',
|
||||
desc => q{# This is the group your web server runs as.
|
||||
desc => q{# Usually, this is the group your web server runs as.
|
||||
# If you have a Windows box, ignore this setting.
|
||||
# If you do not have access to the group your web server runs under,
|
||||
# If you have use_suexec switched on below, this is the group Apache switches
|
||||
# to in order to run Bugzilla scripts.
|
||||
# If you do not have access to the group your scripts will run under,
|
||||
# set this to "". If you do set this to "", then your Bugzilla installation
|
||||
# will be _VERY_ insecure, because some files will be world readable/writable,
|
||||
# and so anyone who can get local access to your machine can do whatever they
|
||||
|
@ -77,6 +79,21 @@ EOT
|
|||
# and you cannot set this up any other way. YOU HAVE BEEN WARNED!
|
||||
# If you set this to anything other than "", you will need to run checksetup.pl
|
||||
# as} . ROOT_USER . qq{, or as a user who is a member of the specified group.\n}
|
||||
},
|
||||
{
|
||||
name => 'use_suexec',
|
||||
default => 0,
|
||||
desc => <<EOT
|
||||
# Set this if Bugzilla runs in an Apache SuexecUserGroup environment.
|
||||
# (If your web server runs control panel software (cPanel, Plesk or similar),
|
||||
# or if your Bugzilla is to run in a shared hosting environment, then you are
|
||||
# almost certainly in an Apache SuexecUserGroup environment.)
|
||||
# If you have a Windows box, ignore this setting.
|
||||
# If set to 0, Bugzilla will set file permissions as tightly as possible.
|
||||
# If set to 1, Bugzilla will set file permissions so that it may work in an
|
||||
# SuexecUserGroup environment. The difference is that static files (CSS,
|
||||
# JavaScript and so on) will receive world read permissions.
|
||||
EOT
|
||||
},
|
||||
{
|
||||
name => 'db_driver',
|
||||
|
@ -309,7 +326,12 @@ sub update_localconfig {
|
|||
if (!defined $localconfig->{$name}) {
|
||||
push(@new_vars, $name);
|
||||
$var->{default} = &{$var->{default}} if ref($var->{default}) eq 'CODE';
|
||||
$localconfig->{$name} = $answer->{$name} || $var->{default};
|
||||
if (exists $answer->{$name}) {
|
||||
$localconfig->{$name} = $answer->{$name};
|
||||
}
|
||||
else {
|
||||
$localconfig->{$name} = $var->{default};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -353,11 +375,11 @@ EOT
|
|||
|
||||
# Re-write localconfig
|
||||
open(my $fh, ">$filename") || die "$filename: $!";
|
||||
foreach my $var (LOCALCONFIG_VARS) {
|
||||
print $fh "\n", $var->{desc},
|
||||
Data::Dumper->Dump([$localconfig->{$var->{name}}],
|
||||
["*$var->{name}"]);
|
||||
}
|
||||
foreach my $var (LOCALCONFIG_VARS) {
|
||||
print $fh "\n", $var->{desc},
|
||||
Data::Dumper->Dump([$localconfig->{$var->{name}}],
|
||||
["*$var->{name}"]);
|
||||
}
|
||||
|
||||
if (@new_vars) {
|
||||
my $newstuff = join(', ', @new_vars);
|
||||
|
|
|
@ -26,19 +26,23 @@ package Bugzilla::Install::Requirements;
|
|||
use strict;
|
||||
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::Extension;
|
||||
use Bugzilla::Install::Util qw(vers_cmp install_string);
|
||||
use List::Util qw(max);
|
||||
use Safe;
|
||||
use Term::ANSIColor;
|
||||
|
||||
use base qw(Exporter);
|
||||
our @EXPORT = qw(
|
||||
REQUIRED_MODULES
|
||||
OPTIONAL_MODULES
|
||||
FEATURE_FILES
|
||||
|
||||
check_requirements
|
||||
check_graphviz
|
||||
have_vers
|
||||
install_command
|
||||
map_files_to_features
|
||||
);
|
||||
|
||||
# This is how many *'s are in the top of each "box" message printed
|
||||
|
@ -135,9 +139,9 @@ sub REQUIRED_MODULES {
|
|||
},
|
||||
);
|
||||
|
||||
my $all_modules = _get_extension_requirements(
|
||||
'REQUIRED_MODULES', \@modules);
|
||||
return $all_modules;
|
||||
my $extra_modules = _get_extension_requirements('REQUIRED_MODULES');
|
||||
push(@modules, @$extra_modules);
|
||||
return \@modules;
|
||||
};
|
||||
|
||||
sub OPTIONAL_MODULES {
|
||||
|
@ -146,13 +150,14 @@ sub OPTIONAL_MODULES {
|
|||
package => 'GD',
|
||||
module => 'GD',
|
||||
version => '1.20',
|
||||
feature => 'Graphical Reports, New Charts, Old Charts'
|
||||
feature => [qw(graphical_reports new_charts old_charts)],
|
||||
},
|
||||
{
|
||||
package => 'Chart',
|
||||
module => 'Chart::Base',
|
||||
version => '1.0',
|
||||
feature => 'New Charts, Old Charts'
|
||||
module => 'Chart::Lines',
|
||||
# Versions below 2.1 cannot be detected accurately.
|
||||
version => '2.1',
|
||||
feature => [qw(new_charts old_charts)],
|
||||
},
|
||||
{
|
||||
package => 'Template-GD',
|
||||
|
@ -160,68 +165,62 @@ sub OPTIONAL_MODULES {
|
|||
# on Template-Toolkits after 2.14, and still works with 2.14 and lower.
|
||||
module => 'Template::Plugin::GD::Image',
|
||||
version => 0,
|
||||
feature => 'Graphical Reports'
|
||||
feature => ['graphical_reports'],
|
||||
},
|
||||
{
|
||||
package => 'GDTextUtil',
|
||||
module => 'GD::Text',
|
||||
version => 0,
|
||||
feature => 'Graphical Reports'
|
||||
feature => ['graphical_reports'],
|
||||
},
|
||||
{
|
||||
package => 'GDGraph',
|
||||
module => 'GD::Graph',
|
||||
version => 0,
|
||||
feature => 'Graphical Reports'
|
||||
feature => ['graphical_reports'],
|
||||
},
|
||||
{
|
||||
package => 'XML-Twig',
|
||||
module => 'XML::Twig',
|
||||
version => 0,
|
||||
feature => 'Move Bugs Between Installations'
|
||||
feature => ['moving', 'updates'],
|
||||
},
|
||||
{
|
||||
package => 'MIME-tools',
|
||||
# MIME::Parser is packaged as MIME::Tools on ActiveState Perl
|
||||
module => ON_WINDOWS ? 'MIME::Tools' : 'MIME::Parser',
|
||||
version => '5.406',
|
||||
feature => 'Move Bugs Between Installations'
|
||||
feature => ['moving'],
|
||||
},
|
||||
{
|
||||
package => 'libwww-perl',
|
||||
module => 'LWP::UserAgent',
|
||||
version => 0,
|
||||
feature => 'Automatic Update Notifications'
|
||||
feature => ['updates'],
|
||||
},
|
||||
{
|
||||
package => 'PatchReader',
|
||||
module => 'PatchReader',
|
||||
version => '0.9.4',
|
||||
feature => 'Patch Viewer'
|
||||
},
|
||||
{
|
||||
package => 'PerlMagick',
|
||||
module => 'Image::Magick',
|
||||
version => 0,
|
||||
feature => 'Optionally Convert BMP Attachments to PNGs'
|
||||
feature => ['patch_viewer'],
|
||||
},
|
||||
{
|
||||
package => 'perl-ldap',
|
||||
module => 'Net::LDAP',
|
||||
version => 0,
|
||||
feature => 'LDAP Authentication'
|
||||
feature => ['auth_ldap'],
|
||||
},
|
||||
{
|
||||
package => 'Authen-SASL',
|
||||
module => 'Authen::SASL',
|
||||
version => 0,
|
||||
feature => 'SMTP Authentication'
|
||||
feature => ['smtp_auth'],
|
||||
},
|
||||
{
|
||||
package => 'RadiusPerl',
|
||||
module => 'Authen::Radius',
|
||||
version => 0,
|
||||
feature => 'RADIUS Authentication'
|
||||
feature => ['auth_radius'],
|
||||
},
|
||||
{
|
||||
package => 'SOAP-Lite',
|
||||
|
@ -229,20 +228,32 @@ sub OPTIONAL_MODULES {
|
|||
# 0.710.04 is required for correct UTF-8 handling, but .04 and .05 are
|
||||
# affected by bug 468009.
|
||||
version => '0.710.06',
|
||||
feature => 'XML-RPC Interface'
|
||||
feature => ['xmlrpc'],
|
||||
},
|
||||
{
|
||||
package => 'JSON-RPC',
|
||||
module => 'JSON::RPC',
|
||||
version => 0,
|
||||
feature => ['jsonrpc'],
|
||||
},
|
||||
{
|
||||
package => 'Test-Taint',
|
||||
module => 'Test::Taint',
|
||||
version => 0,
|
||||
feature => ['jsonrpc', 'xmlrpc'],
|
||||
},
|
||||
{
|
||||
# We need the 'utf8_mode' method of HTML::Parser, for HTML::Scrubber.
|
||||
package => 'HTML-Parser',
|
||||
module => 'HTML::Parser',
|
||||
version => '3.40',
|
||||
feature => 'More HTML in Product/Group Descriptions'
|
||||
feature => ['html_desc'],
|
||||
},
|
||||
{
|
||||
package => 'HTML-Scrubber',
|
||||
module => 'HTML::Scrubber',
|
||||
version => 0,
|
||||
feature => 'More HTML in Product/Group Descriptions'
|
||||
feature => ['html_desc'],
|
||||
},
|
||||
|
||||
# Inbound Email
|
||||
|
@ -250,13 +261,13 @@ sub OPTIONAL_MODULES {
|
|||
package => 'Email-MIME-Attachment-Stripper',
|
||||
module => 'Email::MIME::Attachment::Stripper',
|
||||
version => 0,
|
||||
feature => 'Inbound Email'
|
||||
feature => ['inbound_email'],
|
||||
},
|
||||
{
|
||||
package => 'Email-Reply',
|
||||
module => 'Email::Reply',
|
||||
version => 0,
|
||||
feature => 'Inbound Email'
|
||||
feature => ['inbound_email'],
|
||||
},
|
||||
|
||||
# Mail Queueing
|
||||
|
@ -264,13 +275,13 @@ sub OPTIONAL_MODULES {
|
|||
package => 'TheSchwartz',
|
||||
module => 'TheSchwartz',
|
||||
version => 0,
|
||||
feature => 'Mail Queueing',
|
||||
feature => ['jobqueue'],
|
||||
},
|
||||
{
|
||||
package => 'Daemon-Generic',
|
||||
module => 'Daemon::Generic',
|
||||
version => 0,
|
||||
feature => 'Mail Queueing',
|
||||
feature => ['jobqueue'],
|
||||
},
|
||||
|
||||
# mod_perl
|
||||
|
@ -278,40 +289,52 @@ sub OPTIONAL_MODULES {
|
|||
package => 'mod_perl',
|
||||
module => 'mod_perl2',
|
||||
version => '1.999022',
|
||||
feature => 'mod_perl'
|
||||
feature => ['mod_perl'],
|
||||
},
|
||||
);
|
||||
|
||||
my $all_modules = _get_extension_requirements(
|
||||
'OPTIONAL_MODULES', \@modules);
|
||||
return $all_modules;
|
||||
my $extra_modules = _get_extension_requirements('OPTIONAL_MODULES');
|
||||
push(@modules, @$extra_modules);
|
||||
return \@modules;
|
||||
};
|
||||
|
||||
# This implements the install-requirements hook described in Bugzilla::Hook.
|
||||
sub _get_extension_requirements {
|
||||
my ($function, $base_modules) = @_;
|
||||
my @all_modules;
|
||||
# get a list of all extensions
|
||||
my @extensions = glob(bz_locations()->{'extensionsdir'} . "/*");
|
||||
foreach my $extension (@extensions) {
|
||||
my $file = "$extension/code/install-requirements.pl";
|
||||
if (-e $file) {
|
||||
my $safe = new Safe;
|
||||
# This is a very liberal Safe.
|
||||
$safe->permit(qw(:browse require entereval caller));
|
||||
$safe->rdo($file);
|
||||
if ($@) {
|
||||
warn $@;
|
||||
next;
|
||||
# This maps features to the files that require that feature in order
|
||||
# to compile. It is used by t/001compile.t and mod_perl.pl.
|
||||
use constant FEATURE_FILES => (
|
||||
jsonrpc => ['Bugzilla/WebService/Server/JSONRPC.pm', 'jsonrpc.cgi'],
|
||||
xmlrpc => ['Bugzilla/WebService/Server/XMLRPC.pm', 'xmlrpc.cgi',
|
||||
'Bugzilla/WebService.pm', 'Bugzilla/WebService/*.pm'],
|
||||
moving => ['importxml.pl'],
|
||||
auth_ldap => ['Bugzilla/Auth/Verify/LDAP.pm'],
|
||||
auth_radius => ['Bugzilla/Auth/Verify/RADIUS.pm'],
|
||||
inbound_email => ['email_in.pl'],
|
||||
jobqueue => ['Bugzilla/Job/*', 'Bugzilla/JobQueue.pm',
|
||||
'Bugzilla/JobQueue/*', 'jobqueue.pl'],
|
||||
patch_viewer => ['Bugzilla/Attachment/PatchReader.pm'],
|
||||
updates => ['Bugzilla/Update.pm'],
|
||||
);
|
||||
|
||||
# This implements the REQUIRED_MODULES and OPTIONAL_MODULES stuff
|
||||
# described in in Bugzilla::Extension.
|
||||
sub _get_extension_requirements
|
||||
{
|
||||
my ($function) = @_;
|
||||
Bugzilla::Extension::load_all();
|
||||
my $modules = [];
|
||||
if ($function eq 'REQUIRED_MODULES' || $function eq 'OPTIONAL_MODULES')
|
||||
{
|
||||
no strict 'refs';
|
||||
$function = "Bugzilla::Extension::".lc($function);
|
||||
foreach (Bugzilla::Extension::loaded())
|
||||
{
|
||||
if (my $em = &$function($_))
|
||||
{
|
||||
ref $_->{feature} or $_->{feature} = [ $_->{feature} ];
|
||||
push @$modules, @$em;
|
||||
}
|
||||
my $modules = eval { &{$safe->varglob($function)}($base_modules) };
|
||||
next unless $modules;
|
||||
push(@all_modules, @$modules);
|
||||
}
|
||||
}
|
||||
|
||||
unshift(@all_modules, @$base_modules);
|
||||
return \@all_modules;
|
||||
return $modules;
|
||||
};
|
||||
|
||||
sub check_requirements {
|
||||
|
@ -402,7 +425,8 @@ sub print_module_instructions {
|
|||
print '*' x TABLE_WIDTH . "\n";
|
||||
foreach my $package (@missing) {
|
||||
printf "* \%${longest_name}s * %-${remaining_space}s *\n",
|
||||
$package->{package}, $package->{feature};
|
||||
$package->{package},
|
||||
_translate_feature($package->{feature});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -419,13 +443,13 @@ sub print_module_instructions {
|
|||
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 });
|
||||
print colored(install_string('ppm_repo_add',
|
||||
{ theory_url => $url_to_theory58S }), 'red');
|
||||
# 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";
|
||||
|
@ -453,16 +477,30 @@ sub print_module_instructions {
|
|||
}
|
||||
|
||||
if (my @missing = @{$check_results->{missing}}) {
|
||||
print install_string('commands_required') . "\n";
|
||||
print colored(install_string('commands_required'), 'red') . "\n";
|
||||
foreach my $package (@missing) {
|
||||
my $command = install_command($package);
|
||||
print " $command\n";
|
||||
}
|
||||
}
|
||||
|
||||
if ($output && $check_results->{any_missing} && !ON_WINDOWS) {
|
||||
if ($output && $check_results->{any_missing} && !ON_WINDOWS
|
||||
&& !$check_results->{hide_all})
|
||||
{
|
||||
print install_string('install_all', { perl => $^X });
|
||||
}
|
||||
if (!$check_results->{pass}) {
|
||||
print colored(install_string('installation_failed'), 'red') . "\n\n";
|
||||
}
|
||||
}
|
||||
|
||||
sub _translate_feature {
|
||||
my $features = shift;
|
||||
my @strings;
|
||||
foreach my $feature (@$features) {
|
||||
push(@strings, install_string("feature_$feature"));
|
||||
}
|
||||
return join(', ', @strings);
|
||||
}
|
||||
|
||||
sub check_graphviz {
|
||||
|
@ -543,8 +581,9 @@ sub have_vers {
|
|||
my $want_string = $wanted ? "v$wanted" : install_string('any');
|
||||
|
||||
$ok = "$ok:" if $ok;
|
||||
printf "%s %19s %-9s $ok $vstr $black_string\n",
|
||||
install_string('checking_for'), $package, "($want_string)";
|
||||
my $str = sprintf "%s %19s %-9s $ok $vstr $black_string\n",
|
||||
install_string('checking_for'), $package, "($want_string)";
|
||||
print $vok ? $str : colored($str, 'red');
|
||||
}
|
||||
|
||||
return $vok ? 1 : 0;
|
||||
|
@ -567,6 +606,21 @@ sub install_command {
|
|||
return sprintf $command, $package;
|
||||
}
|
||||
|
||||
# This does a reverse mapping for FEATURE_FILES.
|
||||
sub map_files_to_features {
|
||||
my %features = FEATURE_FILES;
|
||||
my %files;
|
||||
foreach my $feature (keys %features) {
|
||||
my @my_files = @{ $features{$feature} };
|
||||
foreach my $pattern (@my_files) {
|
||||
foreach my $file (glob $pattern) {
|
||||
$files{$file} = $feature;
|
||||
}
|
||||
}
|
||||
}
|
||||
return \%files;
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
@ -584,16 +638,42 @@ perl modules it requires.)
|
|||
|
||||
=head1 CONSTANTS
|
||||
|
||||
=over 4
|
||||
=over
|
||||
|
||||
=item C<REQUIRED_MODULES>
|
||||
|
||||
An arrayref of hashrefs that describes the perl modules required by
|
||||
Bugzilla. The hashes have two keys, C<name> and C<version>, which
|
||||
represent the name of the module and the version that we require.
|
||||
Bugzilla. The hashes have three keys:
|
||||
|
||||
=over
|
||||
|
||||
=item C<package> - The name of the Perl package that you'd find on
|
||||
CPAN for this requirement.
|
||||
|
||||
=item C<module> - The name of a module that can be passed to the
|
||||
C<install> command in C<CPAN.pm> to install this module.
|
||||
|
||||
=item C<version> - The version of this module that we require, or C<0>
|
||||
if any version is acceptable.
|
||||
|
||||
=back
|
||||
|
||||
=item C<OPTIONAL_MODULES>
|
||||
|
||||
An arrayref of hashrefs that describes the perl modules that add
|
||||
additional features to Bugzilla if installed. Its hashes have all
|
||||
the fields of L</REQUIRED_MODULES>, plus a C<feature> item--an arrayref
|
||||
of strings that describe what features require this module.
|
||||
|
||||
=item C<FEATURE_FILES>
|
||||
|
||||
A hashref that describes what files should only be compiled if a certain
|
||||
feature is enabled. The feature is the key, and the values are arrayrefs
|
||||
of file names (which are passed to C<glob>, so shell patterns work).
|
||||
|
||||
=back
|
||||
|
||||
|
||||
=head1 SUBROUTINES
|
||||
|
||||
=over 4
|
||||
|
@ -676,4 +756,9 @@ Returns: C<1> if the check was successful, C<0> otherwise.
|
|||
|
||||
Returns: nothing
|
||||
|
||||
=item C<map_files_to_features>
|
||||
|
||||
Returns a hashref where file names are the keys and the value is the feature
|
||||
that must be enabled in order to compile that file.
|
||||
|
||||
=back
|
||||
|
|
|
@ -27,6 +27,7 @@ package Bugzilla::Install::Util;
|
|||
use strict;
|
||||
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::Extension;
|
||||
|
||||
use File::Basename;
|
||||
use POSIX qw(setlocale LC_CTYPE);
|
||||
|
@ -43,7 +44,7 @@ our @EXPORT_OK = qw(
|
|||
template_include_path
|
||||
vers_cmp
|
||||
get_console_locale
|
||||
prevent_windows_dialog_boxes
|
||||
init_console
|
||||
);
|
||||
|
||||
sub bin_loc {
|
||||
|
@ -91,8 +92,8 @@ sub indicate_progress {
|
|||
|
||||
sub install_string {
|
||||
my ($string_id, $vars) = @_;
|
||||
_cache()->{template_include_path} ||= template_include_path();
|
||||
my $path = _cache()->{template_include_path};
|
||||
_cache()->{install_string_path} ||= template_include_path();
|
||||
my $path = _cache()->{install_string_path};
|
||||
|
||||
my $string_template;
|
||||
# Find the first template that defines this string.
|
||||
|
@ -106,6 +107,8 @@ sub install_string {
|
|||
die "No language defines the string '$string_id'"
|
||||
if !defined $string_template;
|
||||
|
||||
utf8::decode($string_template) if !utf8::is_utf8($string_template);
|
||||
|
||||
$vars ||= {};
|
||||
my @replace_keys = keys %$vars;
|
||||
foreach my $key (@replace_keys) {
|
||||
|
@ -127,15 +130,28 @@ sub install_string {
|
|||
}
|
||||
|
||||
sub include_languages {
|
||||
# If we are in CGI mode (not in checksetup.pl) and if the function has
|
||||
# been called without any parameter, then we cache the result of this
|
||||
# function in Bugzilla->request_cache. This is done to improve the
|
||||
# performance of the template processing.
|
||||
my $to_be_cached = 0;
|
||||
if (not @_) {
|
||||
my $cache = _cache();
|
||||
if (exists $cache->{include_languages}) {
|
||||
return @{ $cache->{include_languages} };
|
||||
}
|
||||
$to_be_cached = 1;
|
||||
}
|
||||
my ($params) = @_;
|
||||
$params ||= {};
|
||||
|
||||
# Basically, the way this works is that we have a list of languages
|
||||
# that we *want*, and a list of languages that Bugzilla actually
|
||||
# supports. The caller tells us what languages they want, by setting
|
||||
# $ENV{HTTP_ACCEPT_LANGUAGE} or $params->{only_language}. The languages
|
||||
# we support are those specified in $params->{use_languages}. Otherwise
|
||||
# we support every language installed in the template/ directory.
|
||||
# $ENV{HTTP_ACCEPT_LANGUAGE}, using the "LANG" cookie or setting
|
||||
# $params->{only_language}. The languages we support are those
|
||||
# specified in $params->{use_languages}. Otherwise we support every
|
||||
# language installed in the template/ directory.
|
||||
|
||||
my @wanted;
|
||||
if ($params->{only_language}) {
|
||||
|
@ -143,6 +159,15 @@ sub include_languages {
|
|||
}
|
||||
else {
|
||||
@wanted = _sort_accept_language($ENV{'HTTP_ACCEPT_LANGUAGE'} || '');
|
||||
# Don't use the cookie if we are in "checksetup.pl". The test
|
||||
# with $ENV{'SERVER_SOFTWARE'} is the same as in
|
||||
# Bugzilla:Util::i_am_cgi.
|
||||
if (exists $ENV{'SERVER_SOFTWARE'}) {
|
||||
my $cgi = Bugzilla->cgi;
|
||||
if (defined (my $lang = $cgi->cookie('LANG'))) {
|
||||
unshift @wanted, $lang;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
my @supported;
|
||||
|
@ -175,30 +200,75 @@ sub include_languages {
|
|||
push(@usedlanguages, 'en');
|
||||
}
|
||||
|
||||
# Cache the result if we are in CGI mode and called without parameter
|
||||
# (see the comment at the top of this function).
|
||||
if ($to_be_cached) {
|
||||
_cache()->{include_languages} = \@usedlanguages;
|
||||
}
|
||||
|
||||
return @usedlanguages;
|
||||
}
|
||||
|
||||
sub template_include_path {
|
||||
my @usedlanguages = include_languages(@_);
|
||||
# Now, we add template directories in the order they will be searched:
|
||||
# Used by template_include_path
|
||||
sub _template_lang_directories {
|
||||
my ($languages, $templatedir) = @_;
|
||||
|
||||
# First, we add extension template directories, because extension templates
|
||||
# override standard templates. Extensions may be localized in the same way
|
||||
# that Bugzilla templates are localized.
|
||||
my @include_path;
|
||||
my @extensions = glob(bz_locations()->{'extensionsdir'} . "/*");
|
||||
foreach my $extension (@extensions) {
|
||||
next if -e "$extension/disabled";
|
||||
foreach my $lang (@usedlanguages) {
|
||||
_add_language_set(\@include_path, $lang, "$extension/template");
|
||||
my @add = qw(custom default);
|
||||
my $project = bz_locations->{'project'};
|
||||
unshift(@add, $project) if $project;
|
||||
|
||||
my @result;
|
||||
foreach my $lang (@$languages) {
|
||||
foreach my $dir (@add) {
|
||||
my $full_dir = "$templatedir/$lang/$dir";
|
||||
if (-d $full_dir) {
|
||||
trick_taint($full_dir);
|
||||
push(@result, $full_dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
return @result;
|
||||
}
|
||||
|
||||
# Used by template_include_path.
|
||||
sub _template_base_directories
|
||||
{
|
||||
my @template_dirs;
|
||||
|
||||
Bugzilla::Extension::load_all();
|
||||
my $dir;
|
||||
foreach (Bugzilla::Extension::loaded())
|
||||
{
|
||||
$dir = extension_template_dir($_);
|
||||
if (-d $dir)
|
||||
{
|
||||
push @template_dirs, $dir;
|
||||
}
|
||||
}
|
||||
|
||||
# Then, we add normal template directories, sorted by language.
|
||||
foreach my $lang (@usedlanguages) {
|
||||
_add_language_set(\@include_path, $lang);
|
||||
}
|
||||
push(@template_dirs, bz_locations()->{'templatedir'});
|
||||
return \@template_dirs;
|
||||
}
|
||||
|
||||
sub template_include_path {
|
||||
my ($params) = @_;
|
||||
my @used_languages = include_languages(@_);
|
||||
# Now, we add template directories in the order they will be searched:
|
||||
my $template_dirs = _template_base_directories();
|
||||
|
||||
my @include_path;
|
||||
foreach my $template_dir (@$template_dirs) {
|
||||
my @lang_dirs = _template_lang_directories(\@used_languages,
|
||||
$template_dir);
|
||||
# Hooks get each set of extension directories separately.
|
||||
if ($params->{hook}) {
|
||||
push(@include_path, \@lang_dirs);
|
||||
}
|
||||
# Whereas everything else just gets a whole INCLUDE_PATH.
|
||||
else {
|
||||
push(@include_path, @lang_dirs);
|
||||
}
|
||||
}
|
||||
return \@include_path;
|
||||
}
|
||||
|
||||
|
@ -260,24 +330,6 @@ sub _get_string_from_file {
|
|||
return $strings{$string_id};
|
||||
}
|
||||
|
||||
# Used by template_include_path.
|
||||
sub _add_language_set {
|
||||
my ($array, $lang, $templatedir) = @_;
|
||||
|
||||
$templatedir ||= bz_locations()->{'templatedir'};
|
||||
my @add = ("$templatedir/$lang/custom", "$templatedir/$lang/default");
|
||||
|
||||
my $project = bz_locations->{'project'};
|
||||
unshift(@add, "$templatedir/$lang/$project") if $project;
|
||||
|
||||
foreach my $dir (@add) {
|
||||
if (-d $dir) {
|
||||
trick_taint($dir);
|
||||
push(@$array, $dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Make an ordered list out of a HTTP Accept-Language header (see RFC 2616, 14.4)
|
||||
# We ignore '*' and <language-range>;q=0
|
||||
# For languages with the same priority q the order remains unchanged.
|
||||
|
@ -333,6 +385,13 @@ sub get_console_locale {
|
|||
return $locale;
|
||||
}
|
||||
|
||||
sub init_console {
|
||||
eval { ON_WINDOWS && require Win32::Console::ANSI; };
|
||||
$ENV{'ANSI_COLORS_DISABLED'} = 1 if ($@ || !-t *STDOUT);
|
||||
$ENV{'HTTP_ACCEPT_LANGUAGE'} ||= get_console_locale();
|
||||
prevent_windows_dialog_boxes();
|
||||
}
|
||||
|
||||
sub prevent_windows_dialog_boxes {
|
||||
# This code comes from http://bugs.activestate.com/show_bug.cgi?id=82183
|
||||
# and prevents Perl modules from popping up dialog boxes, particularly
|
||||
|
@ -355,12 +414,13 @@ sub prevent_windows_dialog_boxes {
|
|||
}
|
||||
|
||||
# This is like request_cache, but it's used only by installation code
|
||||
# for setup.cgi and things like that.
|
||||
# for checksetup.pl and things like that.
|
||||
our $_cache = {};
|
||||
sub _cache {
|
||||
if ($ENV{MOD_PERL}) {
|
||||
require Apache2::RequestUtil;
|
||||
return Apache2::RequestUtil->request->pnotes();
|
||||
# If the normal request_cache is available (which happens any time
|
||||
# after the requirements phase) then we should use that.
|
||||
if (eval { Bugzilla->request_cache; }) {
|
||||
return Bugzilla->request_cache;
|
||||
}
|
||||
return $_cache;
|
||||
}
|
||||
|
@ -377,6 +437,15 @@ sub trick_taint {
|
|||
return (defined($_[0]));
|
||||
}
|
||||
|
||||
sub trim {
|
||||
my ($str) = @_;
|
||||
if ($str) {
|
||||
$str =~ s/^\s+//g;
|
||||
$str =~ s/\s+$//g;
|
||||
}
|
||||
return $str;
|
||||
}
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
@ -416,6 +485,10 @@ running, what perl version we're using, and what OS we're running on.
|
|||
Returns the language to use based on the LC_CTYPE value returned by the OS.
|
||||
If LC_CTYPE is of the form fr-CH, then fr is appended to the list.
|
||||
|
||||
=item C<init_console>
|
||||
|
||||
Sets the C<ANSI_COLORS_DISABLED> and C<HTTP_ACCEPT_LANGUAGE> environment variables.
|
||||
|
||||
=item C<indicate_progress>
|
||||
|
||||
=over
|
||||
|
|
|
@ -27,7 +27,7 @@ use strict;
|
|||
use Bugzilla::Constants;
|
||||
use Bugzilla::Error;
|
||||
use Bugzilla::Install::Util qw(install_string);
|
||||
BEGIN { eval "use base qw(TheSchwartz)"; }
|
||||
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.
|
||||
|
@ -38,8 +38,8 @@ use constant JOB_MAP => {
|
|||
sub new {
|
||||
my $class = shift;
|
||||
|
||||
if (!eval { require TheSchwartz; }) {
|
||||
ThrowCodeError('jobqueue_not_configured');
|
||||
if (!Bugzilla->feature('jobqueue')) {
|
||||
ThrowCodeError('feature_disabled', { feature => 'jobqueue' });
|
||||
}
|
||||
|
||||
my $lc = Bugzilla->localconfig;
|
||||
|
|
|
@ -74,12 +74,6 @@ sub set_description { $_[0]->set('description', $_[1]); }
|
|||
#### Subroutines ######
|
||||
###############################
|
||||
|
||||
sub keyword_count {
|
||||
my ($count) =
|
||||
Bugzilla->dbh->selectrow_array('SELECT COUNT(*) FROM keyworddefs');
|
||||
return $count;
|
||||
}
|
||||
|
||||
sub get_all_with_bug_count {
|
||||
my $class = shift;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
@ -145,8 +139,6 @@ Bugzilla::Keyword - A Keyword that can be added to a bug.
|
|||
|
||||
use Bugzilla::Keyword;
|
||||
|
||||
my $count = Bugzilla::Keyword::keyword_count;
|
||||
|
||||
my $description = $keyword->description;
|
||||
|
||||
my $keywords = Bugzilla::Keyword->get_all_with_bug_count();
|
||||
|
@ -166,14 +158,6 @@ implements.
|
|||
|
||||
=over
|
||||
|
||||
=item C<keyword_count()>
|
||||
|
||||
Description: A utility function to get the total number
|
||||
of keywords defined. Mostly used to see
|
||||
if there are any keywords defined at all.
|
||||
Params: none
|
||||
Returns: An integer, the count of keywords.
|
||||
|
||||
=item C<get_all_with_bug_count()>
|
||||
|
||||
Description: Returns all defined keywords. This is an efficient way
|
||||
|
|
|
@ -82,10 +82,7 @@ sub MessageToMTA {
|
|||
#
|
||||
# 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.
|
||||
# even if the admin changes the "ssl_redirect" parameter some day.
|
||||
$email->header_set('X-Bugzilla-URL', Bugzilla->params->{'urlbase'});
|
||||
|
||||
# We add this header to mark the mail as "auto-generated" and
|
||||
|
@ -171,7 +168,7 @@ sub MessageToMTA {
|
|||
Debug => Bugzilla->params->{'smtp_debug'};
|
||||
}
|
||||
|
||||
Bugzilla::Hook::process('mailer-before_send',
|
||||
Bugzilla::Hook::process('mailer_before_send',
|
||||
{ email => $email, mailer_args => \@args });
|
||||
|
||||
if ($method eq "Test") {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,709 @@
|
|||
# -*- 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 Migration Tool.
|
||||
#
|
||||
# The Initial Developer of the Original Code is Lambda Research
|
||||
# Corporation. Portions created by the Initial Developer are Copyright
|
||||
# (C) 2009 the Initial Developer. All Rights Reserved.
|
||||
#
|
||||
# Contributor(s):
|
||||
# Max Kanat-Alexander <mkanat@bugzilla.org>
|
||||
|
||||
package Bugzilla::Migrate::Gnats;
|
||||
use strict;
|
||||
use base qw(Bugzilla::Migrate);
|
||||
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::Install::Util qw(indicate_progress);
|
||||
use Bugzilla::Util qw(format_time trim generate_random_password lsearch);
|
||||
|
||||
use Email::Address;
|
||||
use Email::MIME;
|
||||
use File::Basename;
|
||||
use IO::File;
|
||||
use List::Util qw(first);
|
||||
|
||||
use constant REQUIRED_MODULES => [
|
||||
{
|
||||
package => 'Email-Simple-FromHandle',
|
||||
module => 'Email::Simple::FromHandle',
|
||||
# This version added seekable handles.
|
||||
version => 0.050,
|
||||
},
|
||||
];
|
||||
|
||||
use constant FIELD_MAP => {
|
||||
'Number' => 'bug_id',
|
||||
'Category' => 'product',
|
||||
'Synopsis' => 'short_desc',
|
||||
'Responsible' => 'assigned_to',
|
||||
'State' => 'bug_status',
|
||||
'Class' => 'cf_type',
|
||||
'Classification' => '',
|
||||
'Originator' => 'reporter',
|
||||
'Arrival-Date' => 'creation_ts',
|
||||
'Last-Modified' => 'delta_ts',
|
||||
'Release' => 'version',
|
||||
'Severity' => 'bug_severity',
|
||||
'Description' => 'comment',
|
||||
};
|
||||
|
||||
use constant VALUE_MAP => {
|
||||
bug_severity => {
|
||||
'serious' => 'major',
|
||||
'cosmetic' => 'trivial',
|
||||
'new-feature' => 'enhancement',
|
||||
'non-critical' => 'normal',
|
||||
},
|
||||
bug_status => {
|
||||
'open' => 'NEW',
|
||||
'analyzed' => 'ASSIGNED',
|
||||
'suspended' => 'RESOLVED',
|
||||
'feedback' => 'RESOLVED',
|
||||
'released' => 'VERIFIED',
|
||||
},
|
||||
bug_status_resolution => {
|
||||
'feedback' => 'FIXED',
|
||||
'released' => 'FIXED',
|
||||
'closed' => 'FIXED',
|
||||
'suspended' => 'LATER',
|
||||
},
|
||||
priority => {
|
||||
'medium' => 'Normal',
|
||||
},
|
||||
};
|
||||
|
||||
use constant GNATS_CONFIG_VARS => (
|
||||
{
|
||||
name => 'gnats_path',
|
||||
default => '/var/lib/gnats',
|
||||
desc => <<END,
|
||||
# The path to the directory that contains the GNATS database.
|
||||
END
|
||||
},
|
||||
{
|
||||
name => 'default_email_domain',
|
||||
default => 'example.com',
|
||||
desc => <<'END',
|
||||
# Some GNATS users do not have full email addresses, but Bugzilla requires
|
||||
# every user to have an email address. What domain should be appended to
|
||||
# usernames that don't have emails, to make them into email addresses?
|
||||
# (For example, if you leave this at the default, "unknown" would become
|
||||
# "unknown@example.com".)
|
||||
END
|
||||
},
|
||||
{
|
||||
name => 'component_name',
|
||||
default => 'General',
|
||||
desc => <<'END',
|
||||
# GNATS has only "Category" to classify bugs. However, Bugzilla has a
|
||||
# multi-level system of Products that contain Components. When importing
|
||||
# GNATS categories, they become a Product with one Component. What should
|
||||
# the name of that Component be?
|
||||
END
|
||||
},
|
||||
{
|
||||
name => 'version_regex',
|
||||
default => '',
|
||||
desc => <<'END',
|
||||
# In GNATS, the "version" field can contain almost anything. However, in
|
||||
# Bugzilla, it's a drop-down, so you don't want too many choices in there.
|
||||
# If you specify a regular expression here, versions will be tested against
|
||||
# this regular expression, and if they match, the first match (the first set
|
||||
# of parentheses in the regular expression, also called "$1") will be used
|
||||
# as the version value for the bug instead of the full version value specified
|
||||
# in GNATS.
|
||||
END
|
||||
},
|
||||
{
|
||||
name => 'default_originator',
|
||||
default => 'gnats-admin',
|
||||
desc => <<'END',
|
||||
# Sometimes, a PR has no valid Originator, so we fall back to the From
|
||||
# header of the email. If the From header also isn't a valid username
|
||||
# (is just a name with spaces in it--we can't convert that to an email
|
||||
# address) then this username (which can either be a GNATS username or an
|
||||
# email address) will be considered to be the Originator of the PR.
|
||||
END
|
||||
}
|
||||
);
|
||||
|
||||
sub CONFIG_VARS {
|
||||
my $self = shift;
|
||||
my @vars = (GNATS_CONFIG_VARS, $self->SUPER::CONFIG_VARS);
|
||||
my $field_map = first { $_->{name} eq 'translate_fields' } @vars;
|
||||
$field_map->{default} = FIELD_MAP;
|
||||
my $value_map = first { $_->{name} eq 'translate_values' } @vars;
|
||||
$value_map->{default} = VALUE_MAP;
|
||||
return @vars;
|
||||
}
|
||||
|
||||
# Directories that aren't projects, or that we shouldn't be parsing
|
||||
use constant SKIP_DIRECTORIES => qw(
|
||||
gnats-adm
|
||||
gnats-queue
|
||||
pending
|
||||
);
|
||||
|
||||
use constant NON_COMMENT_FIELDS => qw(
|
||||
Audit-Trail
|
||||
Closed-Date
|
||||
Confidential
|
||||
Unformatted
|
||||
attachments
|
||||
);
|
||||
|
||||
# Certain fields can contain things that look like fields in them,
|
||||
# because they might contain quoted emails. To avoid mis-parsing,
|
||||
# we list out here the exact order of fields at the end of a PR
|
||||
# and wait for the next field to consider that we actually have
|
||||
# a field to parse.
|
||||
use constant END_FIELD_ORDER => [qw(
|
||||
Description
|
||||
How-To-Repeat
|
||||
Fix
|
||||
Release-Note
|
||||
Audit-Trail
|
||||
Unformatted
|
||||
)];
|
||||
|
||||
use constant CUSTOM_FIELDS => {
|
||||
cf_type => {
|
||||
type => FIELD_TYPE_SINGLE_SELECT,
|
||||
description => 'Type',
|
||||
},
|
||||
};
|
||||
|
||||
use constant FIELD_REGEX => qr/^>(\S+):\s*(.*)$/;
|
||||
|
||||
# Used for bugs that have no Synopsis.
|
||||
use constant NO_SUBJECT => "(no subject)";
|
||||
|
||||
# This is the divider that GNATS uses between attachments in its database
|
||||
# files. It's missign two hyphens at the beginning because MIME Emails use
|
||||
# -- to start boundaries.
|
||||
use constant GNATS_BOUNDARY => '----gnatsweb-attachment----';
|
||||
|
||||
use constant LONG_VERSION_LENGTH => 32;
|
||||
|
||||
#########
|
||||
# Hooks #
|
||||
#########
|
||||
|
||||
sub before_insert {
|
||||
my $self = shift;
|
||||
|
||||
# gnats_id isn't a valid User::create field, and we don't need it
|
||||
# anymore now.
|
||||
delete $_->{gnats_id} foreach @{ $self->users };
|
||||
|
||||
# Grab a version out of a bug for each product, so that there is a
|
||||
# valid "version" argument for Bugzilla::Product->create.
|
||||
foreach my $product (@{ $self->products }) {
|
||||
my $bug = first { $_->{product} eq $product->{name} and $_->{version} }
|
||||
@{ $self->bugs };
|
||||
if (defined $bug) {
|
||||
$product->{version} = $bug->{version};
|
||||
}
|
||||
else {
|
||||
$product->{version} = 'unspecified';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#########
|
||||
# Users #
|
||||
#########
|
||||
|
||||
sub _read_users {
|
||||
my $self = shift;
|
||||
my $path = $self->config('gnats_path');
|
||||
my $file = "$path/gnats-adm/responsible";
|
||||
$self->debug("Reading users from $file");
|
||||
my $default_domain = $self->config('default_email_domain');
|
||||
open(my $users_fh, '<', $file) || die "$file: $!";
|
||||
my @users;
|
||||
foreach my $line (<$users_fh>) {
|
||||
$line = trim($line);
|
||||
next if $line =~ /^#/;
|
||||
my ($id, $name, $email) = split(':', $line, 3);
|
||||
$email ||= "$id\@$default_domain";
|
||||
# We can't call our own translate_value, because that depends on
|
||||
# the existence of user_map, which doesn't exist until after
|
||||
# this method. However, we still want to translate any users found.
|
||||
$email = $self->SUPER::translate_value('user', $email);
|
||||
push(@users, { realname => $name, login_name => $email,
|
||||
gnats_id => $id });
|
||||
}
|
||||
close($users_fh);
|
||||
return \@users;
|
||||
}
|
||||
|
||||
sub user_map {
|
||||
my $self = shift;
|
||||
$self->{user_map} ||= { map { $_->{gnats_id} => $_->{login_name} }
|
||||
@{ $self->users } };
|
||||
return $self->{user_map};
|
||||
}
|
||||
|
||||
sub add_user {
|
||||
my ($self, $id, $email) = @_;
|
||||
return if defined $self->user_map->{$id};
|
||||
$self->user_map->{$id} = $email;
|
||||
push(@{ $self->users }, { login_name => $email, gnats_id => $id });
|
||||
}
|
||||
|
||||
sub user_to_email {
|
||||
my ($self, $value) = @_;
|
||||
if (defined $self->user_map->{$value}) {
|
||||
$value = $self->user_map->{$value};
|
||||
}
|
||||
elsif ($value !~ /@/) {
|
||||
my $domain = $self->config('default_email_domain');
|
||||
$value = "$value\@$domain";
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
############
|
||||
# Products #
|
||||
############
|
||||
|
||||
sub _read_products {
|
||||
my $self = shift;
|
||||
my $path = $self->config('gnats_path');
|
||||
my $file = "$path/gnats-adm/categories";
|
||||
$self->debug("Reading categories from $file");
|
||||
|
||||
open(my $categories_fh, '<', $file) || die "$file: $!";
|
||||
my @products;
|
||||
foreach my $line (<$categories_fh>) {
|
||||
$line = trim($line);
|
||||
next if $line =~ /^#/;
|
||||
my ($name, $description, $assigned_to, $cc) = split(':', $line, 4);
|
||||
my %product = ( name => $name, description => $description );
|
||||
|
||||
my @initial_cc = split(',', $cc);
|
||||
@initial_cc = @{ $self->translate_value('user', \@initial_cc) };
|
||||
$assigned_to = $self->translate_value('user', $assigned_to);
|
||||
my %component = ( name => $self->config('component_name'),
|
||||
description => $description,
|
||||
initialowner => $assigned_to,
|
||||
initial_cc => \@initial_cc );
|
||||
$product{components} = [\%component];
|
||||
push(@products, \%product);
|
||||
}
|
||||
close($categories_fh);
|
||||
return \@products;
|
||||
}
|
||||
|
||||
################
|
||||
# Reading Bugs #
|
||||
################
|
||||
|
||||
sub _read_bugs {
|
||||
my $self = shift;
|
||||
my $path = $self->config('gnats_path');
|
||||
my @directories = glob("$path/*");
|
||||
my @bugs;
|
||||
foreach my $directory (@directories) {
|
||||
next if !-d $directory;
|
||||
my $name = basename($directory);
|
||||
next if grep($_ eq $name, SKIP_DIRECTORIES);
|
||||
push(@bugs, @{ $self->_parse_project($directory) });
|
||||
}
|
||||
@bugs = sort { $a->{Number} <=> $b->{Number} } @bugs;
|
||||
return \@bugs;
|
||||
}
|
||||
|
||||
sub _parse_project {
|
||||
my ($self, $directory) = @_;
|
||||
my @files = glob("$directory/*");
|
||||
|
||||
$self->debug("Reading Project: $directory");
|
||||
# Sometimes other files get into gnats directories.
|
||||
@files = grep { basename($_) =~ /^\d+$/ } @files;
|
||||
my @bugs;
|
||||
my $count = 1;
|
||||
my $total = scalar @files;
|
||||
print basename($directory) . ":\n";
|
||||
foreach my $file (@files) {
|
||||
push(@bugs, $self->_parse_bug_file($file));
|
||||
if (!$self->verbose) {
|
||||
indicate_progress({ current => $count++, every => 5,
|
||||
total => $total });
|
||||
}
|
||||
}
|
||||
return \@bugs;
|
||||
}
|
||||
|
||||
sub _parse_bug_file {
|
||||
my ($self, $file) = @_;
|
||||
$self->debug("Reading $file");
|
||||
open(my $fh, "<", $file) || die "$file: $!";
|
||||
my $email = Email::Simple::FromHandle->new($fh);
|
||||
my $fields = $self->_get_gnats_field_data($email);
|
||||
# We parse attachments here instead of during translate_bug,
|
||||
# because otherwise we'd be taking up huge amounts of memory storing
|
||||
# all the raw attachment data in memory.
|
||||
$fields->{attachments} = $self->_parse_attachments($fields);
|
||||
close($fh);
|
||||
return $fields;
|
||||
}
|
||||
|
||||
sub _get_gnats_field_data {
|
||||
my ($self, $email) = @_;
|
||||
my ($current_field, @value_lines, %fields);
|
||||
$email->reset_handle();
|
||||
my $handle = $email->handle;
|
||||
foreach my $line (<$handle>) {
|
||||
# If this line starts a field name
|
||||
if ($line =~ FIELD_REGEX) {
|
||||
my ($new_field, $rest_of_line) = ($1, $2);
|
||||
|
||||
# If this is one of the last few PR fields, then make sure
|
||||
# that we're getting our fields in the right order.
|
||||
my $new_field_valid = 1;
|
||||
my $current_field_pos =
|
||||
lsearch(END_FIELD_ORDER, $current_field || '');
|
||||
if ($current_field_pos > -1) {
|
||||
my $new_field_pos = lsearch(END_FIELD_ORDER, $new_field);
|
||||
# We accept any field, as long as it's later than this one.
|
||||
$new_field_valid = $new_field_pos > $current_field_pos ? 1 : 0;
|
||||
}
|
||||
|
||||
if ($new_field_valid) {
|
||||
if ($current_field) {
|
||||
$fields{$current_field} = _handle_lines(\@value_lines);
|
||||
@value_lines = ();
|
||||
}
|
||||
$current_field = $new_field;
|
||||
$line = $rest_of_line;
|
||||
}
|
||||
}
|
||||
push(@value_lines, $line) if defined $line;
|
||||
}
|
||||
$fields{$current_field} = _handle_lines(\@value_lines);
|
||||
$fields{cc} = [$email->header('Cc')] if $email->header('Cc');
|
||||
|
||||
# If the Originator is invalid and we don't have a translation for it,
|
||||
# use the From header instead.
|
||||
my $originator = $self->translate_value('reporter', $fields{Originator},
|
||||
{ check_only => 1 });
|
||||
if ($originator !~ Bugzilla->params->{emailregexp}) {
|
||||
# We use the raw header sometimes, because it looks like "From: user"
|
||||
# which Email::Address won't parse but we can still use.
|
||||
my $address = $email->header('From');
|
||||
my ($parsed) = Email::Address->parse($address);
|
||||
if ($parsed) {
|
||||
$address = $parsed->address;
|
||||
}
|
||||
if ($address) {
|
||||
$self->debug(
|
||||
"PR $fields{Number} had an Originator that was not a valid"
|
||||
. " user ($fields{Originator}). Using From ($address)"
|
||||
. " instead.\n");
|
||||
my $address_email = $self->translate_value('reporter', $address,
|
||||
{ check_only => 1 });
|
||||
if ($address_email !~ Bugzilla->params->{emailregexp}) {
|
||||
$self->debug(" From was also invalid, using default_originator.\n");
|
||||
$address = $self->config('default_originator');
|
||||
}
|
||||
$fields{Originator} = $address;
|
||||
}
|
||||
}
|
||||
|
||||
$self->debug(\%fields, 3);
|
||||
return \%fields;
|
||||
}
|
||||
|
||||
sub _handle_lines {
|
||||
my ($lines) = @_;
|
||||
my $value = join('', @$lines);
|
||||
$value =~ s/\s+$//;
|
||||
return $value;
|
||||
}
|
||||
|
||||
####################
|
||||
# Translating Bugs #
|
||||
####################
|
||||
|
||||
sub translate_bug {
|
||||
my ($self, $fields) = @_;
|
||||
|
||||
my ($bug, $other_fields) = $self->SUPER::translate_bug($fields);
|
||||
|
||||
$bug->{attachments} = delete $other_fields->{attachments};
|
||||
|
||||
if (defined $other_fields->{_add_to_comment}) {
|
||||
$bug->{comment} .= delete $other_fields->{_add_to_comment};
|
||||
}
|
||||
|
||||
my ($changes, $extra_comment) =
|
||||
$self->_parse_audit_trail($bug, $other_fields->{'Audit-Trail'});
|
||||
|
||||
my @comments;
|
||||
foreach my $change (@$changes) {
|
||||
if (exists $change->{comment}) {
|
||||
push(@comments, {
|
||||
thetext => $change->{comment},
|
||||
who => $change->{who},
|
||||
bug_when => $change->{bug_when} });
|
||||
delete $change->{comment};
|
||||
}
|
||||
}
|
||||
$bug->{history} = $changes;
|
||||
|
||||
if (trim($extra_comment)) {
|
||||
push(@comments, { thetext => $extra_comment, who => $bug->{reporter},
|
||||
bug_when => $bug->{delta_ts} || $bug->{creation_ts} });
|
||||
}
|
||||
$bug->{comments} = \@comments;
|
||||
|
||||
$bug->{component} = $self->config('component_name');
|
||||
if (!$bug->{short_desc}) {
|
||||
$bug->{short_desc} = NO_SUBJECT;
|
||||
}
|
||||
|
||||
foreach my $attachment (@{ $bug->{attachments} || [] }) {
|
||||
$attachment->{submitter} = $bug->{reporter};
|
||||
$attachment->{creation_ts} = $bug->{creation_ts};
|
||||
}
|
||||
|
||||
$self->debug($bug, 3);
|
||||
return $bug;
|
||||
}
|
||||
|
||||
sub _parse_audit_trail {
|
||||
my ($self, $bug, $audit_trail) = @_;
|
||||
return [] if !trim($audit_trail);
|
||||
$self->debug(" Parsing audit trail...", 2);
|
||||
|
||||
if ($audit_trail !~ /^\S+-Changed-\S+:/ms) {
|
||||
# This is just a comment from the bug's creator.
|
||||
$self->debug(" Audit trail is just a comment.", 2);
|
||||
return ([], $audit_trail);
|
||||
}
|
||||
|
||||
my (@changes, %current_data, $current_column, $on_why);
|
||||
my $extra_comment = '';
|
||||
my $current_field;
|
||||
my @all_lines = split("\n", $audit_trail);
|
||||
foreach my $line (@all_lines) {
|
||||
# GNATS history looks like:
|
||||
# Status-Changed-From-To: open->closed
|
||||
# Status-Changed-By: jack
|
||||
# Status-Changed-When: Mon May 12 14:46:59 2003
|
||||
# Status-Changed-Why:
|
||||
# This is some comment here about the change.
|
||||
if ($line =~ /^(\S+)-Changed-(\S+):(.*)/) {
|
||||
my ($field, $column, $value) = ($1, $2, $3);
|
||||
my $bz_field = $self->translate_field($field);
|
||||
# If it's not a field we're importing, we don't care about
|
||||
# its history.
|
||||
next if !$bz_field;
|
||||
# GNATS doesn't track values for description changes,
|
||||
# unfortunately, and that's the only information we'd be able to
|
||||
# use in Bugzilla for the audit trail on that field.
|
||||
next if $bz_field eq 'comment';
|
||||
$current_field = $bz_field if !$current_field;
|
||||
if ($bz_field ne $current_field) {
|
||||
$self->_store_audit_change(
|
||||
\@changes, $current_field, \%current_data);
|
||||
%current_data = ();
|
||||
$current_field = $bz_field;
|
||||
}
|
||||
$value = trim($value);
|
||||
$self->debug(" $bz_field $column: $value", 3);
|
||||
if ($column eq 'From-To') {
|
||||
my ($from, $to) = split('->', $value, 2);
|
||||
# Sometimes there's just a - instead of a -> between the values.
|
||||
if (!defined($to)) {
|
||||
($from, $to) = split('-', $value, 2);
|
||||
}
|
||||
$current_data{added} = $to;
|
||||
$current_data{removed} = $from;
|
||||
}
|
||||
elsif ($column eq 'By') {
|
||||
my $email = $self->translate_value('user', $value);
|
||||
# Sometimes we hit users in the audit trail that we haven't
|
||||
# seen anywhere else.
|
||||
$current_data{who} = $email;
|
||||
}
|
||||
elsif ($column eq 'When') {
|
||||
$current_data{bug_when} = $self->parse_date($value);
|
||||
}
|
||||
if ($column eq 'Why') {
|
||||
$value = '' if !defined $value;
|
||||
$current_data{comment} = $value;
|
||||
$on_why = 1;
|
||||
}
|
||||
else {
|
||||
$on_why = 0;
|
||||
}
|
||||
}
|
||||
elsif ($on_why) {
|
||||
# "Why" lines are indented four characters.
|
||||
$line =~ s/^\s{4}//;
|
||||
$current_data{comment} .= "$line\n";
|
||||
}
|
||||
else {
|
||||
$self->debug(
|
||||
"Extra Audit-Trail line on $bug->{product} $bug->{bug_id}:"
|
||||
. " $line\n", 2);
|
||||
$extra_comment .= "$line\n";
|
||||
}
|
||||
}
|
||||
$self->_store_audit_change(\@changes, $current_field, \%current_data);
|
||||
return (\@changes, $extra_comment);
|
||||
}
|
||||
|
||||
sub _store_audit_change {
|
||||
my ($self, $changes, $old_field, $current_data) = @_;
|
||||
|
||||
$current_data->{field} = $old_field;
|
||||
$current_data->{removed} =
|
||||
$self->translate_value($old_field, $current_data->{removed});
|
||||
$current_data->{added} =
|
||||
$self->translate_value($old_field, $current_data->{added});
|
||||
push(@$changes, { %$current_data });
|
||||
}
|
||||
|
||||
sub _parse_attachments {
|
||||
my ($self, $fields) = @_;
|
||||
my $unformatted = delete $fields->{'Unformatted'};
|
||||
my $gnats_boundary = GNATS_BOUNDARY;
|
||||
# A sanity checker to make sure that we're parsing attachments right.
|
||||
my $num_attachments = 0;
|
||||
$num_attachments++ while ($unformatted =~ /\Q$gnats_boundary\E/g);
|
||||
# Sometimes there's a GNATS_BOUNDARY that is on the same line as other data.
|
||||
$unformatted =~ s/(\S\s*)\Q$gnats_boundary\E$/$1\n$gnats_boundary/mg;
|
||||
# Often the "Unformatted" section starts with stuff before
|
||||
# ----gnatsweb-attachment---- that isn't necessary.
|
||||
$unformatted =~ s/^\s*From:.+?Reply-to:[^\n]+//s;
|
||||
$unformatted = trim($unformatted);
|
||||
return [] if !$unformatted;
|
||||
$self->debug('Reading attachments...', 2);
|
||||
my $boundary = generate_random_password(48);
|
||||
$unformatted =~ s/\Q$gnats_boundary\E/--$boundary/g;
|
||||
# Sometimes the whole Unformatted section is indented by exactly
|
||||
# one space, and needs to be fixed.
|
||||
if ($unformatted =~ /--\Q$boundary\E\n /) {
|
||||
$unformatted =~ s/^ //mg;
|
||||
}
|
||||
$unformatted = <<END;
|
||||
From: nobody
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/mixed; boundary="$boundary"
|
||||
|
||||
This is a multi-part message in MIME format.
|
||||
--$boundary
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
$unformatted
|
||||
--$boundary--
|
||||
END
|
||||
my $email = new Email::MIME(\$unformatted);
|
||||
my @parts = $email->parts;
|
||||
# Remove the fake body.
|
||||
my $part1 = shift @parts;
|
||||
if ($part1->body) {
|
||||
$self->debug(" Additional Unformatted data found on "
|
||||
. $fields->{Category} . " bug " . $fields->{Number});
|
||||
$self->debug($part1->body, 3);
|
||||
$fields->{_add_comment} .= "\n\nUnformatted:\n" . $part1->body;
|
||||
}
|
||||
|
||||
my @attachments;
|
||||
foreach my $part (@parts) {
|
||||
$self->debug(' Parsing attachment: ' . $part->filename);
|
||||
my $temp_fh = IO::File->new_tmpfile or die ("Can't create tempfile: $!");
|
||||
$temp_fh->binmode;
|
||||
print $temp_fh $part->body;
|
||||
my $content_type = $part->content_type;
|
||||
$content_type =~ s/; name=.+$//;
|
||||
my $attachment = { filename => $part->filename,
|
||||
description => $part->filename,
|
||||
mimetype => $content_type,
|
||||
data => $temp_fh };
|
||||
$self->debug($attachment, 3);
|
||||
push(@attachments, $attachment);
|
||||
}
|
||||
|
||||
if (scalar(@attachments) ne $num_attachments) {
|
||||
warn "WARNING: Expected $num_attachments attachments but got "
|
||||
. scalar(@attachments) . "\n" ;
|
||||
$self->debug($unformatted, 3);
|
||||
}
|
||||
return \@attachments;
|
||||
}
|
||||
|
||||
sub translate_value {
|
||||
my $self = shift;
|
||||
my ($field, $value, $options) = @_;
|
||||
my $original_value = $value;
|
||||
$options ||= {};
|
||||
|
||||
if (!ref($value) and grep($_ eq $field, $self->USER_FIELDS)) {
|
||||
if ($value =~ /(\S+\@\S+)/) {
|
||||
$value = $1;
|
||||
$value =~ s/^<//;
|
||||
$value =~ s/>$//;
|
||||
}
|
||||
else {
|
||||
# Sometimes names have extra stuff on the end like "(Somebody's Name)"
|
||||
$value =~ s/\s+\(.+\)$//;
|
||||
# Sometimes user fields look like "(user)" instead of just "user".
|
||||
$value =~ s/^\((.+)\)$/$1/;
|
||||
$value = trim($value);
|
||||
}
|
||||
}
|
||||
|
||||
if ($field eq 'version' and $value ne '') {
|
||||
my $version_re = $self->config('version_regex');
|
||||
if ($version_re and $value =~ $version_re) {
|
||||
$value = $1;
|
||||
}
|
||||
# In the GNATS that I tested this with, there were many extremely long
|
||||
# values for "version" that caused some import problems (they were
|
||||
# longer than the max allowed version value). So if the version value
|
||||
# is longer than 32 characters, pull out the first thing that looks
|
||||
# like a version number.
|
||||
elsif (length($value) > LONG_VERSION_LENGTH) {
|
||||
$value =~ s/^.+?\b(\d[\w\.]+)\b.+$/$1/;
|
||||
}
|
||||
}
|
||||
|
||||
my @args = @_;
|
||||
$args[1] = $value;
|
||||
|
||||
$value = $self->SUPER::translate_value(@args);
|
||||
return $value if ref $value;
|
||||
|
||||
if (grep($_ eq $field, $self->USER_FIELDS)) {
|
||||
my $from_value = $value;
|
||||
$value = $self->user_to_email($value);
|
||||
$args[1] = $value;
|
||||
# If we got something new from user_to_email, do any necessary
|
||||
# translation of it.
|
||||
$value = $self->SUPER::translate_value(@args);
|
||||
if (!$options->{check_only}) {
|
||||
$self->add_user($from_value, $value);
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
1;
|
|
@ -24,6 +24,7 @@ use strict;
|
|||
package Bugzilla::Object;
|
||||
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::Hook;
|
||||
use Bugzilla::Util;
|
||||
use Bugzilla::Error;
|
||||
|
||||
|
@ -37,6 +38,11 @@ use constant UPDATE_VALIDATORS => {};
|
|||
use constant NUMERIC_COLUMNS => ();
|
||||
use constant DATE_COLUMNS => ();
|
||||
|
||||
# This allows the JSON-RPC interface to return Bugzilla::Object instances
|
||||
# as though they were hashes. In the future, this may be modified to return
|
||||
# less information.
|
||||
sub TO_JSON { return { %{ $_[0] } }; }
|
||||
|
||||
###############################
|
||||
#### Initialization ####
|
||||
###############################
|
||||
|
@ -117,12 +123,29 @@ sub check {
|
|||
if (!ref $param) {
|
||||
$param = { name => $param };
|
||||
}
|
||||
|
||||
# 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});
|
||||
my $check_param = exists $param->{id} ? 'id' : 'name';
|
||||
$param->{$check_param} = trim($param->{$check_param});
|
||||
# If somebody passes us "0", we want to throw an error like
|
||||
# "there is no X with the name 0". This is true even for ids. So here,
|
||||
# we only check if the parameter is undefined or empty.
|
||||
if (!defined $param->{$check_param} or $param->{$check_param} eq '') {
|
||||
ThrowUserError('object_not_specified', { class => $class });
|
||||
}
|
||||
|
||||
my $obj = $class->new($param);
|
||||
if (!$obj) {
|
||||
# We don't want to override the normal template "user" object if
|
||||
# "user" is one of the params.
|
||||
delete $param->{user};
|
||||
if (my $error = delete $param->{_error}) {
|
||||
ThrowUserError($error, { %$param, class => $class });
|
||||
}
|
||||
else {
|
||||
ThrowUserError('object_does_not_exist', { %$param, class => $class });
|
||||
}
|
||||
}
|
||||
return $obj;
|
||||
}
|
||||
|
||||
|
@ -235,7 +258,12 @@ sub _do_list_select {
|
|||
$sql .= " $postamble" if $postamble;
|
||||
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $objects = $dbh->selectall_arrayref($sql, {Slice=>{}}, @$values);
|
||||
# Sometimes the values are tainted, but we don't want to untaint them
|
||||
# for the caller. So we copy the array. It's safe to untaint because
|
||||
# they're only used in placeholders here.
|
||||
my @untainted = @{ $values || [] };
|
||||
trick_taint($_) foreach @untainted;
|
||||
my $objects = $dbh->selectall_arrayref($sql, {Slice=>{}}, @untainted);
|
||||
bless ($_, $class) foreach @$objects;
|
||||
return $objects
|
||||
}
|
||||
|
@ -261,6 +289,10 @@ sub set {
|
|||
superclass => __PACKAGE__,
|
||||
function => 'Bugzilla::Object->set' });
|
||||
|
||||
Bugzilla::Hook::process('object_before_set',
|
||||
{ object => $self, field => $field,
|
||||
value => $value });
|
||||
|
||||
my %validators = (%{$self->VALIDATORS}, %{$self->UPDATE_VALIDATORS});
|
||||
if (exists $validators{$field}) {
|
||||
my $validator = $validators{$field};
|
||||
|
@ -273,6 +305,9 @@ sub set {
|
|||
}
|
||||
|
||||
$self->{$field} = $value;
|
||||
|
||||
Bugzilla::Hook::process('object_end_of_set',
|
||||
{ object => $self, field => $field });
|
||||
}
|
||||
|
||||
sub set_all {
|
||||
|
@ -281,6 +316,8 @@ sub set_all {
|
|||
my $method = "set_$key";
|
||||
$self->$method($params->{$key});
|
||||
}
|
||||
Bugzilla::Hook::process('object_end_of_set_all', { object => $self,
|
||||
params => $params });
|
||||
}
|
||||
|
||||
sub update {
|
||||
|
@ -324,6 +361,10 @@ sub update {
|
|||
$dbh->do("UPDATE $table SET $columns WHERE $id_field = ?", undef,
|
||||
@values, $self->id) if @values;
|
||||
|
||||
Bugzilla::Hook::process('object_end_of_update',
|
||||
{ object => $self, old_object => $old_self,
|
||||
changes => \%changes });
|
||||
|
||||
$dbh->bz_commit_transaction();
|
||||
|
||||
if (wantarray) {
|
||||
|
@ -335,6 +376,7 @@ sub update {
|
|||
|
||||
sub remove_from_db {
|
||||
my $self = shift;
|
||||
Bugzilla::Hook::process('object_before_delete', { object => $self });
|
||||
my $table = $self->DB_TABLE;
|
||||
my $id_field = $self->ID_FIELD;
|
||||
Bugzilla->dbh->do("DELETE FROM $table WHERE $id_field = ?",
|
||||
|
@ -346,6 +388,15 @@ sub remove_from_db {
|
|||
#### Subroutines ######
|
||||
###############################
|
||||
|
||||
sub any_exist {
|
||||
my $class = shift;
|
||||
my $table = $class->DB_TABLE;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $any_exist = $dbh->selectrow_array(
|
||||
"SELECT 1 FROM $table " . $dbh->sql_limit(1));
|
||||
return $any_exist ? 1 : 0;
|
||||
}
|
||||
|
||||
sub create {
|
||||
my ($class, $params) = @_;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
@ -373,6 +424,11 @@ sub _check_field {
|
|||
sub check_required_create_fields {
|
||||
my ($class, $params) = @_;
|
||||
|
||||
# This hook happens here so that even subclasses that don't call
|
||||
# SUPER::create are still affected by the hook.
|
||||
Bugzilla::Hook::process('object_before_create', { class => $class,
|
||||
params => $params });
|
||||
|
||||
foreach my $field ($class->REQUIRED_CREATE_FIELDS) {
|
||||
ThrowCodeError('param_required',
|
||||
{ function => "${class}->create", param => $field })
|
||||
|
@ -403,6 +459,9 @@ sub run_create_validators {
|
|||
$field_values{$field} = $value;
|
||||
}
|
||||
|
||||
Bugzilla::Hook::process('object_end_of_create_validators',
|
||||
{ class => $class, params => \%field_values });
|
||||
|
||||
return \%field_values;
|
||||
}
|
||||
|
||||
|
@ -903,6 +962,11 @@ Returns C<1> if the passed-in value is true, C<0> otherwise.
|
|||
|
||||
=over
|
||||
|
||||
=item C<any_exist>
|
||||
|
||||
Returns C<1> if there are any of these objects in the database,
|
||||
C<0> otherwise.
|
||||
|
||||
=item C<get_all>
|
||||
|
||||
Description: Returns all objects in this table from the database.
|
||||
|
|
|
@ -31,6 +31,7 @@ use Bugzilla::Install::Requirements;
|
|||
use Bugzilla::Mailer;
|
||||
use Bugzilla::Series;
|
||||
use Bugzilla::FlagType::UserList;
|
||||
use Bugzilla::Hook;
|
||||
|
||||
# Currently, we only implement enough of the Bugzilla::Field::Choice
|
||||
# interface to control the visibility of other fields.
|
||||
|
@ -49,18 +50,18 @@ use constant NAME_FIELD => 'name';
|
|||
use constant LIST_ORDER => 'name';
|
||||
|
||||
use constant DB_COLUMNS => qw(
|
||||
id
|
||||
name
|
||||
classification_id
|
||||
description
|
||||
milestoneurl
|
||||
disallownew
|
||||
votesperuser
|
||||
maxvotesperbug
|
||||
votestoconfirm
|
||||
defaultmilestone
|
||||
wiki_url
|
||||
notimetracking
|
||||
id
|
||||
name
|
||||
wiki_url
|
||||
notimetracking
|
||||
classification_id
|
||||
description
|
||||
isactive
|
||||
votesperuser
|
||||
maxvotesperbug
|
||||
votestoconfirm
|
||||
defaultmilestone
|
||||
allows_unconfirmed
|
||||
);
|
||||
|
||||
use constant REQUIRED_CREATE_FIELDS => qw(
|
||||
|
@ -71,25 +72,25 @@ use constant REQUIRED_CREATE_FIELDS => qw(
|
|||
|
||||
use constant UPDATE_COLUMNS => qw(
|
||||
name
|
||||
wiki_url
|
||||
notimetracking
|
||||
description
|
||||
defaultmilestone
|
||||
milestoneurl
|
||||
disallownew
|
||||
isactive
|
||||
votesperuser
|
||||
maxvotesperbug
|
||||
votestoconfirm
|
||||
wiki_url
|
||||
notimetracking
|
||||
allows_unconfirmed
|
||||
);
|
||||
|
||||
use constant VALIDATORS => {
|
||||
allows_unconfirmed => \&Bugzilla::Object::check_boolean,
|
||||
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,
|
||||
isactive => \&Bugzilla::Object::check_boolean,
|
||||
votesperuser => \&_check_votes_per_user,
|
||||
maxvotesperbug => \&_check_votes_per_bug,
|
||||
votestoconfirm => \&_check_votes_to_confirm,
|
||||
|
@ -111,20 +112,26 @@ sub create {
|
|||
|
||||
my $params = $class->run_create_validators(@_);
|
||||
# Some fields do not exist in the DB as is.
|
||||
$params->{classification_id} = delete $params->{classification};
|
||||
if (defined $params->{classification}) {
|
||||
$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);
|
||||
Bugzilla->user->clear_product_cache();
|
||||
|
||||
# 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});
|
||||
Bugzilla::Version->create({name => $version, product => $product});
|
||||
Bugzilla::Milestone->create({ name => $product->default_milestone,
|
||||
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;
|
||||
|
||||
Bugzilla::Hook::process('product_end_of_create', { product => $product });
|
||||
|
||||
$dbh->bz_commit_transaction();
|
||||
return $product;
|
||||
}
|
||||
|
@ -367,17 +374,26 @@ sub update {
|
|||
$dbh->bz_commit_transaction();
|
||||
# Changes have been committed.
|
||||
delete $self->{check_group_controls};
|
||||
Bugzilla->user->clear_product_cache();
|
||||
|
||||
# Now that changes have been committed, we can send emails to voters.
|
||||
foreach my $msg (@msgs) {
|
||||
MessageToMTA($msg);
|
||||
}
|
||||
|
||||
# And send out emails about changed bugs
|
||||
require Bugzilla::BugMail;
|
||||
foreach my $bug_id (@{ $changes->{'confirmed_bugs'} || [] }) {
|
||||
my $sent_bugmail = Bugzilla::BugMail::Send(
|
||||
$bug_id, { changer => Bugzilla->user->login });
|
||||
$changes->{'confirmed_bugs_sent_bugmail'}->{$bug_id} = $sent_bugmail;
|
||||
}
|
||||
|
||||
return $changes;
|
||||
}
|
||||
|
||||
sub remove_from_db {
|
||||
my $self = shift;
|
||||
my ($self, $params) = @_;
|
||||
my $user = Bugzilla->user;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
|
@ -400,8 +416,33 @@ sub remove_from_db {
|
|||
}
|
||||
}
|
||||
|
||||
# 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);
|
||||
if ($params->{delete_series}) {
|
||||
my $series_ids =
|
||||
$dbh->selectcol_arrayref('SELECT series_id
|
||||
FROM series
|
||||
INNER JOIN series_categories
|
||||
ON series_categories.id = series.category
|
||||
WHERE series_categories.name = ?',
|
||||
undef, $self->name);
|
||||
|
||||
if (scalar @$series_ids) {
|
||||
$dbh->do('DELETE FROM series WHERE ' . $dbh->sql_in('series_id', $series_ids));
|
||||
}
|
||||
|
||||
# If no subcategory uses this product name, completely purge it.
|
||||
my $in_use =
|
||||
$dbh->selectrow_array('SELECT 1
|
||||
FROM series
|
||||
INNER JOIN series_categories
|
||||
ON series_categories.id = series.subcategory
|
||||
WHERE series_categories.name = ? ' .
|
||||
$dbh->sql_limit(1),
|
||||
undef, $self->name);
|
||||
if (!$in_use) {
|
||||
$dbh->do('DELETE FROM series_categories WHERE name = ?', undef, $self->name);
|
||||
}
|
||||
}
|
||||
|
||||
$dbh->do("DELETE FROM products WHERE id = ?", undef, $self->id);
|
||||
|
||||
$dbh->bz_commit_transaction();
|
||||
|
@ -570,10 +611,9 @@ sub _create_bug_group {
|
|||
|
||||
# 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));
|
||||
(group_id, product_id, membercontrol, othercontrol)
|
||||
VALUES (?, ?, ?, ?)',
|
||||
undef, ($group->id, $self->id, CONTROLMAPDEFAULT, CONTROLMAPNA));
|
||||
}
|
||||
|
||||
sub _create_series {
|
||||
|
@ -604,15 +644,15 @@ sub _create_series {
|
|||
}
|
||||
|
||||
sub set_name { $_[0]->set('name', $_[1]); }
|
||||
sub set_wiki_url { $_[0]->set('wiki_url', $_[1]); }
|
||||
sub set_notimetracking { $_[0]->set('notimetracking', $_[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_is_active { $_[0]->set('isactive', $_[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_wiki_url { $_[0]->set('wiki_url', $_[1]); }
|
||||
sub set_notimetracking { $_[0]->set('notimetracking', $_[1]); }
|
||||
sub set_allows_unconfirmed { $_[0]->set('allows_unconfirmed', $_[1]); }
|
||||
|
||||
sub set_group_controls {
|
||||
my ($self, $group, $settings) = @_;
|
||||
|
@ -710,8 +750,8 @@ sub group_controls {
|
|||
# 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
|
||||
FROM groups
|
||||
LEFT JOIN group_control_map
|
||||
ON id = group_id
|
||||
$where_or_and product_id = ?
|
||||
$and_or_where isbuggroup = 1};
|
||||
|
@ -866,15 +906,15 @@ sub flag_types
|
|||
$cl->merge($flagtypes{$flagtype->{id}}->{custom_list});
|
||||
$cl->merge($flagtype->{custom_list});
|
||||
$flagtypes{$flagtype->{id}}->{custom_list} = $cl;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$self->{flag_types}->{$type} = [
|
||||
sort { $a->{'sortkey'} <=> $b->{'sortkey'}
|
||||
|| $a->{'name'} cmp $b->{'name'} }
|
||||
values %flagtypes
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $self->{'flag_types'};
|
||||
|
@ -884,9 +924,9 @@ sub flag_types
|
|||
#### Accessors ######
|
||||
###############################
|
||||
|
||||
sub allows_unconfirmed { return $_[0]->{'allows_unconfirmed'}; }
|
||||
sub description { return $_[0]->{'description'}; }
|
||||
sub milestone_url { return $_[0]->{'milestoneurl'}; }
|
||||
sub disallow_new { return $_[0]->{'disallownew'}; }
|
||||
sub is_active { return $_[0]->{'isactive'}; }
|
||||
sub votes_per_user { return $_[0]->{'votesperuser'}; }
|
||||
sub max_votes_per_bug { return $_[0]->{'maxvotesperbug'}; }
|
||||
sub votes_to_confirm { return $_[0]->{'votestoconfirm'}; }
|
||||
|
@ -913,6 +953,17 @@ sub check_product {
|
|||
return $product;
|
||||
}
|
||||
|
||||
sub check {
|
||||
my ($class, $params) = @_;
|
||||
$params = { name => $params } if !ref $params;
|
||||
$params->{_error} = 'product_access_denied';
|
||||
my $product = $class->SUPER::check($params);
|
||||
if (!Bugzilla->user->can_access_product($product)) {
|
||||
ThrowUserError('product_access_denied', $params);
|
||||
}
|
||||
return $product;
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
@ -940,8 +991,7 @@ Bugzilla::Product - Bugzilla product class.
|
|||
my $id = $product->id;
|
||||
my $name = $product->name;
|
||||
my $description = $product->description;
|
||||
my $milestoneurl = $product->milestone_url;
|
||||
my $disallownew = $product->disallow_new;
|
||||
my $isactive = $product->is_active;
|
||||
my $votesperuser = $product->votes_per_user;
|
||||
my $maxvotesperbug = $product->max_votes_per_bug;
|
||||
my $votestoconfirm = $product->votes_to_confirm;
|
||||
|
@ -949,6 +999,7 @@ Bugzilla::Product - Bugzilla product class.
|
|||
my $notimetracking = $product->notimetracking;
|
||||
my $defaultmilestone = $product->default_milestone;
|
||||
my $classificationid = $product->classification_id;
|
||||
my $allows_unconfirmed = $product->allows_unconfirmed;
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
# Joel Peshkin <bugreport@peshkin.net>
|
||||
# Lance Larsh <lance.larsh@oracle.com>
|
||||
# Jesse Clark <jjclark1982@gmail.com>
|
||||
# Rémi Zara <remi_zara@mac.com>
|
||||
|
||||
use strict;
|
||||
|
||||
|
@ -120,12 +121,16 @@ sub COLUMNS {
|
|||
my %special_sql = (
|
||||
deadline => $dbh->sql_date_format('bugs.deadline', '%Y-%m-%d'),
|
||||
actual_time => $actual_time,
|
||||
|
||||
percentage_complete =>
|
||||
"(CASE WHEN $actual_time + bugs.remaining_time = 0.0"
|
||||
. " THEN 0.0"
|
||||
. " ELSE 100"
|
||||
. " * ($actual_time / ($actual_time + bugs.remaining_time))"
|
||||
. " END)",
|
||||
|
||||
'flagtypes.name' => $dbh->sql_group_concat('DISTINCT '
|
||||
. $dbh->sql_string_concat('flagtypes.name', 'flags.status'), "', '"),
|
||||
);
|
||||
|
||||
# Backward-compatibility for old field names. Goes new_name => old_name.
|
||||
|
@ -170,7 +175,7 @@ sub COLUMNS {
|
|||
# The short_short_desc column is identical to short_desc
|
||||
$columns{'short_short_desc'} = $columns{'short_desc'};
|
||||
|
||||
Bugzilla::Hook::process("buglist-columns", { columns => \%columns });
|
||||
Bugzilla::Hook::process('buglist_columns', { columns => \%columns });
|
||||
|
||||
$cache->{search_columns} = \%columns;
|
||||
return $cache->{search_columns};
|
||||
|
@ -194,6 +199,7 @@ sub init {
|
|||
my $self = shift;
|
||||
my @fields = @{ $self->{'fields'} || [] };
|
||||
my $params = $self->{'params'};
|
||||
$params->convert_old_params();
|
||||
$self->{'user'} ||= Bugzilla->user;
|
||||
my $user = $self->{'user'};
|
||||
|
||||
|
@ -216,13 +222,13 @@ sub init {
|
|||
|
||||
my @multi_select_fields = Bugzilla->get_fields({
|
||||
type => [FIELD_TYPE_MULTI_SELECT, FIELD_TYPE_BUG_URLS],
|
||||
obsolete => 0 });
|
||||
obsolete => 0 });
|
||||
foreach my $field (@select_fields) {
|
||||
my $name = $field->name;
|
||||
next if $name eq 'product'; # products don't have sortkeys.
|
||||
$special_order{$name} = [ "$name.sortkey", "$name.value" ],
|
||||
$special_order_join{$name} =
|
||||
"LEFT JOIN $name ON $name.value = bugs.$name";
|
||||
"LEFT JOIN $name ON $name.value = bugs.$name";
|
||||
}
|
||||
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
@ -275,6 +281,11 @@ sub init {
|
|||
}
|
||||
### end Testopia ###
|
||||
|
||||
if (grep($_ eq 'flagtypes.name', @fields)) {
|
||||
push(@supptables, "LEFT JOIN flags ON flags.bug_id = bugs.bug_id AND attach_id IS NULL");
|
||||
push(@supptables, "LEFT JOIN flagtypes ON flagtypes.id = flags.type_id");
|
||||
}
|
||||
|
||||
my $minvotes;
|
||||
if (defined $params->param('votes')) {
|
||||
my $c = trim($params->param('votes'));
|
||||
|
@ -287,21 +298,16 @@ sub init {
|
|||
}
|
||||
}
|
||||
|
||||
if ($params->param('bug_id')) {
|
||||
my $type = "anyexact";
|
||||
if ($params->param('bugidtype') && $params->param('bugidtype') eq 'exclude') {
|
||||
$type = "nowords";
|
||||
}
|
||||
push(@specialchart, ["bug_id", $type, join(',', $params->param('bug_id'))]);
|
||||
}
|
||||
|
||||
# If the user has selected all of either status or resolution, change to
|
||||
# selecting none. This is functionally equivalent, but quite a lot faster.
|
||||
# Also, if the status is __open__ or __closed__, translate those
|
||||
# into their equivalent lists of open and closed statuses.
|
||||
if ($params->param('bug_status')) {
|
||||
my @bug_statuses = $params->param('bug_status');
|
||||
my @legal_statuses = @{get_legal_field_values('bug_status')};
|
||||
# Also include inactive bug statuses, as you can query them.
|
||||
my @legal_statuses =
|
||||
map {$_->name} @{Bugzilla::Field->new({name => 'bug_status'})->legal_values};
|
||||
|
||||
if (scalar(@bug_statuses) == scalar(@legal_statuses)
|
||||
|| $bug_statuses[0] eq "__all__")
|
||||
{
|
||||
|
@ -319,33 +325,37 @@ sub init {
|
|||
|
||||
if ($params->param('resolution')) {
|
||||
my @resolutions = $params->param('resolution');
|
||||
my $legal_resolutions = get_legal_field_values('resolution');
|
||||
# Also include inactive resolutions, as you can query them.
|
||||
my $legal_resolutions = Bugzilla::Field->new({name => 'resolution'})->legal_values;
|
||||
if (scalar(@resolutions) == scalar(@$legal_resolutions)) {
|
||||
$params->delete('resolution');
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
push(@legal_fields, map { $_->name } @multi_select_fields);
|
||||
|
||||
foreach my $field ($params->param()) {
|
||||
if (lsearch(\@legal_fields, $field) != -1) {
|
||||
push(@specialchart, [$field, "anyexact",
|
||||
join(',', $params->param($field))]);
|
||||
# All fields that don't have a . in their name should be specifyable
|
||||
# in the URL directly.
|
||||
my @legal_fields = grep { $_->name !~ /\./ } Bugzilla->get_fields;
|
||||
if (!$user->is_timetracker) {
|
||||
foreach my $field (TIMETRACKING_FIELDS) {
|
||||
@legal_fields = grep { $_->name ne $field } @legal_fields;
|
||||
}
|
||||
}
|
||||
|
||||
if ($params->param('keywords')) {
|
||||
my $t = $params->param('keywords_type');
|
||||
if (!$t || $t eq "or") {
|
||||
$t = "anywords";
|
||||
foreach my $field ($params->param()) {
|
||||
if (grep { $_->name eq $field } @legal_fields) {
|
||||
my $type = $params->param("${field}_type");
|
||||
if (!$type) {
|
||||
if ($field eq 'keywords') {
|
||||
$type = 'anywords';
|
||||
}
|
||||
else {
|
||||
$type = 'anyexact';
|
||||
}
|
||||
}
|
||||
$type = 'matches' if $field eq 'content';
|
||||
push(@specialchart, [$field, $type,
|
||||
join(',', $params->param($field))]);
|
||||
}
|
||||
push(@specialchart, ["keywords", $t, $params->param('keywords')]);
|
||||
}
|
||||
|
||||
foreach my $id ("1", "2") {
|
||||
|
@ -366,7 +376,7 @@ sub init {
|
|||
}
|
||||
}
|
||||
if ($params->param("emaillongdesc$id")) {
|
||||
push(@clist, "commenter", $type, $email);
|
||||
push(@clist, "commenter", $type, $email);
|
||||
}
|
||||
if (@clist) {
|
||||
push(@specialchart, \@clist);
|
||||
|
@ -427,9 +437,9 @@ sub init {
|
|||
my $value_term = " AND actcheck.added = $sql_chvalue";
|
||||
# ---- vfilippov@custis.ru 2010-02-01
|
||||
# Search using bugs.delta_ts is not correct. It's "LAST changed in", not "Changed in".
|
||||
my $bug_creation_clause;
|
||||
my @list;
|
||||
my @actlist;
|
||||
my $bug_creation_clause;
|
||||
my @list;
|
||||
my @actlist;
|
||||
my $seen_longdesc;
|
||||
my $need_commenter;
|
||||
foreach my $f (@chfield)
|
||||
|
@ -437,27 +447,27 @@ sub init {
|
|||
my $term;
|
||||
if ($f eq "[Bug creation]")
|
||||
{
|
||||
# Treat [Bug creation] differently because we need to look
|
||||
# at bugs.creation_ts rather than the bugs_activity table.
|
||||
my @l;
|
||||
if ($sql_chfrom) {
|
||||
my $term = "bugs.creation_ts >= $sql_chfrom";
|
||||
push(@l, $term);
|
||||
$self->search_description({
|
||||
field => 'creation_ts', type => 'greaterthaneq',
|
||||
value => $chfieldfrom, term => $term,
|
||||
});
|
||||
# Treat [Bug creation] differently because we need to look
|
||||
# at bugs.creation_ts rather than the bugs_activity table.
|
||||
my @l;
|
||||
if ($sql_chfrom) {
|
||||
my $term = "bugs.creation_ts >= $sql_chfrom";
|
||||
push(@l, $term);
|
||||
$self->search_description({
|
||||
field => 'creation_ts', type => 'greaterthaneq',
|
||||
value => $chfieldfrom, term => $term,
|
||||
});
|
||||
}
|
||||
if ($sql_chto) {
|
||||
my $term = "bugs.creation_ts <= $sql_chto";
|
||||
push(@l, $term);
|
||||
$self->search_description({
|
||||
field => 'creation_ts', type => 'lessthaneq',
|
||||
value => $chfieldto, term => $term,
|
||||
});
|
||||
}
|
||||
$bug_creation_clause = "(" . join(' AND ', @l) . ")";
|
||||
}
|
||||
if ($sql_chto) {
|
||||
my $term = "bugs.creation_ts <= $sql_chto";
|
||||
push(@l, $term);
|
||||
$self->search_description({
|
||||
field => 'creation_ts', type => 'lessthaneq',
|
||||
value => $chfieldto, term => $term,
|
||||
});
|
||||
}
|
||||
$bug_creation_clause = "(" . join(' AND ', @l) . ")";
|
||||
}
|
||||
elsif ($f eq 'longdesc' || $f eq 'longdescs.isprivate' || $f eq 'commenter')
|
||||
{
|
||||
# Treat comment properties differently because we need to look at longdescs table.
|
||||
|
@ -468,7 +478,7 @@ sub init {
|
|||
# User is searching for a comment with specific text,
|
||||
# but that has no sense if $chvalue was already used for comment privacy.
|
||||
$seen_longdesc = [ $term = "INSTR(actcheck_comment.thetext, $sql_chvalue) > 0" ];
|
||||
}
|
||||
}
|
||||
elsif ($f eq 'commenter')
|
||||
{
|
||||
# User is searching for a comment with specific author
|
||||
|
@ -499,30 +509,30 @@ sub init {
|
|||
$term = 1;
|
||||
if ($sql_chvalue)
|
||||
{
|
||||
$self->search_description({
|
||||
$self->search_description({
|
||||
field => $f, type => 'changedto',
|
||||
value => $chvalue, term => $value_term,
|
||||
});
|
||||
}
|
||||
value => $chvalue, term => $value_term,
|
||||
});
|
||||
}
|
||||
}
|
||||
if ($term)
|
||||
{
|
||||
if ($sql_chfrom)
|
||||
{
|
||||
$self->search_description({
|
||||
$self->search_description({
|
||||
field => $f, type => 'changedafter',
|
||||
value => $chfieldfrom, term => $from_term,
|
||||
});
|
||||
}
|
||||
value => $chfieldfrom, term => $from_term,
|
||||
});
|
||||
}
|
||||
if ($sql_chto)
|
||||
{
|
||||
$self->search_description({
|
||||
$self->search_description({
|
||||
field => $f, type => 'changedbefore',
|
||||
value => $chfieldto, term => $to_term,
|
||||
});
|
||||
value => $chfieldto, term => $to_term,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
my $extra;
|
||||
if (!$bug_creation_clause && !$seen_longdesc || @actlist)
|
||||
|
@ -555,10 +565,10 @@ sub init {
|
|||
" ON actcheck_commenter.userid=actcheck_comment.who AND $need_commenter" : '') .
|
||||
" WHERE $extra";
|
||||
|
||||
# Now that we're done using @list to determine if there are any
|
||||
# regular fields to search (and thus we need bugs_activity),
|
||||
# add the [Bug creation] criterion to the list so we can OR it
|
||||
# together with the others.
|
||||
# Now that we're done using @list to determine if there are any
|
||||
# regular fields to search (and thus we need bugs_activity),
|
||||
# add the [Bug creation] criterion to the list so we can OR it
|
||||
# together with the others.
|
||||
@list = ("bugs.bug_id IN (" . join(' UNION ', @list) . ")");
|
||||
push @list, $bug_creation_clause if $bug_creation_clause;
|
||||
|
||||
|
@ -568,38 +578,34 @@ sub init {
|
|||
my $sql_deadlinefrom;
|
||||
my $sql_deadlineto;
|
||||
if ($user->is_timetracker) {
|
||||
my $deadlinefrom;
|
||||
my $deadlineto;
|
||||
my $deadlinefrom;
|
||||
my $deadlineto;
|
||||
|
||||
if ($params->param('deadlinefrom')){
|
||||
$deadlinefrom = $params->param('deadlinefrom');
|
||||
validate_date($deadlinefrom)
|
||||
|| ThrowUserError('illegal_date', {date => $deadlinefrom,
|
||||
format => 'YYYY-MM-DD'});
|
||||
$sql_deadlinefrom = $dbh->quote($deadlinefrom);
|
||||
trick_taint($sql_deadlinefrom);
|
||||
my $term = "bugs.deadline >= $sql_deadlinefrom";
|
||||
push(@wherepart, $term);
|
||||
$self->search_description({
|
||||
field => 'deadline', type => 'greaterthaneq',
|
||||
value => $deadlinefrom, term => $term,
|
||||
});
|
||||
}
|
||||
if ($params->param('deadlinefrom')){
|
||||
$params->param('deadlinefrom', '') if lc($params->param('deadlinefrom')) eq 'now';
|
||||
$deadlinefrom = SqlifyDate($params->param('deadlinefrom'));
|
||||
$sql_deadlinefrom = $dbh->quote($deadlinefrom);
|
||||
trick_taint($sql_deadlinefrom);
|
||||
my $term = "bugs.deadline >= $sql_deadlinefrom";
|
||||
push(@wherepart, $term);
|
||||
$self->search_description({
|
||||
field => 'deadline', type => 'greaterthaneq',
|
||||
value => $deadlinefrom, term => $term,
|
||||
});
|
||||
}
|
||||
|
||||
if ($params->param('deadlineto')){
|
||||
$deadlineto = $params->param('deadlineto');
|
||||
validate_date($deadlineto)
|
||||
|| ThrowUserError('illegal_date', {date => $deadlineto,
|
||||
format => 'YYYY-MM-DD'});
|
||||
$sql_deadlineto = $dbh->quote($deadlineto);
|
||||
trick_taint($sql_deadlineto);
|
||||
my $term = "bugs.deadline <= $sql_deadlineto";
|
||||
push(@wherepart, $term);
|
||||
$self->search_description({
|
||||
field => 'deadline', type => 'lessthaneq',
|
||||
value => $deadlineto, term => $term,
|
||||
});
|
||||
}
|
||||
if ($params->param('deadlineto')){
|
||||
$params->param('deadlineto', '') if lc($params->param('deadlineto')) eq 'now';
|
||||
$deadlineto = SqlifyDate($params->param('deadlineto'));
|
||||
$sql_deadlineto = $dbh->quote($deadlineto);
|
||||
trick_taint($sql_deadlineto);
|
||||
my $term = "bugs.deadline <= $sql_deadlineto";
|
||||
push(@wherepart, $term);
|
||||
$self->search_description({
|
||||
field => 'deadline', type => 'lessthaneq',
|
||||
value => $deadlineto, term => $term,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
my @textfields = ("short_desc", "longdesc", "bug_file_loc", "status_whiteboard");
|
||||
|
@ -621,10 +627,6 @@ sub init {
|
|||
}
|
||||
}
|
||||
|
||||
if (defined $params->param('content')) {
|
||||
push(@specialchart, ['content', 'matches', $params->param('content')]);
|
||||
}
|
||||
|
||||
my $multi_fields = join('|', map($_->name, @multi_select_fields));
|
||||
|
||||
my $chartid;
|
||||
|
@ -666,7 +668,7 @@ sub init {
|
|||
"^long_?desc,changedby" => \&_long_desc_changedby,
|
||||
"^long_?desc,changedbefore" => \&_long_desc_changedbefore_after,
|
||||
"^long_?desc,changedafter" => \&_long_desc_changedbefore_after,
|
||||
"^content,matches" => \&_content_matches,
|
||||
"^content,(?:not)?matches" => \&_content_matches,
|
||||
"^content," => sub { ThrowUserError("search_content_without_matches"); },
|
||||
"^(?:deadline|creation_ts|delta_ts),(?:lessthan|greaterthan|equals|notequals),(?:-|\\+)?(?:\\d+)(?:[dDwWmMyY])\$" => \&_timestamp_compare,
|
||||
"^commenter,(?:equals|anyexact),(%\\w+%)" => \&_commenter_exact,
|
||||
|
@ -691,6 +693,7 @@ sub init {
|
|||
"^component,(?!changed)" => \&_component_nonchanged,
|
||||
"^product,(?!changed)" => \&_product_nonchanged,
|
||||
"^classification,(?!changed)" => \&_classification_nonchanged,
|
||||
"^keywords,(?:equals|notequals|anyexact|anyword|allwords|nowords)" => \&_keywords_exact,
|
||||
"^keywords,(?!changed)" => \&_keywords_nonchanged,
|
||||
"^dependson,(?!changed)" => \&_dependson_nonchanged,
|
||||
"^blocked,(?!changed)" => \&_blocked_nonchanged,
|
||||
|
@ -709,6 +712,7 @@ sub init {
|
|||
",notregexp" => \&_notregexp,
|
||||
",lessthan" => \&_lessthan,
|
||||
",matches" => sub { ThrowUserError("search_content_without_matches"); },
|
||||
",notmatches" => sub { ThrowUserError("search_content_without_matches"); },
|
||||
",greaterthan" => \&_greaterthan,
|
||||
",anyexact" => \&_anyexact,
|
||||
",anywordssubstr" => \&_anywordsubstr,
|
||||
|
@ -980,12 +984,22 @@ sub init {
|
|||
# Make sure we create a legal SQL query.
|
||||
@andlist = ("1 = 1") if !@andlist;
|
||||
|
||||
my @sql_fields = map { $_ eq EMPTY_COLUMN
|
||||
? EMPTY_COLUMN
|
||||
: COLUMNS->{$_}->{name}
|
||||
? COLUMNS->{$_}->{name} . ' AS ' . $_
|
||||
: $_ } @fields;
|
||||
|
||||
my @sql_fields;
|
||||
foreach my $field (@fields)
|
||||
{
|
||||
if (COLUMNS->{$field}->{name})
|
||||
{
|
||||
my $alias = $field;
|
||||
# Aliases cannot contain dots in them. We convert them to underscores.
|
||||
$alias =~ s/\./_/g;
|
||||
push @sql_fields, $field eq EMPTY_COLUMN
|
||||
? EMPTY_COLUMN : COLUMNS->{$field}->{name} . " AS $alias";
|
||||
}
|
||||
else
|
||||
{
|
||||
push @sql_fields, $field;
|
||||
}
|
||||
}
|
||||
my $query = "SELECT " . join(', ', @sql_fields) .
|
||||
" FROM $suppstring" .
|
||||
" LEFT JOIN bug_group_map " .
|
||||
|
@ -1018,7 +1032,7 @@ sub init {
|
|||
# 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)));
|
||||
qw(bug_id actual_time percentage_complete flagtypes.name)));
|
||||
my $col = COLUMNS->{$field}->{name};
|
||||
push(@groupby, $col) if !grep($_ eq $col, @groupby);
|
||||
}
|
||||
|
@ -1108,6 +1122,7 @@ sub GetByWordList {
|
|||
my ($field, $strs) = (@_);
|
||||
my @list;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
return [] unless defined $strs;
|
||||
|
||||
foreach my $w (split(/[\s,]+/, $strs)) {
|
||||
my $word = $w;
|
||||
|
@ -1234,6 +1249,8 @@ sub BuildOrderBy {
|
|||
}
|
||||
return;
|
||||
}
|
||||
# Aliases cannot contain dots in them. We convert them to underscores.
|
||||
$orderfield =~ s/\./_/g if exists COLUMNS->{$orderfield};
|
||||
|
||||
push(@$stringlist, trim($orderfield . ' ' . $orderdirection));
|
||||
}
|
||||
|
@ -1478,8 +1495,8 @@ sub _content_matches
|
|||
{
|
||||
my $self = shift;
|
||||
my %func_args = @_;
|
||||
my ($chartid, $supptables, $term, $groupby, $fields, $v) =
|
||||
@func_args{qw(chartid supptables term groupby fields v)};
|
||||
my ($chartid, $supptables, $term, $groupby, $fields, $t, $v) =
|
||||
@func_args{qw(chartid supptables term groupby fields t v)};
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
# "content" is an alias for columns containing text for which we
|
||||
|
@ -1489,10 +1506,31 @@ sub _content_matches
|
|||
# accept the "matches" operator, which is specific to full-text
|
||||
# index searches.
|
||||
|
||||
# Add the fulltext table to the query so we can search on it.
|
||||
my $table = "bugs_fulltext_$$chartid";
|
||||
my $comments_col = "comments";
|
||||
$comments_col = "comments_noprivate" unless $self->{'user'}->is_insider;
|
||||
push(@$supptables, "LEFT JOIN bugs_fulltext AS $table " .
|
||||
"ON bugs.bug_id = $table.bug_id");
|
||||
|
||||
# Create search terms to add to the SELECT and WHERE clauses.
|
||||
my ($term1, $rterm1) = $dbh->sql_fulltext_search("$table.$comments_col",
|
||||
$$v, 1);
|
||||
my ($term2, $rterm2) = $dbh->sql_fulltext_search("$table.short_desc",
|
||||
$$v, 2);
|
||||
$rterm1 = $term1 if !$rterm1;
|
||||
$rterm2 = $term2 if !$rterm2;
|
||||
|
||||
# The term to use in the WHERE clause.
|
||||
$$term = "$term1 > 0 OR $term2 > 0";
|
||||
|
||||
# In order to sort by relevance (in case the user requests it),
|
||||
# we SELECT the relevance value so we can add it to the ORDER BY
|
||||
# clause. Every time a new fulltext chart isadded, this adds more
|
||||
# terms to the relevance sql. (That doesn't make sense in
|
||||
# "NOT" charts, but Bugzilla never uses those with fulltext
|
||||
# by default.)
|
||||
#
|
||||
my $text = stem_text($$v);
|
||||
my ($term1, $rterm1) =
|
||||
$dbh->sql_fulltext_search("bugs_fulltext.$comments_col", $text, 1);
|
||||
|
@ -1926,7 +1964,7 @@ sub _classification_nonchanged {
|
|||
$$term);
|
||||
}
|
||||
|
||||
sub _keywords_nonchanged {
|
||||
sub _keywords_exact {
|
||||
my $self = shift;
|
||||
my %func_args = @_;
|
||||
my ($chartid, $v, $ff, $f, $t, $term, $supptables) =
|
||||
|
@ -1965,6 +2003,23 @@ sub _keywords_nonchanged {
|
|||
}
|
||||
}
|
||||
|
||||
sub _keywords_nonchanged {
|
||||
my $self = shift;
|
||||
my %func_args = @_;
|
||||
my ($chartid, $v, $ff, $f, $t, $term, $supptables) =
|
||||
@func_args{qw(chartid v ff f t term supptables)};
|
||||
|
||||
my $k_table = "keywords_$$chartid";
|
||||
my $kd_table = "keyworddefs_$$chartid";
|
||||
|
||||
push(@$supptables, "LEFT JOIN keywords AS $k_table " .
|
||||
"ON $k_table.bug_id = bugs.bug_id");
|
||||
push(@$supptables, "LEFT JOIN keyworddefs AS $kd_table " .
|
||||
"ON $kd_table.id = $k_table.keywordid");
|
||||
|
||||
$$f = "$kd_table.name";
|
||||
}
|
||||
|
||||
sub _dependson_nonchanged {
|
||||
my $self = shift;
|
||||
my %func_args = @_;
|
||||
|
@ -2313,8 +2368,8 @@ sub _changedby
|
|||
}
|
||||
my $id = login_to_id($$v, THROW_ERROR);
|
||||
push @$supptables, "LEFT JOIN bugs_activity AS $table " .
|
||||
"ON $table.bug_id = bugs.bug_id " .
|
||||
"AND $table.fieldid = $fieldid " .
|
||||
"ON $table.bug_id = bugs.bug_id " .
|
||||
"AND $table.fieldid = $fieldid " .
|
||||
"AND $table.who = $id";
|
||||
$$term = "($table.bug_when IS NOT NULL)";
|
||||
}
|
||||
|
@ -2350,76 +2405,26 @@ sub _not_in_search_results
|
|||
|
||||
sub LookupNamedQuery
|
||||
{
|
||||
my ($name, $sharer_id, $query_type, $throw_error, $as_user) = @_;
|
||||
my $user = $as_user || Bugzilla->login(LOGIN_REQUIRED);
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $owner_id;
|
||||
my ($name, $sharer_id, $query_type, $throw_error) = @_;
|
||||
$throw_error = 1 unless defined $throw_error;
|
||||
|
||||
# $name and $sharer_id are safe -- we only use them below in SELECT
|
||||
# placeholders and then in error messages (which are always HTML-filtered).
|
||||
$name || ThrowUserError("query_name_missing");
|
||||
trick_taint($name);
|
||||
if ($sharer_id)
|
||||
{
|
||||
$owner_id = $sharer_id;
|
||||
detaint_natural($owner_id);
|
||||
$owner_id || ThrowUserError('illegal_user_id', { userid => $sharer_id });
|
||||
}
|
||||
else
|
||||
{
|
||||
$owner_id = $user->id;
|
||||
Bugzilla->login(LOGIN_REQUIRED);
|
||||
|
||||
my $constructor = $throw_error ? 'check' : 'new';
|
||||
my $query = Bugzilla::Search::Saved->$constructor(
|
||||
{ user => $sharer_id, name => $name });
|
||||
|
||||
return $query if (!$query and !$throw_error);
|
||||
|
||||
if (defined $query_type and $query->type != $query_type) {
|
||||
ThrowUserError("missing_query", { queryname => $name,
|
||||
sharer_id => $sharer_id });
|
||||
}
|
||||
|
||||
my @args = ($owner_id, $name);
|
||||
my $extra = '';
|
||||
# If $query_type is defined, then we restrict our search.
|
||||
if (defined $query_type)
|
||||
{
|
||||
$extra = ' AND query_type = ? ';
|
||||
detaint_natural($query_type);
|
||||
push @args, $query_type;
|
||||
}
|
||||
my ($id, $result) = $dbh->selectrow_array(
|
||||
"SELECT id, query FROM namedqueries WHERE userid = ? AND name = ? $extra",
|
||||
undef, @args
|
||||
);
|
||||
$query->url
|
||||
|| ThrowUserError("buglist_parameters_required", { queryname => $name });
|
||||
|
||||
# Some DBs (read: Oracle) incorrectly mark this string as UTF-8
|
||||
# even though it has no UTF-8 characters in it, which prevents
|
||||
# Bugzilla::CGI from later reading it correctly.
|
||||
{
|
||||
use utf8;
|
||||
utf8::downgrade($result) if utf8::is_utf8($result);
|
||||
}
|
||||
|
||||
if (!defined($result))
|
||||
{
|
||||
return 0 unless $throw_error;
|
||||
ThrowUserError("missing_query", {
|
||||
queryname => $name,
|
||||
sharer_id => $sharer_id,
|
||||
});
|
||||
}
|
||||
|
||||
if ($sharer_id && $sharer_id ne $user->id)
|
||||
{
|
||||
my $group = $dbh->selectrow_array(
|
||||
"SELECT group_id FROM namedquery_group_map WHERE namedquery_id = ?",
|
||||
undef, $id
|
||||
);
|
||||
if (!grep { $_->id == $group } @{ $user->groups })
|
||||
{
|
||||
ThrowUserError("missing_query", {
|
||||
queryname => $name,
|
||||
sharer_id => $sharer_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$result || ThrowUserError("buglist_parameters_required", { queryname => $name });
|
||||
|
||||
return wantarray ? ($result, $id) : $result;
|
||||
return wantarray ? ($query->url, $query->id) : $query->url;
|
||||
}
|
||||
|
||||
1;
|
||||
|
|
|
@ -33,70 +33,83 @@ use Bugzilla::Util;
|
|||
use base qw(Exporter);
|
||||
@Bugzilla::Search::Quicksearch::EXPORT = qw(quicksearch);
|
||||
|
||||
# Word renamings
|
||||
# Custom mappings for some fields.
|
||||
use constant MAPPINGS => {
|
||||
# Status, Resolution, Platform, OS, Priority, Severity
|
||||
"status" => "bug_status",
|
||||
"resolution" => "resolution", # no change
|
||||
"platform" => "rep_platform",
|
||||
"os" => "op_sys",
|
||||
"opsys" => "op_sys",
|
||||
"priority" => "priority", # no change
|
||||
"pri" => "priority",
|
||||
"severity" => "bug_severity",
|
||||
"sev" => "bug_severity",
|
||||
# People: AssignedTo, Reporter, QA Contact, CC, Added comment (?)
|
||||
"owner" => "assigned_to", # deprecated since bug 76507
|
||||
"assignee" => "assigned_to",
|
||||
"assignedto" => "assigned_to",
|
||||
"reporter" => "reporter", # no change
|
||||
"rep" => "reporter",
|
||||
"qa" => "qa_contact",
|
||||
"qacontact" => "qa_contact",
|
||||
"cc" => "cc", # no change
|
||||
# Product, Version, Component, Target Milestone
|
||||
"product" => "product", # no change
|
||||
"prod" => "product",
|
||||
"version" => "version", # no change
|
||||
"ver" => "version",
|
||||
"component" => "component", # no change
|
||||
"comp" => "component",
|
||||
"milestone" => "target_milestone",
|
||||
"target" => "target_milestone",
|
||||
"targetmilestone" => "target_milestone",
|
||||
# Summary, Description, URL, Status whiteboard, Keywords
|
||||
"summary" => "short_desc",
|
||||
"shortdesc" => "short_desc",
|
||||
"desc" => "longdesc",
|
||||
"description" => "longdesc",
|
||||
#"comment" => "longdesc", # ???
|
||||
# reserve "comment" for "added comment" email search?
|
||||
"longdesc" => "longdesc",
|
||||
"url" => "bug_file_loc",
|
||||
"whiteboard" => "status_whiteboard",
|
||||
"statuswhiteboard" => "status_whiteboard",
|
||||
"sw" => "status_whiteboard",
|
||||
"keywords" => "keywords", # no change
|
||||
"kw" => "keywords",
|
||||
"group" => "bug_group",
|
||||
"flag" => "flagtypes.name",
|
||||
"requestee" => "requestees.login_name",
|
||||
"req" => "requestees.login_name",
|
||||
"setter" => "setters.login_name",
|
||||
"set" => "setters.login_name",
|
||||
# Attachments
|
||||
"attachment" => "attachments.description",
|
||||
"attachmentdesc" => "attachments.description",
|
||||
"attachdesc" => "attachments.description",
|
||||
"attachmentdata" => "attach_data.thedata",
|
||||
"attachdata" => "attach_data.thedata",
|
||||
"attachmentmimetype" => "attachments.mimetype",
|
||||
"attachmimetype" => "attachments.mimetype"
|
||||
# Status, Resolution, Platform, OS, Priority, Severity
|
||||
"status" => "bug_status",
|
||||
"platform" => "rep_platform",
|
||||
"os" => "op_sys",
|
||||
"severity" => "bug_severity",
|
||||
|
||||
# People: AssignedTo, Reporter, QA Contact, CC, etc.
|
||||
"assignee" => "assigned_to",
|
||||
|
||||
# Product, Version, Component, Target Milestone
|
||||
"milestone" => "target_milestone",
|
||||
|
||||
# Summary, Description, URL, Status whiteboard, Keywords
|
||||
"summary" => "short_desc",
|
||||
"description" => "longdesc",
|
||||
"comment" => "longdesc",
|
||||
"url" => "bug_file_loc",
|
||||
"whiteboard" => "status_whiteboard",
|
||||
"sw" => "status_whiteboard",
|
||||
"kw" => "keywords",
|
||||
"group" => "bug_group",
|
||||
|
||||
# Flags
|
||||
"flag" => "flagtypes.name",
|
||||
"requestee" => "requestees.login_name",
|
||||
"setter" => "setters.login_name",
|
||||
|
||||
# Attachments
|
||||
"attachment" => "attachments.description",
|
||||
"attachmentdesc" => "attachments.description",
|
||||
"attachdesc" => "attachments.description",
|
||||
"attachmentdata" => "attach_data.thedata",
|
||||
"attachdata" => "attach_data.thedata",
|
||||
"attachmentmimetype" => "attachments.mimetype",
|
||||
"attachmimetype" => "attachments.mimetype"
|
||||
};
|
||||
|
||||
sub FIELD_MAP {
|
||||
my $cache = Bugzilla->request_cache;
|
||||
return $cache->{quicksearch_fields} if $cache->{quicksearch_fields};
|
||||
|
||||
# Get all the fields whose names don't contain periods. (Fields that
|
||||
# contain periods are always handled in MAPPINGS.)
|
||||
my @db_fields = grep { $_->name !~ /\./ }
|
||||
Bugzilla->get_fields({ obsolete => 0 });
|
||||
my %full_map = (%{ MAPPINGS() }, map { $_->name => $_->name } @db_fields);
|
||||
|
||||
# Eliminate the fields that start with bug_ or rep_, because those are
|
||||
# handled by the MAPPINGS instead, and we don't want too many names
|
||||
# for them. (Also, otherwise "rep" doesn't match "reporter".)
|
||||
#
|
||||
# Remove "status_whiteboard" because we have "whiteboard" for it in
|
||||
# the mappings, and otherwise "stat" can't match "status".
|
||||
#
|
||||
# Also, don't allow searching the _accessible stuff via quicksearch
|
||||
# (both because it's unnecessary and because otherwise
|
||||
# "reporter_accessible" and "reporter" both match "rep".
|
||||
delete @full_map{qw(rep_platform bug_status bug_file_loc bug_group
|
||||
bug_severity bug_status
|
||||
status_whiteboard
|
||||
cclist_accessible reporter_accessible)};
|
||||
|
||||
$cache->{quicksearch_fields} = \%full_map;
|
||||
|
||||
return $cache->{quicksearch_fields};
|
||||
}
|
||||
|
||||
# Certain fields, when specified like "field:value" get an operator other
|
||||
# than "substring"
|
||||
use constant FIELD_OPERATOR => {
|
||||
content => 'matches',
|
||||
owner_idle_time => 'greaterthan',
|
||||
};
|
||||
|
||||
# We might want to put this into localconfig or somewhere
|
||||
use constant PLATFORMS => ('pc', 'sun', 'macintosh', 'mac');
|
||||
use constant OPSYSTEMS => ('windows', 'win', 'linux');
|
||||
use constant PRODUCT_EXCEPTIONS => (
|
||||
'row', # [Browser]
|
||||
# ^^^
|
||||
|
@ -108,233 +121,82 @@ use constant COMPONENT_EXCEPTIONS => (
|
|||
# ^^^^
|
||||
);
|
||||
|
||||
# Quicksearch-wide globals for boolean charts.
|
||||
our ($chart, $and, $or);
|
||||
|
||||
sub quicksearch {
|
||||
my ($searchstring) = (@_);
|
||||
my $cgi = Bugzilla->cgi;
|
||||
my $urlbase = correct_urlbase();
|
||||
|
||||
$chart = 0;
|
||||
$and = 0;
|
||||
$or = 0;
|
||||
# Don't use fucking globals, use a blessed object
|
||||
my $self = bless {
|
||||
chart => 0,
|
||||
and => 0,
|
||||
or => 0,
|
||||
};
|
||||
|
||||
# Remove leading and trailing commas and whitespace.
|
||||
$searchstring =~ s/(^[\s,]+|[\s,]+$)//g;
|
||||
ThrowUserError('buglist_parameters_required') unless ($searchstring);
|
||||
|
||||
if ($searchstring =~ m/^[0-9,\s]*$/) {
|
||||
# Bug number(s) only.
|
||||
|
||||
# Allow separation by comma or whitespace.
|
||||
$searchstring =~ s/[,\s]+/,/g;
|
||||
|
||||
if (index($searchstring, ',') < $[) {
|
||||
# Single bug number; shortcut to show_bug.cgi.
|
||||
print $cgi->redirect(-uri => "${urlbase}show_bug.cgi?id=$searchstring");
|
||||
exit;
|
||||
}
|
||||
else {
|
||||
# List of bug numbers.
|
||||
$cgi->param('bug_id', $searchstring);
|
||||
$cgi->param('order', 'bugs.bug_id');
|
||||
$cgi->param('bugidtype', 'include');
|
||||
}
|
||||
_bug_numbers_only($searchstring);
|
||||
}
|
||||
else {
|
||||
# It's not just a bug number or a list of bug numbers.
|
||||
# Maybe it's an alias?
|
||||
if ($searchstring =~ /^([^,\s]+)$/) {
|
||||
if (Bugzilla->dbh->selectrow_array(q{SELECT COUNT(*)
|
||||
FROM bugs
|
||||
WHERE alias = ?},
|
||||
undef,
|
||||
$1)) {
|
||||
print $cgi->redirect(-uri => "${urlbase}show_bug.cgi?id=$1");
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
# It's no alias either, so it's a more complex query.
|
||||
my $legal_statuses = get_legal_field_values('bug_status');
|
||||
my $legal_resolutions = get_legal_field_values('resolution');
|
||||
_handle_alias($searchstring);
|
||||
|
||||
# Globally translate " AND ", " OR ", " NOT " to space, pipe, dash.
|
||||
$searchstring =~ s/\s+AND\s+/ /g;
|
||||
$searchstring =~ s/\s+OR\s+/|/g;
|
||||
$searchstring =~ s/\s+NOT\s+/ -/g;
|
||||
|
||||
my @words = splitString($searchstring);
|
||||
my $searchComments =
|
||||
$#words < Bugzilla->params->{'quicksearch_comment_cutoff'};
|
||||
my @openStates = BUG_STATE_OPEN;
|
||||
my @closedStates;
|
||||
my @unknownFields;
|
||||
my (%states, %resolutions);
|
||||
$self->{words} = [ splitString($searchstring) ];
|
||||
$self->{content} = '';
|
||||
$self->{unknown_fields} = [];
|
||||
$self->{ambiguous_fields} = {};
|
||||
|
||||
foreach (@$legal_statuses) {
|
||||
push @closedStates, $_ unless is_open_state($_);
|
||||
}
|
||||
|
||||
if ($words[0] eq 'OPEN')
|
||||
{
|
||||
shift @words;
|
||||
%states = map { $_ => 1 } @openStates;
|
||||
}
|
||||
elsif ($words[0] =~ /^[A-Z]+(,[A-Z]+)*$/)
|
||||
{
|
||||
# e.g. NEW,ASSI,REOP,FIX
|
||||
my (%st, %res);
|
||||
if (matchPrefixes(\%st, \%res, [split(/,/, $words[0])],
|
||||
$legal_statuses, $legal_resolutions))
|
||||
{
|
||||
shift @words;
|
||||
%states = %st;
|
||||
%resolutions = %res;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
# Default: search for ALL BUGS! (Vitaliy Filippov <vfilippov@custis.ru> 2009-01-30)
|
||||
%states = map { $_ => 1 } @$legal_statuses;
|
||||
}
|
||||
|
||||
my $content = '';
|
||||
$self->_handle_status_and_resolution;
|
||||
|
||||
# Loop over all main-level QuickSearch words.
|
||||
foreach my $qsword (@words) {
|
||||
foreach my $qsword (@{$self->{words}}) {
|
||||
my $negate = substr($qsword, 0, 1) eq '-';
|
||||
if ($negate) {
|
||||
$qsword = substr($qsword, 1);
|
||||
}
|
||||
|
||||
my $firstChar = substr($qsword, 0, 1);
|
||||
my $baseWord = substr($qsword, 1);
|
||||
my @subWords = split(/[\|,]/, $baseWord);
|
||||
if ($firstChar eq '+' || $firstChar eq '#') {
|
||||
$content .= ' +' . join ' +', @subWords if @subWords;
|
||||
}
|
||||
elsif ($firstChar eq ':') {
|
||||
foreach (@subWords) {
|
||||
addChart('product', 'substring', $_, $negate);
|
||||
addChart('component', 'substring', $_, $negate);
|
||||
}
|
||||
}
|
||||
elsif ($firstChar eq '@') {
|
||||
foreach (@subWords) {
|
||||
addChart('assigned_to', 'substring', $_, $negate);
|
||||
}
|
||||
}
|
||||
elsif ($firstChar eq '[') {
|
||||
$content .= ' ' . $baseWord;
|
||||
addChart('status_whiteboard', 'substring', $baseWord, $negate);
|
||||
}
|
||||
elsif ($firstChar eq '!') {
|
||||
addChart('keywords', 'anywords', $baseWord, $negate);
|
||||
|
||||
}
|
||||
else { # No special first char
|
||||
|
||||
# No special first char
|
||||
if (!$self->_handle_special_first_chars($qsword, $negate)) {
|
||||
# Split by '|' to get all operands for a boolean OR.
|
||||
foreach my $or_operand (split(/\|/, $qsword)) {
|
||||
if ($or_operand =~ /^votes:([0-9]+)$/) {
|
||||
# votes:xx ("at least xx votes")
|
||||
addChart('votes', 'greaterthan', $1 - 1, $negate);
|
||||
}
|
||||
elsif ($or_operand =~ /^(?:flag:)?([^\?]+\?)([^\?]*)$/) {
|
||||
# Flag and requestee shortcut
|
||||
addChart('flagtypes.name', 'substring', $1, $negate);
|
||||
$and++; $or = 0; # Next chart for boolean AND
|
||||
addChart('requestees.login_name', 'substring', $2, $negate);
|
||||
}
|
||||
elsif ($or_operand =~ /^([^:]+):([^:]+)$/) {
|
||||
# generic field1,field2,field3:value1,value2 notation
|
||||
my @fields = split(/,/, $1);
|
||||
my @values = split(/,/, $2);
|
||||
foreach my $field (@fields)
|
||||
{
|
||||
if ($field eq 'status')
|
||||
{
|
||||
my (%st, %res);
|
||||
if (matchPrefixes(\%st, \%res, \@values, $legal_statuses, $legal_resolutions))
|
||||
{
|
||||
%states = %st;
|
||||
%resolutions = %res;
|
||||
}
|
||||
last;
|
||||
}
|
||||
# Skip and record any unknown fields
|
||||
if (!defined(MAPPINGS->{$field})) {
|
||||
push(@unknownFields, $field);
|
||||
next;
|
||||
}
|
||||
$field = MAPPINGS->{$field};
|
||||
foreach (@values) {
|
||||
addChart($field, 'substring', $_, $negate);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
|
||||
if (!$self->_handle_field_names($or_operand, $negate))
|
||||
{
|
||||
# Having ruled out the special cases, we may now split
|
||||
# by comma, which is another legal boolean OR indicator.
|
||||
foreach my $word (split(/,/, $or_operand)) {
|
||||
# Platform and operating system
|
||||
if (grep({lc($word) eq $_} PLATFORMS)
|
||||
|| grep({lc($word) eq $_} OPSYSTEMS)) {
|
||||
addChart('rep_platform', 'substring',
|
||||
$word, $negate);
|
||||
addChart('op_sys', 'substring',
|
||||
$word, $negate);
|
||||
if (!$self->_special_field_syntax($word, $negate)) {
|
||||
$self->_default_quicksearch_word($word, $negate);
|
||||
}
|
||||
# Priority
|
||||
elsif ($word =~ m/^[pP]([1-5](-[1-5])?)$/) {
|
||||
addChart('priority', 'regexp',
|
||||
"[$1]", $negate);
|
||||
}
|
||||
# Severity
|
||||
elsif (grep({lc($word) eq substr($_, 0, 3)}
|
||||
@{get_legal_field_values('bug_severity')})) {
|
||||
addChart('bug_severity', 'substring',
|
||||
$word, $negate);
|
||||
}
|
||||
# Votes (votes>xx)
|
||||
elsif ($word =~ m/^votes>([0-9]+)$/) {
|
||||
addChart('votes', 'greaterthan',
|
||||
$1, $negate);
|
||||
}
|
||||
# Votes (votes>=xx, votes=>xx)
|
||||
elsif ($word =~ m/^votes(>=|=>)([0-9]+)$/) {
|
||||
addChart('votes', 'greaterthan',
|
||||
$2-1, $negate);
|
||||
|
||||
}
|
||||
else { # Default QuickSearch word
|
||||
$content .= ' '.$word;
|
||||
}
|
||||
} # foreach my $word (split(/,/, $qsword))
|
||||
} # votes and generic field detection
|
||||
} # foreach (split(/\|/, $_))
|
||||
} # "switch" $firstChar
|
||||
$and++;
|
||||
$or = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$self->{and}++;
|
||||
$self->{or} = 0;
|
||||
} # foreach (@words)
|
||||
$cgi->param('content', $content);
|
||||
$cgi->param('content', $self->{content});
|
||||
|
||||
# If we have wanted resolutions, allow closed states
|
||||
if (keys %resolutions) {
|
||||
foreach (@closedStates) { $states{$_} = 1 }
|
||||
if (keys %{$self->{resolutions}}) {
|
||||
foreach (@{get_legal_field_values('bug_status')}) {
|
||||
$self->{states}->{$_} = 1 unless is_open_state($_);
|
||||
}
|
||||
}
|
||||
|
||||
$cgi->param('bug_status', keys(%states));
|
||||
$cgi->param('resolution', keys(%resolutions));
|
||||
$cgi->param('bug_status', keys %{$self->{states}});
|
||||
$cgi->param('resolution', keys %{$self->{resolutions}});
|
||||
|
||||
# Inform user about any unknown fields
|
||||
if (scalar(@unknownFields)) {
|
||||
if (@{$self->{unknown_fields}} || %{$self->{ambiguous_fields}}) {
|
||||
ThrowUserError("quicksearch_unknown_field",
|
||||
{ fields => \@unknownFields });
|
||||
{ unknown => $self->{unknown_fields},
|
||||
ambiguous => $self->{ambiguous_fields} });
|
||||
}
|
||||
|
||||
# Make sure we have some query terms left
|
||||
|
@ -346,6 +208,7 @@ sub quicksearch {
|
|||
my $modified_query_string = $cgi->canonicalise_query(@params_to_strip);
|
||||
|
||||
if ($cgi->param('load')) {
|
||||
my $urlbase = correct_urlbase();
|
||||
# Param 'load' asks us to display the query in the advanced search form.
|
||||
print $cgi->redirect(-uri => "${urlbase}query.cgi?format=advanced&"
|
||||
. $modified_query_string);
|
||||
|
@ -358,6 +221,271 @@ sub quicksearch {
|
|||
return $modified_query_string;
|
||||
}
|
||||
|
||||
##########################
|
||||
# Parts of quicksearch() #
|
||||
##########################
|
||||
|
||||
sub _bug_numbers_only {
|
||||
my $searchstring = shift;
|
||||
my $cgi = Bugzilla->cgi;
|
||||
# Allow separation by comma or whitespace.
|
||||
$searchstring =~ s/[,\s]+/,/g;
|
||||
|
||||
if ($searchstring !~ /,/) {
|
||||
# Single bug number; shortcut to show_bug.cgi.
|
||||
print $cgi->redirect(
|
||||
-uri => correct_urlbase() . "show_bug.cgi?id=$searchstring");
|
||||
exit;
|
||||
}
|
||||
else {
|
||||
# List of bug numbers.
|
||||
$cgi->param('bug_id', $searchstring);
|
||||
$cgi->param('order', 'bugs.bug_id');
|
||||
$cgi->param('bug_id_type', 'anyexact');
|
||||
}
|
||||
}
|
||||
|
||||
sub _handle_alias {
|
||||
my $searchstring = shift;
|
||||
if ($searchstring =~ /^([^,\s]+)$/) {
|
||||
my $alias = $1;
|
||||
# We use this direct SQL because we want quicksearch to be VERY fast.
|
||||
my $is_alias = Bugzilla->dbh->selectrow_array(
|
||||
q{SELECT 1 FROM bugs WHERE alias = ?}, undef, $alias);
|
||||
if ($is_alias) {
|
||||
print Bugzilla->cgi->redirect(
|
||||
-uri => correct_urlbase() . "show_bug.cgi?id=$alias");
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sub _handle_status_and_resolution
|
||||
{
|
||||
my $self = shift;
|
||||
$self->{legal_statuses} = get_legal_field_values('bug_status');
|
||||
$self->{legal_resolutions} = get_legal_field_values('resolution');
|
||||
|
||||
my @openStates = BUG_STATE_OPEN;
|
||||
my @closedStates;
|
||||
my (%states, %resolutions);
|
||||
|
||||
foreach (get_legal_field_values('bug_status')) {
|
||||
push(@closedStates, $_) unless is_open_state($_);
|
||||
}
|
||||
|
||||
if ($self->{words}->[0] eq 'OPEN')
|
||||
{
|
||||
shift @{$self->{words}};
|
||||
%states = map { $_ => 1 } @openStates;
|
||||
}
|
||||
elsif ($self->{words}->[0] =~ /^[A-Z]+(,[A-Z]+)*$/)
|
||||
{
|
||||
my (%st, %res);
|
||||
if (matchPrefixes(\%st, \%res, [split(/,/, $self->{words}->[0])],
|
||||
$self->{legal_statuses}, $self->{legal_resolutions}))
|
||||
{
|
||||
shift @{$self->{words}};
|
||||
%states = %st;
|
||||
%resolutions = %res;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
# Default: search for ALL BUGS! (Vitaliy Filippov <vfilippov@custis.ru> 2009-01-30)
|
||||
%states = map { $_ => 1 } @$self->{legal_statuses};
|
||||
}
|
||||
|
||||
$self->{states} = \%states;
|
||||
$self->{resolutions} = \%resolutions;
|
||||
}
|
||||
|
||||
sub _handle_special_first_chars {
|
||||
my $self = shift;
|
||||
my ($qsword, $negate) = @_;
|
||||
|
||||
my $firstChar = substr($qsword, 0, 1);
|
||||
my $baseWord = substr($qsword, 1);
|
||||
my @subWords = split(/[\|,]/, $baseWord);
|
||||
|
||||
if ($firstChar eq '+' || $firstChar eq '#') {
|
||||
$self->{content} .= ' +' . join ' +', @subWords if @subWords;
|
||||
return 1;
|
||||
}
|
||||
if ($firstChar eq ':') {
|
||||
foreach (@subWords) {
|
||||
$self->addChart('product', 'substring', $_, $negate);
|
||||
$self->addChart('component', 'substring', $_, $negate);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
if ($firstChar eq '@') {
|
||||
$self->addChart('assigned_to', 'substring', $_, $negate) foreach (@subWords);
|
||||
return 1;
|
||||
}
|
||||
if ($firstChar eq '[') {
|
||||
$self->{content} .= ' ' . $baseWord;
|
||||
$self->addChart('status_whiteboard', 'substring', $baseWord, $negate);
|
||||
return 1;
|
||||
}
|
||||
if ($firstChar eq '!') {
|
||||
$self->addChart('keywords', 'anywords', $baseWord, $negate);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
sub _handle_field_names {
|
||||
my $self = shift;
|
||||
my ($or_operand, $negate) = @_;
|
||||
|
||||
# votes:xx ("at least xx votes")
|
||||
if ($or_operand =~ /^votes:([0-9]+)$/) {
|
||||
$self->addChart('votes', 'greaterthan', $1 - 1, $negate);
|
||||
return 1;
|
||||
}
|
||||
|
||||
# Flag and requestee shortcut
|
||||
if ($or_operand =~ /^(?:flag:)?([^\?]+\?)([^\?]*)$/) {
|
||||
$self->addChart('flagtypes.name', 'substring', $1, $negate);
|
||||
$self->{and}++; $self->{or} = 0; # Next boolean AND
|
||||
$self->addChart('requestees.login_name', 'substring', $2, $negate);
|
||||
return 1;
|
||||
}
|
||||
|
||||
# generic field1,field2,field3:value1,value2 notation
|
||||
if ($or_operand =~ /^([^:]+):([^:]+)$/) {
|
||||
my @fields = split(/,/, $1);
|
||||
my @values = split(/,/, $2);
|
||||
foreach my $field (@fields) {
|
||||
if ($field eq 'status')
|
||||
{
|
||||
my (%st, %res);
|
||||
if (matchPrefixes(\%st, \%res, \@values, $self->{legal_statuses}, $self->{legal_resolutions}))
|
||||
{
|
||||
$self->{states} = \%st;
|
||||
$self->{resolutions} = \%res;
|
||||
}
|
||||
last;
|
||||
}
|
||||
my $translated = _translate_field_name($field);
|
||||
# Skip and record any unknown fields
|
||||
if (!defined $translated) {
|
||||
push @{$self->{unknown_fields}}, $field;
|
||||
next;
|
||||
}
|
||||
# If we got back an array, that means the substring is
|
||||
# ambiguous and could match more than field name
|
||||
elsif (ref $translated) {
|
||||
$self->{ambiguous_fields}->{$field} = $translated;
|
||||
next;
|
||||
}
|
||||
foreach my $value (@values) {
|
||||
my $operator = FIELD_OPERATOR->{$translated} || 'substring';
|
||||
$self->addChart($translated, $operator, $value, $negate);
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
sub _translate_field_name {
|
||||
my $field = shift;
|
||||
$field = lc($field);
|
||||
my $field_map = FIELD_MAP;
|
||||
|
||||
# If the field exactly matches a mapping, just return right now.
|
||||
return $field_map->{$field} if exists $field_map->{$field};
|
||||
|
||||
# Check if we match, as a starting substring, exactly one field.
|
||||
my @field_names = keys %$field_map;
|
||||
my @matches = grep { $_ =~ /^\Q$field\E/ } @field_names;
|
||||
# Eliminate duplicates that are actually the same field
|
||||
# (otherwise "assi" matches both "assignee" and "assigned_to", and
|
||||
# the lines below fail when they shouldn't.)
|
||||
my %match_unique = map { $field_map->{$_} => $_ } @matches;
|
||||
@matches = values %match_unique;
|
||||
|
||||
if (scalar(@matches) == 1) {
|
||||
return $field_map->{$matches[0]};
|
||||
}
|
||||
elsif (scalar(@matches) > 1) {
|
||||
return \@matches;
|
||||
}
|
||||
|
||||
# Check if we match exactly one custom field, ignoring the cf_ on the
|
||||
# custom fields (to allow people to type things like "build" for
|
||||
# "cf_build").
|
||||
my %cfless;
|
||||
foreach my $name (@field_names) {
|
||||
my $no_cf = $name;
|
||||
if ($no_cf =~ s/^cf_//) {
|
||||
if ($field eq $no_cf) {
|
||||
return $field_map->{$name};
|
||||
}
|
||||
$cfless{$no_cf} = $name;
|
||||
}
|
||||
}
|
||||
|
||||
# See if we match exactly one substring of any of the cf_-less fields.
|
||||
my @cfless_matches = grep { $_ =~ /^\Q$field\E/ } (keys %cfless);
|
||||
|
||||
if (scalar(@cfless_matches) == 1) {
|
||||
my $match = $cfless_matches[0];
|
||||
my $actual_field = $cfless{$match};
|
||||
return $field_map->{$actual_field};
|
||||
}
|
||||
elsif (scalar(@matches) > 1) {
|
||||
return \@matches;
|
||||
}
|
||||
|
||||
return undef;
|
||||
}
|
||||
|
||||
sub _special_field_syntax {
|
||||
my $self = shift;
|
||||
my ($word, $negate) = @_;
|
||||
|
||||
# P1-5 Syntax
|
||||
if ($word =~ m/^P(\d+)(?:-(\d+))?$/i) {
|
||||
my $start = $1 - 1;
|
||||
$start = 0 if $start < 0;
|
||||
my $end = $2 - 1;
|
||||
|
||||
my $legal_priorities = get_legal_field_values('priority');
|
||||
$end = scalar(@$legal_priorities) - 1
|
||||
if $end > (scalar @$legal_priorities - 1);
|
||||
my $prios = $legal_priorities->[$start];
|
||||
if ($end) {
|
||||
$prios = join(',', @$legal_priorities[$start..$end])
|
||||
}
|
||||
$self->addChart('priority', 'anyexact', $prios, $negate);
|
||||
return 1;
|
||||
}
|
||||
|
||||
# Votes (votes>xx)
|
||||
if ($word =~ m/^votes>([0-9]+)$/) {
|
||||
$self->addChart('votes', 'greaterthan', $1, $negate);
|
||||
return 1;
|
||||
}
|
||||
|
||||
# Votes (votes>=xx, votes=>xx)
|
||||
if ($word =~ m/^votes(>=|=>)([0-9]+)$/) {
|
||||
$self->addChart('votes', 'greaterthan', $2-1, $negate);
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
sub _default_quicksearch_word {
|
||||
my $self = shift;
|
||||
my ($word, $negate) = @_;
|
||||
$self->{content} .= ' '.$word;
|
||||
}
|
||||
|
||||
###########################################################################
|
||||
# Helpers
|
||||
###########################################################################
|
||||
|
@ -366,6 +494,8 @@ sub quicksearch {
|
|||
sub splitString
|
||||
{
|
||||
my $string = shift;
|
||||
my @quoteparts;
|
||||
my @parts;
|
||||
|
||||
my @quoteparts = split /\"/, $string, -1;
|
||||
my @parts;
|
||||
|
@ -420,33 +550,25 @@ sub matchPrefixes {
|
|||
sub negateComparisonType {
|
||||
my $comparisonType = shift;
|
||||
|
||||
if ($comparisonType eq 'substring') {
|
||||
return 'notsubstring';
|
||||
}
|
||||
elsif ($comparisonType eq 'anywords') {
|
||||
if ($comparisonType eq 'anywords') {
|
||||
return 'nowords';
|
||||
}
|
||||
elsif ($comparisonType eq 'regexp') {
|
||||
return 'notregexp';
|
||||
}
|
||||
else {
|
||||
# Don't know how to negate that
|
||||
ThrowCodeError('unknown_comparison_type');
|
||||
}
|
||||
return "not$comparisonType";
|
||||
}
|
||||
|
||||
# Add a boolean chart
|
||||
sub addChart {
|
||||
my $self = shift;
|
||||
my ($field, $comparisonType, $value, $negate) = @_;
|
||||
|
||||
$negate && ($comparisonType = negateComparisonType($comparisonType));
|
||||
makeChart("$chart-$and-$or", $field, $comparisonType, $value);
|
||||
makeChart("$self->{chart}-$self->{and}-$self->{or}", $field, $comparisonType, $value);
|
||||
if ($negate) {
|
||||
$and++;
|
||||
$or = 0;
|
||||
$self->{and}++;
|
||||
$self->{or} = 0;
|
||||
}
|
||||
else {
|
||||
$or++;
|
||||
$self->{or}++;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -33,6 +33,8 @@ use Bugzilla::Search qw(IsValidQueryType);
|
|||
use Bugzilla::User;
|
||||
use Bugzilla::Util;
|
||||
|
||||
use Scalar::Util qw(blessed);
|
||||
|
||||
#############
|
||||
# Constants #
|
||||
#############
|
||||
|
@ -58,6 +60,63 @@ use constant VALIDATORS => {
|
|||
|
||||
use constant UPDATE_COLUMNS => qw(name query query_type);
|
||||
|
||||
###############
|
||||
# Constructor #
|
||||
###############
|
||||
|
||||
sub new {
|
||||
my $class = shift;
|
||||
my $param = shift;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
my $user;
|
||||
if (ref $param) {
|
||||
$user = $param->{user} || Bugzilla->user;
|
||||
my $name = $param->{name};
|
||||
if (!defined $name) {
|
||||
ThrowCodeError('bad_arg',
|
||||
{argument => 'name',
|
||||
function => "${class}::new"});
|
||||
}
|
||||
my $condition = 'userid = ? AND name = ?';
|
||||
my $user_id = blessed $user ? $user->id : $user;
|
||||
detaint_natural($user_id)
|
||||
|| ThrowCodeError('param_must_be_numeric',
|
||||
{function => $class . '::_init', param => 'user'});
|
||||
my @values = ($user_id, $name);
|
||||
$param = { condition => $condition, values => \@values };
|
||||
}
|
||||
|
||||
unshift @_, $param;
|
||||
my $self = $class->SUPER::new(@_);
|
||||
if ($self) {
|
||||
$self->{user} = $user if blessed $user;
|
||||
|
||||
# Some DBs (read: Oracle) incorrectly mark the query string as UTF-8
|
||||
# when it's coming out of the database, even though it has no UTF-8
|
||||
# characters in it, which prevents Bugzilla::CGI from later reading
|
||||
# it correctly.
|
||||
utf8::downgrade($self->{query}) if utf8::is_utf8($self->{query});
|
||||
}
|
||||
return $self;
|
||||
}
|
||||
|
||||
sub check {
|
||||
my $class = shift;
|
||||
my $search = $class->SUPER::check(@_);
|
||||
my $user = Bugzilla->user;
|
||||
return $search if $search->user->id == $user->id;
|
||||
|
||||
if (!$search->shared_with_group
|
||||
or !$user->in_group($search->shared_with_group))
|
||||
{
|
||||
ThrowUserError('missing_query', { queryname => $search->name,
|
||||
sharer_id => $search->user->id });
|
||||
}
|
||||
|
||||
return $search;
|
||||
}
|
||||
|
||||
##############
|
||||
# Validators #
|
||||
##############
|
||||
|
@ -224,8 +283,8 @@ sub shared_with_users {
|
|||
# Simple Accessors #
|
||||
####################
|
||||
|
||||
sub bug_ids_only { return ($_[0]->{'query_type'} == LIST_OF_BUGS) ? 1 : 0; }
|
||||
sub url { return $_[0]->{'query'}; }
|
||||
sub type { return $_[0]->{'query_type'}; }
|
||||
sub url { return $_[0]->{'query'}; }
|
||||
|
||||
sub user {
|
||||
my ($self) = @_;
|
||||
|
@ -278,7 +337,8 @@ documented below.
|
|||
|
||||
=item C<new>
|
||||
|
||||
Does not accept a bare C<name> argument. Instead, accepts only an id.
|
||||
Takes either an id, or the named parameters C<user> and C<name>.
|
||||
C<user> can be either a L<Bugzilla::User> object or a numeric user id.
|
||||
|
||||
See also: L<Bugzilla::Object/new>.
|
||||
|
||||
|
@ -311,9 +371,9 @@ Whether or not this search should be displayed in the footer for the
|
|||
I<current user> (not the owner of the search, but the person actually
|
||||
using Bugzilla right now).
|
||||
|
||||
=item C<bug_ids_only>
|
||||
=item C<type>
|
||||
|
||||
True if the search contains only a list of Bug IDs.
|
||||
The numeric id of the type of search this is (from L<Bugzilla::Constants>).
|
||||
|
||||
=item C<shared_with_group>
|
||||
|
||||
|
|
|
@ -69,6 +69,7 @@ sub new {
|
|||
# We've been given a load of parameters to create a new Series from.
|
||||
# Currently, undef is always passed as the first parameter; this allows
|
||||
# you to call writeToDatabase() unconditionally.
|
||||
# XXX - You cannot set category_id and subcategory_id from here.
|
||||
$self->initFromParameters(@_);
|
||||
}
|
||||
else {
|
||||
|
@ -90,7 +91,7 @@ sub initFromDatabase {
|
|||
|
||||
my @series = $dbh->selectrow_array("SELECT series.series_id, cc1.name, " .
|
||||
"cc2.name, series.name, series.creator, series.frequency, " .
|
||||
"series.query, series.is_public " .
|
||||
"series.query, series.is_public, series.category, series.subcategory " .
|
||||
"FROM series " .
|
||||
"INNER JOIN series_categories AS cc1 " .
|
||||
" ON series.category = cc1.id " .
|
||||
|
@ -117,8 +118,9 @@ sub initFromParameters {
|
|||
my $self = shift;
|
||||
|
||||
($self->{'series_id'}, $self->{'category'}, $self->{'subcategory'},
|
||||
$self->{'name'}, $self->{'creator'}, $self->{'frequency'},
|
||||
$self->{'query'}, $self->{'public'}) = @_;
|
||||
$self->{'name'}, $self->{'creator_id'}, $self->{'frequency'},
|
||||
$self->{'query'}, $self->{'public'}, $self->{'category_id'},
|
||||
$self->{'subcategory_id'}) = @_;
|
||||
|
||||
# If the first parameter is undefined, check if this series already
|
||||
# exists and update it series_id accordingly
|
||||
|
@ -147,7 +149,7 @@ sub initFromCGI {
|
|||
$self->{'name'} = $cgi->param('name')
|
||||
|| ThrowUserError("missing_name");
|
||||
|
||||
$self->{'creator'} = Bugzilla->user->id;
|
||||
$self->{'creator_id'} = Bugzilla->user->id;
|
||||
|
||||
$self->{'frequency'} = $cgi->param('frequency');
|
||||
detaint_natural($self->{'frequency'})
|
||||
|
@ -198,7 +200,7 @@ sub writeToDatabase {
|
|||
$dbh->do("INSERT INTO series (creator, category, subcategory, " .
|
||||
"name, frequency, query, is_public) VALUES " .
|
||||
"(?, ?, ?, ?, ?, ?, ?)", undef,
|
||||
$self->{'creator'}, $category_id, $subcategory_id, $self->{'name'},
|
||||
$self->{'creator_id'}, $category_id, $subcategory_id, $self->{'name'},
|
||||
$self->{'frequency'}, $self->{'query'}, $self->{'public'});
|
||||
|
||||
# Retrieve series_id
|
||||
|
@ -253,4 +255,27 @@ sub getCategoryID {
|
|||
return $category_id;
|
||||
}
|
||||
|
||||
##########
|
||||
# Methods
|
||||
##########
|
||||
sub id { return $_[0]->{'series_id'}; }
|
||||
sub name { return $_[0]->{'name'}; }
|
||||
|
||||
sub creator {
|
||||
my $self = shift;
|
||||
|
||||
if (!$self->{creator} && $self->{creator_id}) {
|
||||
require Bugzilla::User;
|
||||
$self->{creator} = new Bugzilla::User($self->{creator_id});
|
||||
}
|
||||
return $self->{creator};
|
||||
}
|
||||
|
||||
sub remove_from_db {
|
||||
my $self = shift;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
$dbh->do('DELETE FROM series WHERE series_id = ?', undef, $self->id);
|
||||
}
|
||||
|
||||
1;
|
||||
|
|
|
@ -66,6 +66,7 @@ sub VALIDATORS {
|
|||
sub create {
|
||||
my $class = shift;
|
||||
my $self = $class->SUPER::create(@_);
|
||||
delete Bugzilla->request_cache->{status_bug_state_open};
|
||||
add_missing_bug_status_transitions();
|
||||
return $self;
|
||||
}
|
||||
|
@ -80,6 +81,7 @@ sub remove_from_db {
|
|||
WHERE old_status = ? OR new_status = ?',
|
||||
undef, $id, $id);
|
||||
$dbh->bz_commit_transaction();
|
||||
delete Bugzilla->request_cache->{status_bug_state_open};
|
||||
}
|
||||
|
||||
###############################
|
||||
|
@ -120,9 +122,12 @@ sub _check_value {
|
|||
###############################
|
||||
|
||||
sub BUG_STATE_OPEN {
|
||||
# XXX - We should cache this list.
|
||||
my $dbh = Bugzilla->dbh;
|
||||
return @{$dbh->selectcol_arrayref('SELECT value FROM bug_status WHERE is_open = 1')};
|
||||
my $cache = Bugzilla->request_cache;
|
||||
$cache->{status_bug_state_open} ||=
|
||||
$dbh->selectcol_arrayref('SELECT value FROM bug_status
|
||||
WHERE is_open = 1');
|
||||
return @{ $cache->{status_bug_state_open} };
|
||||
}
|
||||
|
||||
# Tells you whether or not the argument is a valid "open" state.
|
||||
|
@ -171,28 +176,6 @@ sub can_change_to {
|
|||
return $self->{'can_change_to'};
|
||||
}
|
||||
|
||||
sub can_change_from {
|
||||
my $self = shift;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
if (!defined $self->{'can_change_from'}) {
|
||||
my $old_status_ids = $dbh->selectcol_arrayref('SELECT old_status
|
||||
FROM status_workflow
|
||||
INNER JOIN bug_status
|
||||
ON id = old_status
|
||||
WHERE isactive = 1
|
||||
AND new_status = ?
|
||||
AND old_status IS NOT NULL',
|
||||
undef, $self->id);
|
||||
|
||||
# Allow the bug status to remain unchanged.
|
||||
push(@$old_status_ids, $self->id);
|
||||
$self->{'can_change_from'} = Bugzilla::Status->new_from_list($old_status_ids);
|
||||
}
|
||||
|
||||
return $self->{'can_change_from'};
|
||||
}
|
||||
|
||||
sub comment_required_on_change_from {
|
||||
my ($self, $old_status) = @_;
|
||||
my ($cond, $values) = $self->_status_condition($old_status);
|
||||
|
@ -295,17 +278,6 @@ below.
|
|||
|
||||
Returns: A list of Bugzilla::Status objects.
|
||||
|
||||
=item C<can_change_from>
|
||||
|
||||
Description: Returns the list of active statuses a bug can be changed from
|
||||
given the new bug status. If the bug status is available on
|
||||
bug creation, this method doesn't return this information.
|
||||
You have to call C<can_change_to> instead.
|
||||
|
||||
Params: none.
|
||||
|
||||
Returns: A list of Bugzilla::Status objects.
|
||||
|
||||
=item C<comment_required_on_change_from>
|
||||
|
||||
=over
|
||||
|
|
|
@ -35,27 +35,31 @@ package Bugzilla::Template;
|
|||
use utf8;
|
||||
use strict;
|
||||
|
||||
use Bugzilla::Bug;
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::Hook;
|
||||
use Bugzilla::Install::Requirements;
|
||||
use Bugzilla::Install::Util qw(install_string template_include_path include_languages);
|
||||
use Bugzilla::Install::Util qw(install_string template_include_path
|
||||
include_languages);
|
||||
use Bugzilla::Keyword;
|
||||
use Bugzilla::Util;
|
||||
use Bugzilla::User;
|
||||
use Bugzilla::Error;
|
||||
use Bugzilla::Status;
|
||||
use Bugzilla::Token;
|
||||
use Bugzilla::Hook;
|
||||
|
||||
use Cwd qw(abs_path);
|
||||
use MIME::Base64;
|
||||
use MIME::QuotedPrint qw(encode_qp);
|
||||
use Encode qw(encode);
|
||||
use Date::Format ();
|
||||
use File::Basename qw(dirname);
|
||||
use File::Basename qw(basename dirname);
|
||||
use File::Find;
|
||||
use File::Path qw(rmtree mkpath);
|
||||
use File::Spec;
|
||||
use IO::Dir;
|
||||
use JSON;
|
||||
use Scalar::Util qw(blessed);
|
||||
|
||||
use base qw(Template);
|
||||
|
||||
|
@ -166,7 +170,7 @@ sub nl2br
|
|||
# If you want to modify this routine, read the comments carefully
|
||||
|
||||
sub quoteUrls {
|
||||
my ($text, $curr_bugid) = (@_);
|
||||
my ($text, $bug, $comment) = (@_);
|
||||
return $text unless $text;
|
||||
|
||||
# We use /g for speed, but uris can have other things inside them
|
||||
|
@ -194,6 +198,26 @@ sub quoteUrls {
|
|||
my $count = 0;
|
||||
my $tmp;
|
||||
|
||||
my @hook_regexes;
|
||||
Bugzilla::Hook::process('bug_format_comment',
|
||||
{ text => \$text, bug => $bug, regexes => \@hook_regexes,
|
||||
comment => $comment });
|
||||
|
||||
foreach my $re (@hook_regexes) {
|
||||
my ($match, $replace) = @$re{qw(match replace)};
|
||||
if (ref($replace) eq 'CODE') {
|
||||
$text =~ s/$match/($things[$count++] = $replace->({matches => [
|
||||
$1, $2, $3, $4,
|
||||
$5, $6, $7, $8,
|
||||
$9, $10]}))
|
||||
&& ("\0\0" . ($count-1) . "\0\0")/egx;
|
||||
}
|
||||
else {
|
||||
$text =~ s/$match/($things[$count++] = $replace)
|
||||
&& ("\0\0" . ($count-1) . "\0\0")/egx;
|
||||
}
|
||||
}
|
||||
|
||||
# Provide tooltips for full bug links (Bug 74355)
|
||||
my $urlbase_re = '(' . join('|',
|
||||
map { qr/$_/ } grep($_, Bugzilla->params->{'urlbase'},
|
||||
|
@ -204,11 +228,11 @@ sub quoteUrls {
|
|||
~egox;
|
||||
|
||||
# non-mailto protocols
|
||||
my $safe_protocols = join '|', SAFE_PROTOCOLS;
|
||||
my $safe_protocols = join('|', SAFE_PROTOCOLS);
|
||||
|
||||
$text =~ s~\b((?:$safe_protocols): # The protocol:
|
||||
[^\s<>\"]+ # Any non-whitespace
|
||||
[\w\/]) # so that we end in \w or /
|
||||
[^\s<>\"]+ # Any non-whitespace
|
||||
[\w\/]) # so that we end in \w or /
|
||||
~($tmp = html_quote($1)) &&
|
||||
($things[$count++] = "<a href=\"$tmp\">$tmp</a>") &&
|
||||
("\0\0" . ($count-1) . "\0\0")
|
||||
|
@ -243,28 +267,23 @@ sub quoteUrls {
|
|||
$text =~ s~</span >\n<span class="quote">~\n~g;
|
||||
|
||||
# mailto:
|
||||
$text =~ s~\b(mailto:)?([\w\.\-\+\=]+\@[\w\-]+(?:\.[\w\-]+)+)\b
|
||||
~<a href=\"mailto:$2\">$1$2</a>~igx;
|
||||
|
||||
# attachment links - handle both cases separately for simplicity
|
||||
$text =~ s~((?:^Created\ an\ |\b)attachment\s*\(id=(\d+)\)(\s*\[details\])?)
|
||||
~($things[$count++] = get_attachment_link($2, $1)) &&
|
||||
("\0\0" . ($count-1) . "\0\0")
|
||||
~egmx;
|
||||
$text =~ s~\b((mailto:)?)([\w\.\-\+\=]+\@[\w\-]+(?:\.[\w\-]+)+)\b
|
||||
~<a href=\"mailto:$3\">$1$3</a>~igx;
|
||||
|
||||
# attachment links
|
||||
$text =~ s~\b(attachment\s*\#?\s*(\d+))
|
||||
~($things[$count++] = get_attachment_link($2, $1)) &&
|
||||
("\0\0" . ($count-1) . "\0\0")
|
||||
~egsxi;
|
||||
|
||||
# Current bug ID this comment belongs to
|
||||
my $current_bugurl = $curr_bugid ? "show_bug.cgi?id=$curr_bugid" : "";
|
||||
my $current_bugurl = $bug ? ("show_bug.cgi?id=" . $bug->id) : "";
|
||||
|
||||
# This handles bug a, comment b type stuff. Because we're using /g
|
||||
# we have to do this in one pattern, and so this is semi-messy.
|
||||
# Also, we can't use $bug_re?$comment_re? because that will match the
|
||||
# empty string
|
||||
my $bug_word = get_term('bug');
|
||||
my $bug_word = template_var('terms')->{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)
|
||||
|
@ -295,8 +314,8 @@ sub get_attachment_link {
|
|||
detaint_natural($attachid)
|
||||
|| die "get_attachment_link() called with non-integer attachment number";
|
||||
|
||||
my ($bugid, $isobsolete, $desc) =
|
||||
$dbh->selectrow_array('SELECT bug_id, isobsolete, description
|
||||
my ($bugid, $isobsolete, $desc, $is_patch) =
|
||||
$dbh->selectrow_array('SELECT bug_id, isobsolete, description, ispatch
|
||||
FROM attachments WHERE attach_id = ?',
|
||||
undef, $attachid);
|
||||
|
||||
|
@ -314,9 +333,17 @@ sub get_attachment_link {
|
|||
|
||||
$link_text =~ s/ \[details\]$//;
|
||||
my $linkval = correct_urlbase()."attachment.cgi?id=$attachid";
|
||||
|
||||
# If the attachment is a patch, try to link to the diff rather
|
||||
# than the text, by default.
|
||||
my $patchlink = "";
|
||||
if ($is_patch and Bugzilla->feature('patch_viewer')) {
|
||||
$patchlink = '&action=diff';
|
||||
}
|
||||
|
||||
# Whitespace matters here because these links are in <pre> tags.
|
||||
return qq|<span class="$className">|
|
||||
. qq|<a href="${linkval}" name="attach_${attachid}" title="$title">$link_text</a>|
|
||||
. qq|<a href="${linkval}${patchlink}" name="attach_${attachid}" title="$title">$link_text</a>|
|
||||
. qq| <a href="${linkval}&action=edit" title="$title">[details]</a>|
|
||||
. qq|</span>|;
|
||||
}
|
||||
|
@ -333,45 +360,36 @@ sub get_attachment_link {
|
|||
# comment in the bug
|
||||
|
||||
sub get_bug_link {
|
||||
my ($bug_num, $link_text, $options) = @_;
|
||||
my ($bug, $link_text, $options) = @_;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
if (!defined($bug_num) || ($bug_num eq "")) {
|
||||
return "<missing bug number>";
|
||||
if (!$bug) {
|
||||
return html_quote('<missing bug number>');
|
||||
}
|
||||
my $quote_bug_num = html_quote($bug_num);
|
||||
detaint_natural($bug_num) || return "<invalid bug number: $quote_bug_num>";
|
||||
|
||||
my ($bug_alias, $bug_state, $bug_res, $bug_desc, $bug_product, $bug_component) =
|
||||
$dbh->selectrow_array('SELECT b.alias, b.bug_status, b.resolution, b.short_desc, p.name, c.name
|
||||
FROM bugs b, products p, components c WHERE b.bug_id=? AND p.id=b.product_id AND c.id=b.component_id',
|
||||
undef, $bug_num);
|
||||
$bug = blessed($bug) ? $bug : new Bugzilla::Bug($bug);
|
||||
return $link_text if $bug->{error};
|
||||
|
||||
if ($bug_state) {
|
||||
# CustIS Bug 53691
|
||||
my $title = get_text('get_status', {status => $bug_state});
|
||||
if ($bug_state eq 'RESOLVED' && $bug_res)
|
||||
{
|
||||
$title .= ' ' . get_text('get_resolution', {resolution => $bug_res});
|
||||
}
|
||||
if (Bugzilla->user->can_see_bug($bug_num)) {
|
||||
$title .= " - $bug_product/$bug_component - $bug_desc";
|
||||
if (Bugzilla->params->{usebugaliases} && $options->{use_alias} && $link_text =~ /^\d+$/ && $bug_alias) {
|
||||
$link_text = $bug_alias;
|
||||
}
|
||||
}
|
||||
# Prevent code injection in the title.
|
||||
$title = html_quote(clean_text($title));
|
||||
|
||||
my $linkval = correct_urlbase()."show_bug.cgi?id=$bug_num";
|
||||
if ($options->{comment_num}) {
|
||||
$linkval .= "#c" . $options->{comment_num};
|
||||
}
|
||||
return qq{<span class="bz_st_$bug_state"><a href="$linkval" title="$title">$link_text</a></span>};
|
||||
my $title = get_text('get_status', { status => $bug->bug_status });
|
||||
if ($bug->resolution) {
|
||||
$title .= ' ' . get_text('get_resolution',
|
||||
{ resolution => $bug->resolution });
|
||||
}
|
||||
else {
|
||||
return qq{$link_text};
|
||||
if (Bugzilla->user->can_see_bug($bug)) {
|
||||
$title .= ' - ' . $bug->product.'/'.$bug->component . ' - ' . $bug->short_desc;
|
||||
if (Bugzilla->params->{usebugaliases} && $options->{use_alias} && $link_text =~ /^\d+$/ && $bug->alias) {
|
||||
$link_text = $bug->alias;
|
||||
}
|
||||
}
|
||||
# Prevent code injection in the title.
|
||||
$title = html_quote(clean_text($title));
|
||||
|
||||
my $linkval = correct_urlbase()."show_bug.cgi?id=".$bug->id;
|
||||
if ($options->{comment_num}) {
|
||||
$linkval .= "#c" . $options->{comment_num};
|
||||
}
|
||||
# CustIS Bug 53691
|
||||
return "<span class=\"bz_st_".$bug->bug_status."\"><a href=\"$linkval\" title=\"$title\">$link_text</a></span>";
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
|
@ -389,6 +407,9 @@ use Template::Stash::XS;
|
|||
|
||||
$Template::Config::STASH = 'Template::Stash::XS';
|
||||
|
||||
# Allow keys to start with an underscore or a dot.
|
||||
$Template::Stash::PRIVATE = undef;
|
||||
|
||||
# Add "contains***" methods to list variables that search for one or more
|
||||
# items in a list and return boolean values representing whether or not
|
||||
# one/all/any item(s) were found.
|
||||
|
@ -457,19 +478,12 @@ sub create {
|
|||
my $class = shift;
|
||||
my %opts = @_;
|
||||
|
||||
# checksetup.pl will call us once for any template/lang directory.
|
||||
# We need a possibility to reset the cache, so that no files from
|
||||
# the previous language pollute the action.
|
||||
if ($opts{'clean_cache'}) {
|
||||
delete Bugzilla->request_cache->{template_include_path_};
|
||||
}
|
||||
# IMPORTANT - If you make any FILTER changes here, make sure to
|
||||
# make them in t/004.template.t also, if required.
|
||||
|
||||
# IMPORTANT - If you make any configuration changes here, make sure to
|
||||
# make them in t/004.template.t and checksetup.pl.
|
||||
|
||||
return $class->new({
|
||||
my $config = {
|
||||
# Colon-separated list of directories containing templates.
|
||||
INCLUDE_PATH => [\&getTemplateIncludePath],
|
||||
INCLUDE_PATH => $opts{'include_path'} || getTemplateIncludePath(),
|
||||
|
||||
# Remove white-space before template directives (PRE_CHOMP) and at the
|
||||
# beginning and end of templates and template blocks (TRIM) for better
|
||||
|
@ -478,10 +492,18 @@ sub create {
|
|||
PRE_CHOMP => 1,
|
||||
TRIM => 1,
|
||||
|
||||
# Bugzilla::Template::Plugin::Hook uses the absolute (in mod_perl)
|
||||
# or relative (in mod_cgi) paths of hook files to explicitly compile
|
||||
# a specific file. Also, these paths may be absolute at any time
|
||||
# if a packager has modified bz_locations() to contain absolute
|
||||
# paths.
|
||||
ABSOLUTE => 1,
|
||||
RELATIVE => $ENV{MOD_PERL} ? 0 : 1,
|
||||
|
||||
COMPILE_DIR => bz_locations()->{'datadir'} . "/template",
|
||||
|
||||
# Initialize templates (f.e. by loading plugins like Hook).
|
||||
PRE_PROCESS => "global/initialize.none.tmpl",
|
||||
PRE_PROCESS => ["global/initialize.none.tmpl"],
|
||||
|
||||
ENCODING => Bugzilla->params->{'utf8'} ? 'UTF-8' : undef,
|
||||
|
||||
|
@ -582,10 +604,10 @@ sub create {
|
|||
css_class_quote => \&Bugzilla::Util::css_class_quote ,
|
||||
|
||||
quoteUrls => [ sub {
|
||||
my ($context, $bug) = @_;
|
||||
my ($context, $bug, $comment) = @_;
|
||||
return sub {
|
||||
my $text = shift;
|
||||
return quoteUrls($text, $bug);
|
||||
return quoteUrls($text, $bug, $comment);
|
||||
};
|
||||
},
|
||||
1
|
||||
|
@ -671,39 +693,7 @@ sub create {
|
|||
1
|
||||
],
|
||||
|
||||
# Bug 120030: Override html filter to obscure the '@' in user
|
||||
# visible strings.
|
||||
# Bug 319331: Handle BiDi disruptions.
|
||||
html => sub {
|
||||
my ($var) = Template::Filters::html_filter(@_);
|
||||
# Obscure '@'.
|
||||
$var =~ s/\@/\@/g;
|
||||
if (Bugzilla->params->{'utf8'}) {
|
||||
# Remove the following characters because they're
|
||||
# influencing BiDi:
|
||||
# --------------------------------------------------------
|
||||
# |Code |Name |UTF-8 representation|
|
||||
# |------|--------------------------|--------------------|
|
||||
# |U+202a|Left-To-Right Embedding |0xe2 0x80 0xaa |
|
||||
# |U+202b|Right-To-Left Embedding |0xe2 0x80 0xab |
|
||||
# |U+202c|Pop Directional Formatting|0xe2 0x80 0xac |
|
||||
# |U+202d|Left-To-Right Override |0xe2 0x80 0xad |
|
||||
# |U+202e|Right-To-Left Override |0xe2 0x80 0xae |
|
||||
# --------------------------------------------------------
|
||||
#
|
||||
# The following are characters influencing BiDi, too, but
|
||||
# they can be spared from filtering because they don't
|
||||
# influence more than one character right or left:
|
||||
# --------------------------------------------------------
|
||||
# |Code |Name |UTF-8 representation|
|
||||
# |------|--------------------------|--------------------|
|
||||
# |U+200e|Left-To-Right Mark |0xe2 0x80 0x8e |
|
||||
# |U+200f|Right-To-Left Mark |0xe2 0x80 0x8f |
|
||||
# --------------------------------------------------------
|
||||
$var =~ s/[\x{202a}-\x{202e}]//g;
|
||||
}
|
||||
return $var;
|
||||
},
|
||||
html => \&Bugzilla::Util::html_quote,
|
||||
|
||||
html_light => \&Bugzilla::Util::html_light_quote,
|
||||
|
||||
|
@ -747,10 +737,18 @@ sub create {
|
|||
$var =~ s/\>/>/g;
|
||||
$var =~ s/\"/\"/g;
|
||||
$var =~ s/\&/\&/g;
|
||||
# Now remove extra whitespace, and wrap it to 72 characters.
|
||||
# Now remove extra whitespace...
|
||||
my $collapse_filter = $Template::Filters::FILTERS->{collapse};
|
||||
$var = $collapse_filter->($var);
|
||||
$var = wrap_comment($var, 72);
|
||||
# And if we're not in the WebService, wrap the message.
|
||||
# (Wrapping the message in the WebService is unnecessary
|
||||
# and causes awkward things like \n's appearing in error
|
||||
# messages in JSON-RPC.)
|
||||
unless (Bugzilla->usage_mode == USAGE_MODE_JSON
|
||||
or Bugzilla->usage_mode == USAGE_MODE_XMLRPC)
|
||||
{
|
||||
$var = wrap_comment($var, 72);
|
||||
}
|
||||
return $var;
|
||||
},
|
||||
|
||||
|
@ -800,17 +798,18 @@ sub create {
|
|||
# If an sudo session is in progress, this is the user we're faking
|
||||
'user' => sub { return Bugzilla->user; },
|
||||
|
||||
# Currenly active language
|
||||
# XXX Eventually this should probably be replaced with something
|
||||
# like Bugzilla->language.
|
||||
'current_language' => sub {
|
||||
my ($language) = include_languages();
|
||||
return $language;
|
||||
},
|
||||
|
||||
# If an sudo session is in progress, this is the user who
|
||||
# started the session.
|
||||
'sudoer' => sub { return Bugzilla->sudoer; },
|
||||
|
||||
# SendBugMail - sends mail about a bug, using Bugzilla::BugMail.pm
|
||||
'SendBugMail' => sub {
|
||||
my ($id, $mailrecipients) = (@_);
|
||||
require Bugzilla::BugMail;
|
||||
Bugzilla::BugMail::Send($id, $mailrecipients);
|
||||
},
|
||||
|
||||
# StopBugMail - stops mail about a bug, modifying `lastdiffed`
|
||||
'StopBugMail' => sub {
|
||||
my ($id) = @_;
|
||||
|
@ -840,24 +839,55 @@ sub create {
|
|||
return $cache->{template_bug_fields};
|
||||
},
|
||||
|
||||
# Whether or not keywords are enabled, in this Bugzilla.
|
||||
'use_keywords' => sub { return Bugzilla::Keyword->any_exist; },
|
||||
|
||||
'last_bug_list' => sub {
|
||||
my @bug_list;
|
||||
my $cgi = Bugzilla->cgi;
|
||||
if ($cgi->cookie("BUGLIST")) {
|
||||
@bug_list = split(/:/, $cgi->cookie("BUGLIST"));
|
||||
}
|
||||
return \@bug_list;
|
||||
},
|
||||
|
||||
'feature_enabled' => sub { return Bugzilla->feature(@_); },
|
||||
|
||||
# field_descs can be somewhat slow to generate, so we generate
|
||||
# it only once per-language no matter how many times
|
||||
# $template->process() is called.
|
||||
'field_descs' => sub { return template_var('field_descs') },
|
||||
|
||||
'install_string' => \&Bugzilla::Install::Util::install_string,
|
||||
|
||||
# These don't work as normal constants.
|
||||
DB_MODULE => \&Bugzilla::Constants::DB_MODULE,
|
||||
REQUIRED_MODULES =>
|
||||
\&Bugzilla::Install::Requirements::REQUIRED_MODULES,
|
||||
OPTIONAL_MODULES => sub {
|
||||
my @optional = @{OPTIONAL_MODULES()};
|
||||
@optional = sort {$a->{feature} cmp $b->{feature}}
|
||||
@optional;
|
||||
foreach my $item (@optional) {
|
||||
my @features;
|
||||
foreach my $feat_id (@{ $item->{feature} }) {
|
||||
push(@features, install_string("feature_$feat_id"));
|
||||
}
|
||||
$item->{feature} = \@features;
|
||||
}
|
||||
return \@optional;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
}) || die("Template creation failed: " . $class->error());
|
||||
local $Template::Config::CONTEXT = 'Bugzilla::Template::Context';
|
||||
|
||||
Bugzilla::Hook::process('template_before_create', { config => $config });
|
||||
my $template = $class->new($config)
|
||||
|| die("Template creation failed: " . $class->error());
|
||||
return $template;
|
||||
}
|
||||
|
||||
# Used as part of the two subroutines below.
|
||||
our (%_templates_to_precompile, $_current_path);
|
||||
|
||||
our %_templates_to_precompile;
|
||||
sub precompile_templates {
|
||||
my ($output) = @_;
|
||||
|
||||
|
@ -866,49 +896,37 @@ sub precompile_templates {
|
|||
if (-e "$datadir/template") {
|
||||
print install_string('template_removing_dir') . "\n" if $output;
|
||||
|
||||
# XXX This frequently fails if the webserver made the files, because
|
||||
# then the webserver owns the directories. We could fix that by
|
||||
# doing a chmod/chown on all the directories here.
|
||||
# This frequently fails if the webserver made the files, because
|
||||
# then the webserver owns the directories.
|
||||
rmtree("$datadir/template");
|
||||
|
||||
# Check that the directory was really removed
|
||||
if(-e "$datadir/template") {
|
||||
print "\n\n";
|
||||
print "The directory '$datadir/template' could not be removed.\n";
|
||||
print "Please remove it manually and rerun checksetup.pl.\n\n";
|
||||
exit;
|
||||
# Check that the directory was really removed, and if not, move it
|
||||
# into data/deleteme/.
|
||||
if (-e "$datadir/template") {
|
||||
print STDERR "\n\n",
|
||||
install_string('template_removal_failed',
|
||||
{ datadir => $datadir }), "\n\n";
|
||||
mkpath("$datadir/deleteme");
|
||||
my $random = generate_random_password();
|
||||
rename("$datadir/template", "$datadir/deleteme/$random")
|
||||
or die "move failed: $!";
|
||||
}
|
||||
}
|
||||
|
||||
print install_string('template_precompile') if $output;
|
||||
|
||||
my $templatedir = bz_locations()->{'templatedir'};
|
||||
# Don't hang on templates which use the CGI library
|
||||
eval("use CGI qw(-no_debug)");
|
||||
my $paths = template_include_path({ use_languages => Bugzilla->languages });
|
||||
|
||||
my $dir_reader = new IO::Dir($templatedir) || die "$templatedir: $!";
|
||||
my @language_dirs = grep { /^[a-z-]+$/i } $dir_reader->read;
|
||||
$dir_reader->close;
|
||||
foreach my $dir (@$paths) {
|
||||
my $template = Bugzilla::Template->create(include_path => [$dir]);
|
||||
|
||||
foreach my $dir (@language_dirs) {
|
||||
next if ($dir eq 'CVS');
|
||||
-d "$templatedir/$dir/default" || -d "$templatedir/$dir/custom"
|
||||
|| next;
|
||||
local $ENV{'HTTP_ACCEPT_LANGUAGE'} = $dir;
|
||||
my $template = Bugzilla::Template->create(clean_cache => 1);
|
||||
|
||||
# Precompile all the templates found in all the directories.
|
||||
%_templates_to_precompile = ();
|
||||
foreach my $subdir (qw(custom extension default), bz_locations()->{'project'}) {
|
||||
next unless $subdir; # If 'project' is empty.
|
||||
$_current_path = File::Spec->catdir($templatedir, $dir, $subdir);
|
||||
next unless -d $_current_path;
|
||||
# Traverse the template hierarchy.
|
||||
find({ wanted => \&_precompile_push, no_chdir => 1 }, $_current_path);
|
||||
}
|
||||
# Traverse the template hierarchy.
|
||||
find({ wanted => \&_precompile_push, no_chdir => 1 }, $dir);
|
||||
# The sort isn't totally necessary, but it makes debugging easier
|
||||
# by making the templates always be compiled in the same order.
|
||||
foreach my $file (sort keys %_templates_to_precompile) {
|
||||
$file =~ s{^\Q$dir\E/}{};
|
||||
# Compile the template but throw away the result. This has the side-
|
||||
# effect of writing the compiled version to disk.
|
||||
$template->context->template($file);
|
||||
|
@ -918,28 +936,17 @@ sub precompile_templates {
|
|||
# Under mod_perl, we look for templates using the absolute path of the
|
||||
# template directory, which causes Template Toolkit to look for their
|
||||
# *compiled* versions using the full absolute path under the data/template
|
||||
# directory. (Like data/template/var/www/html/mod_perl/.) To avoid
|
||||
# directory. (Like data/template/var/www/html/bugzilla/.) To avoid
|
||||
# re-compiling templates under mod_perl, we symlink to the
|
||||
# already-compiled templates. This doesn't work on Windows.
|
||||
if (!ON_WINDOWS) {
|
||||
my $abs_root = dirname(abs_path($templatedir));
|
||||
my $todir = "$datadir/template$abs_root";
|
||||
mkpath($todir);
|
||||
# We use abs2rel so that the symlink will look like
|
||||
# "../../../../template" which works, while just
|
||||
# "data/template/template/" doesn't work.
|
||||
my $fromdir = File::Spec->abs2rel("$datadir/template/template", $todir);
|
||||
# We eval for systems that can't symlink at all, where "symlink"
|
||||
# throws a fatal error.
|
||||
eval { symlink($fromdir, "$todir/template")
|
||||
or warn "Failed to symlink from $fromdir to $todir: $!" };
|
||||
# We do these separately in case they're in different locations.
|
||||
_do_template_symlink(bz_locations()->{'templatedir'});
|
||||
_do_template_symlink(bz_locations()->{'extensionsdir'});
|
||||
}
|
||||
|
||||
# If anything created a Template object before now, clear it out.
|
||||
delete Bugzilla->request_cache->{template};
|
||||
# This is the single variable used to precompile templates,
|
||||
# which needs to be cleared as well.
|
||||
delete Bugzilla->request_cache->{template_include_path_};
|
||||
|
||||
print install_string('done') . "\n" if $output;
|
||||
}
|
||||
|
@ -950,11 +957,40 @@ sub _precompile_push {
|
|||
return if (-d $name);
|
||||
return if ($name =~ /\/CVS\//);
|
||||
return if ($name !~ /\.tmpl$/);
|
||||
|
||||
$name =~ s/\Q$_current_path\E\///;
|
||||
$_templates_to_precompile{$name} = 1;
|
||||
}
|
||||
|
||||
# Helper for precompile_templates
|
||||
sub _do_template_symlink {
|
||||
my $dir_to_symlink = shift;
|
||||
|
||||
my $abs_path = abs_path($dir_to_symlink);
|
||||
|
||||
# If $dir_to_symlink is already an absolute path (as might happen
|
||||
# with packagers who set $libpath to an absolute path), then we don't
|
||||
# need to do this symlink.
|
||||
return if ($abs_path eq $dir_to_symlink);
|
||||
|
||||
my $abs_root = dirname($abs_path);
|
||||
my $dir_name = basename($abs_path);
|
||||
my $datadir = bz_locations()->{'datadir'};
|
||||
my $container = "$datadir/template$abs_root";
|
||||
mkpath($container);
|
||||
my $target = "$datadir/template/$dir_name";
|
||||
# Check if the directory exists, because if there are no extensions,
|
||||
# there won't be an "data/template/extensions" directory to link to.
|
||||
if (-d $target) {
|
||||
# We use abs2rel so that the symlink will look like
|
||||
# "../../../../template" which works, while just
|
||||
# "data/template/template/" doesn't work.
|
||||
my $relative_target = File::Spec->abs2rel($target, $container);
|
||||
|
||||
my $link_name = "$container/$dir_name";
|
||||
symlink($relative_target, $link_name)
|
||||
or warn "Could not make $link_name a symlink to $relative_target: $!";
|
||||
}
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
# -*- 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 ITA Software.
|
||||
# Portions created by the Initial Developer are Copyright (C) 2009
|
||||
# the Initial Developer. All Rights Reserved.
|
||||
#
|
||||
# Contributor(s):
|
||||
# Max Kanat-Alexander <mkanat@bugzilla.org>
|
||||
|
||||
# This exists to implement the template-before_process hook.
|
||||
package Bugzilla::Template::Context;
|
||||
use strict;
|
||||
use base qw(Template::Context);
|
||||
|
||||
use Bugzilla::Hook;
|
||||
use Scalar::Util qw(blessed);
|
||||
|
||||
sub process {
|
||||
my $self = shift;
|
||||
# We don't want to run the template_before_process hook for
|
||||
# template hooks (but we do want it to run if a hook calls
|
||||
# PROCESS inside itself). The problem is that the {component}->{name} of
|
||||
# hooks is unreliable--sometimes it starts with ./ and it's the
|
||||
# full path to the hook template, and sometimes it's just the relative
|
||||
# name (like hook/global/field-descs-end.none.tmpl). Also, calling
|
||||
# template_before_process for hook templates doesn't seem too useful,
|
||||
# because that's already part of the extension and they should be able
|
||||
# to modify their hook if they want (or just modify the variables in the
|
||||
# calling template).
|
||||
if (not delete $self->{bz_in_hook}) {
|
||||
$self->{bz_in_process} = 1;
|
||||
}
|
||||
my $result = $self->SUPER::process(@_);
|
||||
delete $self->{bz_in_process};
|
||||
return $result;
|
||||
}
|
||||
|
||||
# This method is called by Template-Toolkit exactly once per template or
|
||||
# block (look at a compiled template) so this is an ideal place for us to
|
||||
# modify the variables before a template or block runs.
|
||||
#
|
||||
# We don't do it during Context::process because at that time
|
||||
# our stash hasn't been set correctly--the parameters we were passed
|
||||
# in the PROCESS or INCLUDE directive haven't been set, and if we're
|
||||
# in an INCLUDE, the stash is not yet localized during process().
|
||||
sub stash {
|
||||
my $self = shift;
|
||||
my $stash = $self->SUPER::stash(@_);
|
||||
|
||||
my $name = $stash->get([ 'component', 0, 'name', 0 ]);
|
||||
my $pre_process = $self->config->{PRE_PROCESS};
|
||||
|
||||
# Checking bz_in_process tells us that we were indeed called as part of a
|
||||
# Context::process, and not at some other point.
|
||||
#
|
||||
# Checking $name makes sure that we're processing a file, and not just a
|
||||
# block, by checking that the name has a period in it. We don't allow
|
||||
# blocks because their names are too unreliable--an extension could have
|
||||
# a block with the same name, or multiple files could have a same-named
|
||||
# block, and then your extension would malfunction.
|
||||
#
|
||||
# We also make sure that we don't run, ever, during the PRE_PROCESS
|
||||
# templates, because if somebody calls Throw*Error globally inside of
|
||||
# template_before_process, that causes an infinite recursion into
|
||||
# the PRE_PROCESS templates (because Bugzilla, while inside
|
||||
# global/intialize.none.tmpl, loads the template again to create the
|
||||
# template object for Throw*Error).
|
||||
#
|
||||
# Checking Bugzilla::Hook::in prevents infinite recursion on this hook.
|
||||
if ($self->{bz_in_process} and $name =~ /\./
|
||||
and !grep($_ eq $name, @$pre_process)
|
||||
and !Bugzilla::Hook::in('template_before_process'))
|
||||
{
|
||||
Bugzilla::Hook::process("template_before_process",
|
||||
{ vars => $stash, context => $self,
|
||||
file => $name });
|
||||
}
|
||||
|
||||
# This prevents other calls to stash() that might somehow happen
|
||||
# later in the file from also triggering the hook.
|
||||
delete $self->{bz_in_process};
|
||||
|
||||
return $stash;
|
||||
}
|
||||
|
||||
# We need a DESTROY sub for the same reason that Bugzilla::CGI does.
|
||||
sub DESTROY {
|
||||
my $self = shift;
|
||||
$self->SUPER::DESTROY(@_);
|
||||
};
|
||||
|
||||
1;
|
|
@ -20,91 +20,88 @@
|
|||
# Contributor(s): Myk Melez <myk@mozilla.org>
|
||||
# Zach Lipton <zach@zachlipton.com>
|
||||
# Elliotte Martin <everythingsolved.com>
|
||||
#
|
||||
# Max Kanat-Alexander <mkanat@bugzilla.org>
|
||||
|
||||
package Bugzilla::Template::Plugin::Hook;
|
||||
|
||||
use strict;
|
||||
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::Install::Util qw(include_languages);
|
||||
use Bugzilla::Template;
|
||||
use Bugzilla::Util;
|
||||
use Bugzilla::Error;
|
||||
use File::Spec;
|
||||
|
||||
use base qw(Template::Plugin);
|
||||
|
||||
sub load {
|
||||
my ($class, $context) = @_;
|
||||
return $class;
|
||||
}
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::Install::Util qw(include_languages template_include_path);
|
||||
use Bugzilla::Util;
|
||||
use Bugzilla::Error;
|
||||
|
||||
use File::Spec;
|
||||
|
||||
sub new {
|
||||
my ($class, $context) = @_;
|
||||
return bless { _CONTEXT => $context }, $class;
|
||||
}
|
||||
|
||||
sub _context { return $_[0]->{_CONTEXT} }
|
||||
|
||||
sub process {
|
||||
my ($self, $hook_name, $template) = @_;
|
||||
$template ||= $self->{_CONTEXT}->stash->{component}->{name};
|
||||
|
||||
my @hooks;
|
||||
my $context = $self->_context();
|
||||
$template ||= $context->stash->get([ 'component', 0, 'name', 0 ]);
|
||||
|
||||
# sanity check:
|
||||
if (!$template =~ /[\w\.\/\-_\\]+/) {
|
||||
ThrowCodeError('template_invalid', { name => $template});
|
||||
if ($template !~ /^[\w\.\/\-_\\]+$/) {
|
||||
ThrowCodeError('template_invalid', { name => $template });
|
||||
}
|
||||
|
||||
# also get extension hook files that live in extensions/:
|
||||
# parse out the parts of the template name
|
||||
my ($vol, $subpath, $filename) = File::Spec->splitpath($template);
|
||||
$subpath = $subpath || '';
|
||||
$filename =~ m/(.*)\.(.*)\.tmpl/;
|
||||
my $templatename = $1;
|
||||
my (undef, $path, $filename) = File::Spec->splitpath($template);
|
||||
$path ||= '';
|
||||
$filename =~ m/(.+)\.(.+)\.tmpl$/;
|
||||
my $template_name = $1;
|
||||
my $type = $2;
|
||||
# munge the filename to create the extension hook filename:
|
||||
my $extensiontemplate = $subpath.'/'.$templatename.'-'.$hook_name.'.'.$type.'.tmpl';
|
||||
my @extensions = glob(bz_locations()->{'extensionsdir'} . "/*");
|
||||
my @usedlanguages = include_languages({use_languages => Bugzilla->languages});
|
||||
foreach my $extension (@extensions) {
|
||||
next if -e "$extension/disabled";
|
||||
foreach my $language (@usedlanguages) {
|
||||
my $file = $extension.'/template/'.$language.'/'.$extensiontemplate;
|
||||
|
||||
# Hooks are named like this:
|
||||
my $extension_template = "$path$template_name-$hook_name.$type.tmpl";
|
||||
|
||||
# Get the hooks out of the cache if they exist. Otherwise, read them
|
||||
# from the disk.
|
||||
my $cache = Bugzilla->request_cache->{template_plugin_hook_cache} ||= {};
|
||||
my $lang = Bugzilla->request_cache->{language} || '';
|
||||
$cache->{"${lang}__$extension_template"}
|
||||
||= $self->_get_hooks($extension_template);
|
||||
|
||||
# process() accepts an arrayref of templates, so we just pass the whole
|
||||
# arrayref.
|
||||
$context->{bz_in_hook} = 1; # See Bugzilla::Template::Context
|
||||
return $context->process($cache->{"${lang}__$extension_template"});
|
||||
}
|
||||
|
||||
sub _get_hooks {
|
||||
my ($self, $extension_template) = @_;
|
||||
|
||||
my $template_sets = _template_hook_include_path();
|
||||
my @hooks;
|
||||
foreach my $dir_set (@$template_sets) {
|
||||
foreach my $template_dir (@$dir_set) {
|
||||
my $file = "$template_dir/hook/$extension_template";
|
||||
if (-e $file) {
|
||||
# tt is stubborn and won't take a template file not in its
|
||||
# include path, so we open a filehandle and give it to process()
|
||||
# so the hook gets invoked:
|
||||
open (my $fh, $file);
|
||||
push(@hooks, $fh);
|
||||
my $template = $self->_context->template($file);
|
||||
push(@hooks, $template);
|
||||
# Don't run the hook for more than one language.
|
||||
last;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
my $paths = $self->{_CONTEXT}->{LOAD_TEMPLATES}->[0]->paths;
|
||||
return \@hooks;
|
||||
}
|
||||
|
||||
# we keep this too since you can still put hook templates in
|
||||
# template/en/custom/hook
|
||||
foreach my $path (@$paths) {
|
||||
my @files = glob("$path/hook/$template/$hook_name/*.tmpl");
|
||||
|
||||
# Have to remove the templates path (INCLUDE_PATH) from the
|
||||
# file path since the template processor auto-adds it back.
|
||||
@files = map($_ =~ /^$path\/(.*)$/ ? $1 : {}, @files);
|
||||
|
||||
# Add found files to the list of hooks, but removing duplicates,
|
||||
# which can happen when there are identical hooks or duplicate
|
||||
# directories in the INCLUDE_PATH (the latter probably being a TT bug).
|
||||
foreach my $file (@files) {
|
||||
push(@hooks, $file) unless grep($file eq $_, @hooks);
|
||||
}
|
||||
}
|
||||
|
||||
my $output;
|
||||
foreach my $hook (@hooks) {
|
||||
$output .= $self->{_CONTEXT}->process($hook);
|
||||
}
|
||||
return $output;
|
||||
sub _template_hook_include_path {
|
||||
my $cache = Bugzilla->request_cache;
|
||||
my $language = $cache->{language} || '';
|
||||
my $cache_key = "template_plugin_hook_include_path_$language";
|
||||
$cache->{$cache_key} ||= template_include_path({
|
||||
use_languages => Bugzilla->languages,
|
||||
only_language => $language,
|
||||
hook => 1,
|
||||
});
|
||||
return $cache->{$cache_key};
|
||||
}
|
||||
|
||||
1;
|
||||
|
@ -165,8 +162,4 @@ Output from processing template extension.
|
|||
|
||||
L<Template::Plugin>
|
||||
|
||||
L<http://www.bugzilla.org/docs/tip/html/customization.html>
|
||||
|
||||
L<http://bugzilla.mozilla.org/show_bug.cgi?id=229658>
|
||||
|
||||
L<http://bugzilla.mozilla.org/show_bug.cgi?id=298341>
|
||||
L<http://wiki.mozilla.org/Bugzilla:Writing_Extensions>
|
||||
|
|
|
@ -142,7 +142,7 @@ sub IssuePasswordToken {
|
|||
|
||||
ThrowUserError('too_soon_for_new_token', {'type' => 'password'}) if $too_soon;
|
||||
|
||||
my ($token, $token_ts) = _create_token($user->id, 'password', $::ENV{'REMOTE_ADDR'});
|
||||
my ($token, $token_ts) = _create_token($user->id, 'password', remote_ip());
|
||||
|
||||
# Mail the user the token along with instructions for using it.
|
||||
my $template = Bugzilla->template_inner($user->settings->{'lang'}->{'value'});
|
||||
|
@ -283,7 +283,7 @@ sub Cancel {
|
|||
my $user = new Bugzilla::User($userid);
|
||||
|
||||
$vars->{'emailaddress'} = $userid ? $user->email : $eventdata;
|
||||
$vars->{'remoteaddress'} = $::ENV{'REMOTE_ADDR'};
|
||||
$vars->{'remoteaddress'} = remote_ip();
|
||||
$vars->{'token'} = $token;
|
||||
$vars->{'tokentype'} = $tokentype;
|
||||
$vars->{'issuedate'} = $issuedate;
|
||||
|
|
|
@ -27,29 +27,20 @@ use constant TIMEOUT => 5; # Number of seconds before timeout.
|
|||
|
||||
# Look for new releases and notify logged in administrators about them.
|
||||
sub get_notifications {
|
||||
return if !Bugzilla->feature('updates');
|
||||
return if (Bugzilla->params->{'upgrade_notification'} eq 'disabled');
|
||||
|
||||
# If the XML::Twig module is missing, we won't be able to parse
|
||||
# the XML file. So there is no need to go further.
|
||||
eval("require XML::Twig");
|
||||
return if $@;
|
||||
|
||||
my $local_file = bz_locations()->{'datadir'} . LOCAL_FILE;
|
||||
# Update the local XML file if this one doesn't exist or if
|
||||
# the last modification time (stat[9]) is older than TIME_INTERVAL.
|
||||
if (!-e $local_file || (time() - (stat($local_file))[9] > TIME_INTERVAL)) {
|
||||
# Are we sure we didn't try to refresh this file already
|
||||
# but we failed because we cannot modify its timestamp?
|
||||
my $can_alter = (-e $local_file) ? utime(undef, undef, $local_file) : 1;
|
||||
if ($can_alter) {
|
||||
unlink $local_file; # Make sure the old copy is away.
|
||||
my $error = _synchronize_data();
|
||||
# If an error is returned, leave now.
|
||||
return $error if $error;
|
||||
}
|
||||
else {
|
||||
return {'error' => 'no_update', 'xml_file' => $local_file};
|
||||
unlink $local_file; # Make sure the old copy is away.
|
||||
if (-e $local_file) {
|
||||
return { 'error' => 'no_update', xml_file => $local_file };
|
||||
}
|
||||
my $error = _synchronize_data();
|
||||
# If an error is returned, leave now.
|
||||
return $error if $error;
|
||||
}
|
||||
|
||||
# If we cannot access the local XML file, ignore it.
|
||||
|
@ -128,9 +119,6 @@ sub get_notifications {
|
|||
}
|
||||
|
||||
sub _synchronize_data {
|
||||
eval("require LWP::UserAgent");
|
||||
return {'error' => 'missing_package', 'package' => 'LWP::UserAgent'} if $@;
|
||||
|
||||
my $local_file = bz_locations()->{'datadir'} . LOCAL_FILE;
|
||||
|
||||
my $ua = LWP::UserAgent->new();
|
||||
|
|
523
Bugzilla/User.pm
523
Bugzilla/User.pm
|
@ -50,8 +50,9 @@ use Bugzilla::Classification;
|
|||
use Bugzilla::Field;
|
||||
use Bugzilla::Group;
|
||||
|
||||
use Scalar::Util qw(blessed);
|
||||
use DateTime::TimeZone;
|
||||
use Scalar::Util qw(blessed);
|
||||
use Storable qw(dclone);
|
||||
|
||||
use base qw(Bugzilla::Object Exporter);
|
||||
@Bugzilla::User::EXPORT = qw(is_available_username
|
||||
|
@ -135,6 +136,18 @@ sub new {
|
|||
return $class->SUPER::new(@_);
|
||||
}
|
||||
|
||||
sub super_user {
|
||||
my $invocant = shift;
|
||||
my $class = ref($invocant) || $invocant;
|
||||
my ($param) = @_;
|
||||
|
||||
my $user = dclone(DEFAULT_USER);
|
||||
$user->{groups} = [Bugzilla::Group->get_all];
|
||||
$user->{bless_groups} = [Bugzilla::Group->get_all];
|
||||
bless $user, $class;
|
||||
return $user;
|
||||
}
|
||||
|
||||
sub update {
|
||||
my $self = shift;
|
||||
my $changes = $self->SUPER::update(@_);
|
||||
|
@ -234,6 +247,15 @@ sub is_disabled { $_[0]->disabledtext ? 1 : 0; }
|
|||
sub showmybugslink { $_[0]->{showmybugslink}; }
|
||||
sub email_disabled { $_[0]->{disable_mail}; }
|
||||
sub email_enabled { !($_[0]->{disable_mail}); }
|
||||
sub cryptpassword {
|
||||
my $self = shift;
|
||||
# We don't store it because we never want it in the object (we
|
||||
# don't want to accidentally dump even the hash somewhere).
|
||||
my ($pw) = Bugzilla->dbh->selectrow_array(
|
||||
'SELECT cryptpassword FROM profiles WHERE userid = ?',
|
||||
undef, $self->id);
|
||||
return $pw;
|
||||
}
|
||||
|
||||
sub set_authorizer {
|
||||
my ($self, $authorizer) = @_;
|
||||
|
@ -404,8 +426,8 @@ sub groups {
|
|||
|
||||
my $rows = $dbh->selectall_arrayref(
|
||||
"SELECT DISTINCT grantor_id, member_id
|
||||
FROM group_group_map
|
||||
WHERE grant_type = " . GROUP_MEMBERSHIP);
|
||||
FROM group_group_map
|
||||
WHERE grant_type = " . GROUP_MEMBERSHIP);
|
||||
|
||||
my %group_membership;
|
||||
foreach my $row (@$rows) {
|
||||
|
@ -466,18 +488,18 @@ sub bless_groups {
|
|||
|
||||
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.
|
||||
# Get all groups for the user where:
|
||||
# + They have direct bless privileges
|
||||
# + They are a member of a group that inherits bless privs.
|
||||
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 = ?
|
||||
FROM groups, user_group_map, group_group_map AS ggm
|
||||
WHERE user_group_map.user_id = ?
|
||||
AND ( (user_group_map.isbless = 1
|
||||
AND groups.id=user_group_map.group_id)
|
||||
OR (groups.id = ggm.grantor_id
|
||||
AND groups.id=user_group_map.group_id)
|
||||
OR (groups.id = ggm.grantor_id
|
||||
AND ggm.grant_type = ' . GROUP_BLESS . '
|
||||
AND ' . $dbh->sql_in('ggm.member_id', \@group_ids)
|
||||
. ') )';
|
||||
|
@ -496,6 +518,7 @@ sub bless_groups {
|
|||
|
||||
sub in_group {
|
||||
my ($self, $group, $product_id) = @_;
|
||||
$group = $group->name if blessed $group;
|
||||
if (scalar grep($_->name eq $group, @{ $self->groups })) {
|
||||
return 1;
|
||||
}
|
||||
|
@ -647,7 +670,7 @@ sub visible_bugs {
|
|||
my @check_ids = grep(!exists $visible_cache->{$_}, @bug_ids);
|
||||
|
||||
if (@check_ids) {
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $user_id = $self->id;
|
||||
my $sth;
|
||||
# Speed up the can_see_bug case.
|
||||
|
@ -667,12 +690,12 @@ sub visible_bugs {
|
|||
"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
|
||||
FROM bugs
|
||||
LEFT JOIN cc
|
||||
ON cc.bug_id = bugs.bug_id
|
||||
AND cc.who = $user_id
|
||||
LEFT JOIN bug_group_map
|
||||
ON bugs.bug_id = bug_group_map.bug_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 IN (' . join(',', ('?') x @check_ids) . ')
|
||||
|
@ -682,15 +705,16 @@ sub visible_bugs {
|
|||
}
|
||||
|
||||
$sth->execute(@check_ids);
|
||||
my $use_qa_contact = Bugzilla->params->{'useqacontact'};
|
||||
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'}
|
||||
|| ($use_qa_contact
|
||||
&& $qacontact && ($qacontact == $user_id))
|
||||
|| ($owner == $user_id)
|
||||
|| ($isoncclist && $cclist_access)
|
||||
|| ($isoncclist && $cclist_access)
|
||||
|| !$missinggroup) ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
@ -698,6 +722,13 @@ sub visible_bugs {
|
|||
return [grep { $visible_cache->{blessed $_ ? $_->id : $_} } @$bugs];
|
||||
}
|
||||
|
||||
sub clear_product_cache {
|
||||
my $self = shift;
|
||||
delete $self->{enterable_products};
|
||||
delete $self->{selectable_products};
|
||||
delete $self->{selectable_classifications};
|
||||
}
|
||||
|
||||
sub can_see_product {
|
||||
my ($self, $product_name) = @_;
|
||||
|
||||
|
@ -709,32 +740,23 @@ sub get_selectable_products {
|
|||
my $class_id = shift;
|
||||
my $class_restricted = Bugzilla->params->{'useclassification'} && $class_id;
|
||||
|
||||
if (!defined $self->{selectable_products})
|
||||
{
|
||||
if (!defined $self->{selectable_products}) {
|
||||
my $query =
|
||||
"(SELECT id, name AS pname FROM products" .
|
||||
" LEFT JOIN group_control_map g ON g.product_id = products.id " .
|
||||
" AND g.membercontrol=" . CONTROLMAPMANDATORY .
|
||||
" AND g.group_id NOT IN (" . $self->groups_as_string . ")" .
|
||||
" WHERE group_id IS NULL) ";
|
||||
|
||||
if (Bugzilla->params->{useentrygroupdefault})
|
||||
{
|
||||
$query .=
|
||||
" UNION (SELECT id, name AS pname FROM products" .
|
||||
" LEFT JOIN group_control_map g ON g.product_id=products.id" .
|
||||
" AND g.entry != 0 AND g.group_id NOT IN (".$self->groups_as_string.")" .
|
||||
" WHERE g.group_id IS NULL) ";
|
||||
}
|
||||
|
||||
$query .= "UNION (SELECT id, tr_products.name AS pname FROM products AS tr_products ".
|
||||
"INNER JOIN test_plans ON tr_products.id = test_plans.product_id ".
|
||||
"INNER JOIN test_plan_permissions ON test_plan_permissions.plan_id = test_plans.plan_id ".
|
||||
"WHERE test_plan_permissions.userid = ?)";
|
||||
|
||||
$query .= "ORDER BY pname ";
|
||||
|
||||
my $prod_ids = Bugzilla->dbh->selectcol_arrayref($query,undef,$self->id);
|
||||
" WHERE group_id IS NULL)" .
|
||||
" UNION (SELECT id, name AS pname FROM products" .
|
||||
" LEFT JOIN group_control_map g ON g.product_id=products.id" .
|
||||
" AND g.entry != 0 AND g.group_id NOT IN (".$self->groups_as_string.")" .
|
||||
" WHERE g.group_id IS NULL)" .
|
||||
" UNION (SELECT id, tr_products.name AS pname FROM products AS tr_products ".
|
||||
" INNER JOIN test_plans ON tr_products.id = test_plans.product_id ".
|
||||
" INNER JOIN test_plan_permissions ON test_plan_permissions.plan_id = test_plans.plan_id ".
|
||||
" WHERE test_plan_permissions.userid = ?)" .
|
||||
" ORDER BY pname";
|
||||
my $prod_ids = Bugzilla->dbh->selectcol_arrayref($query, undef, $self->id);
|
||||
$self->{selectable_products} = Bugzilla::Product->new_from_list($prod_ids);
|
||||
}
|
||||
|
||||
|
@ -750,7 +772,7 @@ sub get_selectable_classifications {
|
|||
my ($self) = @_;
|
||||
|
||||
if (!defined $self->{selectable_classifications}) {
|
||||
my $products = $self->get_selectable_products;
|
||||
my $products = $self->get_selectable_products;
|
||||
my %class_ids = map { $_->classification_id => 1 } @$products;
|
||||
|
||||
$self->{selectable_classifications} = Bugzilla::Classification->new_from_list([keys %class_ids]);
|
||||
|
@ -759,18 +781,27 @@ sub get_selectable_classifications {
|
|||
}
|
||||
|
||||
sub can_enter_product {
|
||||
my ($self, $product_name, $warn) = @_;
|
||||
my ($self, $input, $warn) = @_;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
$warn ||= 0;
|
||||
|
||||
if (!defined($product_name)) {
|
||||
$input = trim($input) if !ref $input;
|
||||
if (!defined $input or $input eq '') {
|
||||
return unless $warn == THROW_ERROR;
|
||||
ThrowUserError('object_not_specified',
|
||||
{ class => 'Bugzilla::Product' });
|
||||
}
|
||||
|
||||
if (!scalar @{ $self->get_enterable_products }) {
|
||||
return unless $warn == THROW_ERROR;
|
||||
ThrowUserError('no_products');
|
||||
}
|
||||
my $product = new Bugzilla::Product({name => $product_name});
|
||||
|
||||
my $product = blessed($input) ? $input
|
||||
: new Bugzilla::Product({ name => $input });
|
||||
my $can_enter =
|
||||
$product && grep($_->name eq $product->name, @{$self->get_enterable_products});
|
||||
$product && grep($_->name eq $product->name,
|
||||
@{ $self->get_enterable_products });
|
||||
|
||||
return 1 if $can_enter;
|
||||
|
||||
|
@ -779,21 +810,26 @@ sub can_enter_product {
|
|||
# Check why access was denied. These checks are slow,
|
||||
# but that's fine, because they only happen if we fail.
|
||||
|
||||
# We don't just use $product->name for error messages, because if it
|
||||
# changes case from $input, then that's a clue that the product does
|
||||
# exist but is hidden.
|
||||
my $name = blessed($input) ? $input->name : $input;
|
||||
|
||||
# The product could not exist or you could be denied...
|
||||
if (!$product || !$product->user_has_access($self)) {
|
||||
ThrowUserError('entry_access_denied', {product => $product_name});
|
||||
ThrowUserError('entry_access_denied', { product => $name });
|
||||
}
|
||||
# It could be closed for bug entry...
|
||||
elsif ($product->disallow_new) {
|
||||
ThrowUserError('product_disabled', {product => $product});
|
||||
elsif (!$product->is_active) {
|
||||
ThrowUserError('product_disabled', { product => $product });
|
||||
}
|
||||
# It could have no components...
|
||||
elsif (!@{$product->components}) {
|
||||
ThrowUserError('missing_component', {product => $product});
|
||||
ThrowUserError('missing_component', { product => $product });
|
||||
}
|
||||
# It could have no versions...
|
||||
elsif (!@{$product->versions}) {
|
||||
ThrowUserError ('missing_version', {product => $product});
|
||||
ThrowUserError ('missing_version', { product => $product });
|
||||
}
|
||||
|
||||
die "can_enter_product reached an unreachable location.";
|
||||
|
@ -815,7 +851,7 @@ sub get_enterable_products {
|
|||
AND group_control_map.entry != 0
|
||||
AND group_id NOT IN (' . $self->groups_as_string . ')
|
||||
WHERE group_id IS NULL
|
||||
AND products.disallownew = 0') || []};
|
||||
AND products.isactive = 1') || []};
|
||||
|
||||
if (@enterable_ids) {
|
||||
# And all of these products must have at least one component
|
||||
|
@ -834,8 +870,8 @@ sub get_enterable_products {
|
|||
}
|
||||
|
||||
sub can_access_product {
|
||||
my ($self, $product_name) = @_;
|
||||
|
||||
my ($self, $product) = @_;
|
||||
my $product_name = blessed($product) ? $product->name : $product;
|
||||
return scalar(grep {$_->name eq $product_name} @{$self->get_accessible_products});
|
||||
}
|
||||
|
||||
|
@ -858,7 +894,7 @@ sub check_can_admin_product {
|
|||
|
||||
($self->in_group('editcomponents', $product->id) && $self->can_see_product($product->name))
|
||||
|| $self->in_group('editcomponents')
|
||||
|| ThrowUserError('product_admin_denied', {product => $product->name});
|
||||
|| ThrowUserError('product_admin_denied', {product => $product->name});
|
||||
|
||||
# Return the validated product object.
|
||||
return $product;
|
||||
|
@ -1116,15 +1152,11 @@ sub match {
|
|||
# first try wildcards
|
||||
my $wildstr = $str;
|
||||
|
||||
if ($wildstr =~ s/\*/\%/g # don't do wildcards if no '*' in the string
|
||||
&& $user->id
|
||||
# or if we only want exact matches
|
||||
&& Bugzilla->params->{'usermatchmode'} ne 'off')
|
||||
{
|
||||
|
||||
# Do not do wildcards if there is no '*' in the string.
|
||||
if ($wildstr =~ s/\*/\%/g && $user->id) {
|
||||
# Build the query.
|
||||
trick_taint($wildstr);
|
||||
my $query = "SELECT DISTINCT login_name FROM profiles ";
|
||||
my $query = "SELECT DISTINCT userid FROM profiles ";
|
||||
if (Bugzilla->params->{'usevisibilitygroups'}) {
|
||||
$query .= "INNER JOIN user_group_map
|
||||
ON user_group_map.user_id = profiles.userid ";
|
||||
|
@ -1138,15 +1170,12 @@ sub match {
|
|||
join(', ', (-1, @{$user->visible_groups_inherited})) . ") ";
|
||||
}
|
||||
$query .= " AND disabledtext = '' " if $exclude_disabled;
|
||||
$query .= " ORDER BY login_name ";
|
||||
$query .= $dbh->sql_limit($limit) if $limit;
|
||||
|
||||
# Execute the query, retrieve the results, and make them into
|
||||
# User objects.
|
||||
my $user_logins = $dbh->selectcol_arrayref($query, undef, ($wildstr, $wildstr));
|
||||
foreach my $login_name (@$user_logins) {
|
||||
push(@users, new Bugzilla::User({ name => $login_name }));
|
||||
}
|
||||
my $user_ids = $dbh->selectcol_arrayref($query, undef, ($wildstr, $wildstr));
|
||||
@users = @{Bugzilla::User->new_from_list($user_ids)};
|
||||
}
|
||||
else { # try an exact match
|
||||
# Exact matches don't care if a user is disabled.
|
||||
|
@ -1159,14 +1188,10 @@ sub match {
|
|||
}
|
||||
|
||||
# then try substring search
|
||||
if ((scalar(@users) == 0)
|
||||
&& $user->id
|
||||
&& (Bugzilla->params->{'usermatchmode'} eq 'search')
|
||||
&& (length($str) >= 3))
|
||||
{
|
||||
if (!scalar(@users) && length($str) >= 3 && $user->id) {
|
||||
trick_taint($str);
|
||||
|
||||
my $query = "SELECT DISTINCT login_name FROM profiles ";
|
||||
my $query = "SELECT DISTINCT userid FROM profiles ";
|
||||
if (Bugzilla->params->{'usevisibilitygroups'}) {
|
||||
$query .= "INNER JOIN user_group_map
|
||||
ON user_group_map.user_id = profiles.userid ";
|
||||
|
@ -1180,68 +1205,22 @@ sub match {
|
|||
join(', ', (-1, @{$user->visible_groups_inherited})) . ") ";
|
||||
}
|
||||
$query .= " AND disabledtext = '' " if $exclude_disabled;
|
||||
$query .= " ORDER BY login_name ";
|
||||
$query .= $dbh->sql_limit($limit) if $limit;
|
||||
|
||||
my $user_logins = $dbh->selectcol_arrayref($query, undef, ($str, $str));
|
||||
foreach my $login_name (@$user_logins) {
|
||||
push(@users, new Bugzilla::User({ name => $login_name }));
|
||||
}
|
||||
my $user_ids = $dbh->selectcol_arrayref($query, undef, ($str, $str));
|
||||
@users = @{Bugzilla::User->new_from_list($user_ids)};
|
||||
}
|
||||
return \@users;
|
||||
}
|
||||
|
||||
# match_field() is a CGI wrapper for the match() function.
|
||||
#
|
||||
# Here's what it does:
|
||||
#
|
||||
# 1. Accepts a list of fields along with whether they may take multiple values
|
||||
# 2. Takes the values of those fields from the first parameter, a $cgi object
|
||||
# and passes them to match()
|
||||
# 3. Checks the results of the match and displays confirmation or failure
|
||||
# messages as appropriate.
|
||||
#
|
||||
# The confirmation screen functions the same way as verify-new-product and
|
||||
# confirm-duplicate, by rolling all of the state information into a
|
||||
# form which is passed back, but in this case the searched fields are
|
||||
# replaced with the search results.
|
||||
#
|
||||
# The act of displaying the confirmation or failure messages means it must
|
||||
# throw a template and terminate. When confirmation is sent, all of the
|
||||
# searchable fields have been replaced by exact fields and the calling script
|
||||
# is executed as normal.
|
||||
#
|
||||
# You also have the choice of *never* displaying the confirmation screen.
|
||||
# In this case, match_field will return one of the three USER_MATCH
|
||||
# constants described in the POD docs. To make match_field behave this
|
||||
# way, pass in MATCH_SKIP_CONFIRM as the third argument.
|
||||
#
|
||||
# match_field must be called early in a script, before anything external is
|
||||
# done with the form data.
|
||||
#
|
||||
# In order to do a simple match without dealing with templates, confirmation,
|
||||
# or globals, simply calling Bugzilla::User::match instead will be
|
||||
# sufficient.
|
||||
|
||||
# How to call it:
|
||||
#
|
||||
# Bugzilla::User::match_field($cgi, {
|
||||
# 'field_name' => { 'type' => fieldtype },
|
||||
# 'field_name2' => { 'type' => fieldtype },
|
||||
# [...]
|
||||
# });
|
||||
#
|
||||
# fieldtype can be either 'single' or 'multi'.
|
||||
#
|
||||
|
||||
sub match_field {
|
||||
my $cgi = shift; # CGI object to look up fields in
|
||||
my $fields = shift; # arguments as a hash
|
||||
my $data = shift || Bugzilla->input_params; # hash to look up fields in
|
||||
my $behavior = shift || 0; # A constant that tells us how to act
|
||||
my $matches = {}; # the values sent to the template
|
||||
my $matchsuccess = 1; # did the match fail?
|
||||
my $need_confirm = 0; # whether to display confirmation screen
|
||||
my $match_multiple = 0; # whether we ever matched more than one user
|
||||
my @non_conclusive_fields; # fields which don't have a unique user.
|
||||
|
||||
my $params = Bugzilla->params;
|
||||
|
||||
|
@ -1259,7 +1238,8 @@ sub match_field {
|
|||
$expanded_fields->{$field_pattern} = $fields->{$field_pattern};
|
||||
}
|
||||
else {
|
||||
my @field_names = grep(/$field_pattern/, $cgi->param());
|
||||
my @field_names = grep(/$field_pattern/, keys %$data);
|
||||
|
||||
foreach my $field_name (@field_names) {
|
||||
$expanded_fields->{$field_name} =
|
||||
{ type => $fields->{$field_pattern}->{'type'} };
|
||||
|
@ -1285,7 +1265,7 @@ sub match_field {
|
|||
# No need to look for a valid requestee if the flag(type)
|
||||
# has been deleted (may occur in race conditions).
|
||||
delete $expanded_fields->{$field_name};
|
||||
$cgi->delete($field_name);
|
||||
delete $data->{$field_name};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1293,54 +1273,34 @@ sub match_field {
|
|||
}
|
||||
$fields = $expanded_fields;
|
||||
|
||||
for my $field (keys %{$fields}) {
|
||||
foreach my $field (keys %{$fields}) {
|
||||
next unless defined $data->{$field};
|
||||
|
||||
# Tolerate fields that do not exist.
|
||||
#
|
||||
# This is so that fields like qa_contact can be specified in the code
|
||||
# and it won't break if the CGI object does not know about them.
|
||||
#
|
||||
# It has the side-effect that if a bad field name is passed it will be
|
||||
# quietly ignored rather than raising a code error.
|
||||
|
||||
next if !defined $cgi->param($field);
|
||||
|
||||
# We need to move the query to $raw_field, where it will be split up,
|
||||
# modified by the search, and put back into the CGI environment
|
||||
# incrementally.
|
||||
|
||||
my $raw_field = join(" ", $cgi->param($field));
|
||||
|
||||
# When we add back in values later, it matters that we delete
|
||||
# the field here, and not set it to '', so that we will add
|
||||
# things to an empty list, and not to a list containing one
|
||||
# empty string.
|
||||
# If the field accepts only one match (type eq "single") and
|
||||
# no match or more than one match is found for this field,
|
||||
# we will set it back to '' so that the field remains defined
|
||||
# outside this function (it was if we came here; else we would
|
||||
# have returned earlier above).
|
||||
# If the field accepts several matches (type eq "multi") and no match
|
||||
# is found, we leave this field undefined (= empty array).
|
||||
$cgi->delete($field);
|
||||
|
||||
my @queries = ();
|
||||
#Concatenate login names, so that we have a common way to handle them.
|
||||
my $raw_field;
|
||||
if (ref $data->{$field}) {
|
||||
$raw_field = join(" ", @{$data->{$field}});
|
||||
}
|
||||
else {
|
||||
$raw_field = $data->{$field};
|
||||
}
|
||||
$raw_field = clean_text($raw_field || '');
|
||||
|
||||
# Now we either split $raw_field by spaces/commas and put the list
|
||||
# into @queries, or in the case of fields which only accept single
|
||||
# entries, we simply use the verbatim text.
|
||||
|
||||
$raw_field =~ s/^\s+|\s+$//sg; # trim leading/trailing space
|
||||
|
||||
# single field
|
||||
my @queries;
|
||||
if ($fields->{$field}->{'type'} eq 'single') {
|
||||
@queries = ($raw_field) unless $raw_field =~ /^\s*$/;
|
||||
|
||||
# multi-field
|
||||
@queries = ($raw_field);
|
||||
# We will repopulate it later if a match is found, else it must
|
||||
# be set to an empty string so that the field remains defined.
|
||||
$data->{$field} = '';
|
||||
}
|
||||
elsif ($fields->{$field}->{'type'} eq 'multi') {
|
||||
@queries = split(/[\s,]+/, $raw_field);
|
||||
|
||||
@queries = split(/[\s,;]+/, $raw_field);
|
||||
# We will repopulate it later if a match is found, else it must
|
||||
# be undefined.
|
||||
delete $data->{$field};
|
||||
}
|
||||
else {
|
||||
# bad argument
|
||||
|
@ -1350,40 +1310,31 @@ sub match_field {
|
|||
});
|
||||
}
|
||||
|
||||
# Tolerate fields that do not exist (in case you specify
|
||||
# e.g. the QA contact, and it's currently not in use).
|
||||
next unless (defined $raw_field && $raw_field ne '');
|
||||
|
||||
my $limit = 0;
|
||||
if ($params->{'maxusermatches'}) {
|
||||
$limit = $params->{'maxusermatches'} + 1;
|
||||
}
|
||||
|
||||
my @logins;
|
||||
for my $query (@queries) {
|
||||
|
||||
my $users = match(
|
||||
$query, # match string
|
||||
$limit, # match limit
|
||||
1 # exclude_disabled
|
||||
);
|
||||
|
||||
# skip confirmation for exact matches
|
||||
if ((scalar(@{$users}) == 1)
|
||||
&& (lc(@{$users}[0]->login) eq lc($query)))
|
||||
|
||||
{
|
||||
$cgi->append(-name=>$field,
|
||||
-values=>[@{$users}[0]->login]);
|
||||
|
||||
next;
|
||||
}
|
||||
|
||||
$matches->{$field}->{$query}->{'users'} = $users;
|
||||
$matches->{$field}->{$query}->{'status'} = 'success';
|
||||
|
||||
# here is where it checks for multiple matches
|
||||
|
||||
if (scalar(@{$users}) == 1) { # exactly one match
|
||||
push(@logins, @{$users}[0]->login);
|
||||
|
||||
$cgi->append(-name=>$field,
|
||||
-values=>[@{$users}[0]->login]);
|
||||
# skip confirmation for exact matches
|
||||
next if (lc(@{$users}[0]->login) eq lc($query));
|
||||
|
||||
$matches->{$field}->{$query}->{'status'} = 'success';
|
||||
$need_confirm = 1 if $params->{'confirmuniqueusermatch'};
|
||||
|
||||
}
|
||||
|
@ -1391,6 +1342,7 @@ sub match_field {
|
|||
&& ($params->{'maxusermatches'} != 1)) {
|
||||
$need_confirm = 1;
|
||||
$match_multiple = 1;
|
||||
push(@non_conclusive_fields, $field);
|
||||
|
||||
if (($params->{'maxusermatches'})
|
||||
&& (scalar(@{$users}) > $params->{'maxusermatches'}))
|
||||
|
@ -1398,23 +1350,31 @@ sub match_field {
|
|||
$matches->{$field}->{$query}->{'status'} = 'trunc';
|
||||
pop @{$users}; # take the last one out
|
||||
}
|
||||
else {
|
||||
$matches->{$field}->{$query}->{'status'} = 'success';
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
# everything else fails
|
||||
$matchsuccess = 0; # fail
|
||||
push(@non_conclusive_fields, $field);
|
||||
$matches->{$field}->{$query}->{'status'} = 'fail';
|
||||
$need_confirm = 1; # confirmation screen shows failures
|
||||
}
|
||||
|
||||
$matches->{$field}->{$query}->{'users'} = $users;
|
||||
}
|
||||
# Above, we deleted the field before adding matches. If no match
|
||||
# or more than one match has been found for a field expecting only
|
||||
# one match (type eq "single"), we set it back to '' so
|
||||
# that the caller of this function can still check whether this
|
||||
|
||||
# If no match or more than one match has been found for a field
|
||||
# expecting only one match (type eq "single"), we set it back to ''
|
||||
# so that the caller of this function can still check whether this
|
||||
# field was defined or not (and it was if we came here).
|
||||
if (!defined $cgi->param($field)
|
||||
&& $fields->{$field}->{'type'} eq 'single') {
|
||||
$cgi->param($field, '');
|
||||
if ($fields->{$field}->{'type'} eq 'single') {
|
||||
$data->{$field} = $logins[0] || '';
|
||||
}
|
||||
elsif (scalar @logins) {
|
||||
$data->{$field} = \@logins;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1430,22 +1390,24 @@ sub match_field {
|
|||
}
|
||||
|
||||
# Skip confirmation if we were told to, or if we don't need to confirm.
|
||||
return $retval if ($behavior == MATCH_SKIP_CONFIRM || !$need_confirm);
|
||||
if ($behavior == MATCH_SKIP_CONFIRM || !$need_confirm) {
|
||||
return wantarray ? ($retval, \@non_conclusive_fields) : $retval;
|
||||
}
|
||||
|
||||
my $template = Bugzilla->template;
|
||||
my $cgi = Bugzilla->cgi;
|
||||
my $vars = {};
|
||||
|
||||
$vars->{'script'} = Bugzilla->cgi->url(-relative => 1); # for self-referencing URLs
|
||||
$vars->{'script'} = $cgi->url(-relative => 1); # for self-referencing URLs
|
||||
$vars->{'fields'} = $fields; # fields being matched
|
||||
$vars->{'matches'} = $matches; # matches that were made
|
||||
$vars->{'matchsuccess'} = $matchsuccess; # continue or fail
|
||||
$vars->{'matchmultiple'} = $match_multiple;
|
||||
|
||||
print Bugzilla->cgi->header();
|
||||
print $cgi->header();
|
||||
|
||||
$template->process("global/confirm-user-match.html.tmpl", $vars)
|
||||
|| ThrowTemplateError($template->error());
|
||||
|
||||
exit;
|
||||
|
||||
}
|
||||
|
@ -1493,7 +1455,6 @@ sub wants_bug_mail {
|
|||
my $self = shift;
|
||||
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.
|
||||
|
@ -1531,17 +1492,17 @@ sub wants_bug_mail {
|
|||
# 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.
|
||||
# You role is new if the bug itself is.
|
||||
# Only makes sense for the assignee, QA contact and the CC list.
|
||||
if ($relationship == REL_ASSIGNEE
|
||||
|| $relationship == REL_QA
|
||||
|| $relationship == REL_CC)
|
||||
{
|
||||
$events{+EVT_ADDED_REMOVED} = 1;
|
||||
}
|
||||
{
|
||||
$events{+EVT_ADDED_REMOVED} = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if ($comments_concatenated =~ /Created an attachment \(/) {
|
||||
if (grep { $_->type == CMT_ATTACHMENT_CREATED } @$comments) {
|
||||
$events{+EVT_ATTACHMENT} = 1;
|
||||
}
|
||||
elsif (defined($$comments[0])) {
|
||||
|
@ -1664,6 +1625,19 @@ sub is_timetracker {
|
|||
return $self->{'is_timetracker'};
|
||||
}
|
||||
|
||||
sub wants_worktime_reminder {
|
||||
my $self = shift;
|
||||
my ($new_bug) = @_;
|
||||
my $set = 'remind_me_about_worktime';
|
||||
$new_bug and $set .= '_newbug';
|
||||
return $self &&
|
||||
$self->is_timetracker && # user is timetracker
|
||||
$self->settings->{$set} && # user wants to be reminded about worktime
|
||||
$self->settings->{$set}->{value} &&
|
||||
lc $self->settings->{$set}->{value} ne 'off'
|
||||
? 1 : 0;
|
||||
}
|
||||
|
||||
sub get_userlist {
|
||||
my $self = shift;
|
||||
|
||||
|
@ -1751,6 +1725,54 @@ sub create {
|
|||
return $user;
|
||||
}
|
||||
|
||||
###########################
|
||||
# Account Lockout Methods #
|
||||
###########################
|
||||
|
||||
sub account_is_locked_out {
|
||||
my $self = shift;
|
||||
my $login_failures = scalar @{ $self->account_ip_login_failures };
|
||||
return $login_failures >= MAX_LOGIN_ATTEMPTS ? 1 : 0;
|
||||
}
|
||||
|
||||
sub note_login_failure {
|
||||
my $self = shift;
|
||||
my $ip_addr = remote_ip();
|
||||
trick_taint($ip_addr);
|
||||
Bugzilla->dbh->do("INSERT INTO login_failure (user_id, ip_addr, login_time)
|
||||
VALUES (?, ?, LOCALTIMESTAMP(0))",
|
||||
undef, $self->id, $ip_addr);
|
||||
delete $self->{account_ip_login_failures};
|
||||
}
|
||||
|
||||
sub clear_login_failures {
|
||||
my $self = shift;
|
||||
my $ip_addr = remote_ip();
|
||||
trick_taint($ip_addr);
|
||||
Bugzilla->dbh->do(
|
||||
'DELETE FROM login_failure WHERE user_id = ? AND ip_addr = ?',
|
||||
undef, $self->id, $ip_addr);
|
||||
delete $self->{account_ip_login_failures};
|
||||
}
|
||||
|
||||
sub account_ip_login_failures {
|
||||
my $self = shift;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $time = $dbh->sql_interval(LOGIN_LOCKOUT_INTERVAL, 'MINUTE');
|
||||
my $ip_addr = remote_ip();
|
||||
trick_taint($ip_addr);
|
||||
$self->{account_ip_login_failures} ||= Bugzilla->dbh->selectall_arrayref(
|
||||
"SELECT login_time, ip_addr, user_id FROM login_failure
|
||||
WHERE user_id = ? AND login_time > LOCALTIMESTAMP(0) - $time
|
||||
AND ip_addr = ?
|
||||
ORDER BY login_time", {Slice => {}}, $self->id, $ip_addr);
|
||||
return $self->{account_ip_login_failures};
|
||||
}
|
||||
|
||||
###############
|
||||
# Subroutines #
|
||||
###############
|
||||
|
||||
sub is_available_username {
|
||||
my ($username, $old_username) = @_;
|
||||
|
||||
|
@ -1776,7 +1798,7 @@ sub is_available_username {
|
|||
$dbh->sql_position(q{':'}, 'eventdata') . "- 1)) = ?)
|
||||
OR (tokentype = 'emailnew'
|
||||
AND SUBSTRING(eventdata, (" .
|
||||
$dbh->sql_position(q{':'}, 'eventdata') . "+ 1)) = ?)",
|
||||
$dbh->sql_position(q{':'}, 'eventdata') . "+ 1), LENGTH(eventdata)) = ?)",
|
||||
undef, ($username, $username));
|
||||
|
||||
if ($eventdata) {
|
||||
|
@ -1801,10 +1823,10 @@ sub login_to_id
|
|||
}
|
||||
else
|
||||
{
|
||||
my $dbh = Bugzilla->dbh;
|
||||
# No need to validate $login -- it will be used by the following SELECT
|
||||
# statement only, so it's safe to simply trick_taint.
|
||||
trick_taint($login);
|
||||
my $dbh = Bugzilla->dbh;
|
||||
# No need to validate $login -- it will be used by the following SELECT
|
||||
# statement only, so it's safe to simply trick_taint.
|
||||
trick_taint($login);
|
||||
$cache->{$login} = $user_id = $dbh->selectrow_array(
|
||||
"SELECT userid FROM profiles WHERE " . $dbh->sql_istrcmp('login_name', '?'),
|
||||
undef, $login
|
||||
|
@ -1835,8 +1857,6 @@ sub validate_password {
|
|||
|
||||
if (length($password) < USER_PASSWORD_MIN_LENGTH) {
|
||||
ThrowUserError('password_too_short');
|
||||
} elsif (length($password) > USER_PASSWORD_MAX_LENGTH) {
|
||||
ThrowUserError('password_too_long');
|
||||
} elsif ((defined $matchpassword) && ($password ne $matchpassword)) {
|
||||
ThrowUserError('passwords_dont_match');
|
||||
}
|
||||
|
@ -1911,6 +1931,18 @@ confirmation screen.
|
|||
|
||||
=head1 METHODS
|
||||
|
||||
=head2 Constructors
|
||||
|
||||
=over
|
||||
|
||||
=item C<super_user>
|
||||
|
||||
Returns a user who is in all groups, but who does not really exist in the
|
||||
database. Used for non-web scripts like L<checksetup> that need to make
|
||||
database changes and so on.
|
||||
|
||||
=back
|
||||
|
||||
=head2 Saved and Shared Queries
|
||||
|
||||
=over
|
||||
|
@ -1945,6 +1977,29 @@ groups.
|
|||
|
||||
=back
|
||||
|
||||
=head2 Account Lockout
|
||||
|
||||
=over
|
||||
|
||||
=item C<account_is_locked_out>
|
||||
|
||||
Returns C<1> if the account has failed to log in too many times recently,
|
||||
and thus is locked out for a period of time. Returns C<0> otherwise.
|
||||
|
||||
=item C<account_ip_login_failures>
|
||||
|
||||
Returns an arrayref of hashrefs, that contains information about the recent
|
||||
times that this account has failed to log in from the current remote IP.
|
||||
The hashes contain C<ip_addr>, C<login_time>, and C<user_id>.
|
||||
|
||||
=item C<note_login_failure>
|
||||
|
||||
This notes that this account has failed to log in, and stores the fact
|
||||
in the database. The storing happens immediately, it does not wait for
|
||||
you to call C<update>.
|
||||
|
||||
=back
|
||||
|
||||
=head2 Other Methods
|
||||
|
||||
=over
|
||||
|
@ -2080,6 +2135,12 @@ care of by the constructor. However, when updating the email address, the
|
|||
user may be placed into different groups, based on a new email regexp. This
|
||||
method should be called in such a case to force reresolution of these groups.
|
||||
|
||||
=item C<clear_product_cache>
|
||||
|
||||
Clears the stored values for L</get_selectable_products>,
|
||||
L</get_enterable_products>, etc. so that their data will be read from
|
||||
the database again. Used mostly by L<Bugzilla::Product>.
|
||||
|
||||
=item C<get_selectable_products>
|
||||
|
||||
Description: Returns all products the user is allowed to access. This list
|
||||
|
@ -2127,10 +2188,11 @@ method should be called in such a case to force reresolution of these groups.
|
|||
|
||||
Returns: an array of product objects.
|
||||
|
||||
=item C<can_access_product(product_name)>
|
||||
=item C<can_access_product($product)>
|
||||
|
||||
Returns 1 if the user can search or enter bugs into the specified product,
|
||||
and 0 if the user should not be aware of the existence of the product.
|
||||
Returns 1 if the user can search or enter bugs into the specified product
|
||||
(either a L<Bugzilla::Product> or a product name), and 0 if the user should
|
||||
not be aware of the existence of the product.
|
||||
|
||||
=item C<get_accessible_products>
|
||||
|
||||
|
@ -2307,6 +2369,49 @@ Untaints C<$passwd1> if successful.
|
|||
If a second password is passed in, this function also verifies that
|
||||
the two passwords match.
|
||||
|
||||
=item C<match_field($data, $fields, $behavior)>
|
||||
|
||||
=over
|
||||
|
||||
=item B<Description>
|
||||
|
||||
Wrapper for the C<match()> function.
|
||||
|
||||
=item B<Params>
|
||||
|
||||
=over
|
||||
|
||||
=item C<$fields> - A hashref with field names as keys and a hash as values.
|
||||
Each hash is of the form { 'type' => 'single|multi' }, which specifies
|
||||
whether the field can take a single login name only or several.
|
||||
|
||||
=item C<$data> (optional) - A hashref with field names as keys and field values
|
||||
as values. If undefined, C<Bugzilla-E<gt>input_params> is used.
|
||||
|
||||
=item C<$behavior> (optional) - If set to C<MATCH_SKIP_CONFIRM>, no confirmation
|
||||
screen is displayed. In that case, the fields which don't match a unique user
|
||||
are left undefined. If not set, a confirmation screen is displayed if at
|
||||
least one field doesn't match any login name or match more than one.
|
||||
|
||||
=back
|
||||
|
||||
=item B<Returns>
|
||||
|
||||
If the third parameter is set to C<MATCH_SKIP_CONFIRM>, the function returns
|
||||
either C<USER_MATCH_SUCCESS> if all fields can be set unambiguously,
|
||||
C<USER_MATCH_FAILED> if at least one field doesn't match any user account,
|
||||
or C<USER_MATCH_MULTIPLE> if some fields match more than one user account.
|
||||
|
||||
If the third parameter is not set, then if all fields could be set
|
||||
unambiguously, nothing is returned, else a confirmation page is displayed.
|
||||
|
||||
=item B<Note>
|
||||
|
||||
This function must be called early in a script, before anything external
|
||||
is done with the data.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
||||
=head1 SEE ALSO
|
||||
|
|
337
Bugzilla/Util.pm
337
Bugzilla/Util.pm
|
@ -37,17 +37,18 @@ use base qw(Exporter);
|
|||
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
|
||||
i_am_cgi correct_urlbase remote_ip
|
||||
lsearch do_ssl_redirect_if_required use_attachbase
|
||||
diff_arrays
|
||||
trim wrap_hard wrap_comment find_wrap_point
|
||||
format_time format_time_decimal validate_date
|
||||
validate_time
|
||||
validate_time datetime_from
|
||||
file_mod_time is_7bit_clean
|
||||
bz_crypt generate_random_password
|
||||
validate_email_syntax clean_text
|
||||
get_fielddesc get_term
|
||||
get_text disable_utf8 stem_text);
|
||||
stem_text
|
||||
intersect
|
||||
get_text template_var disable_utf8);
|
||||
|
||||
use Bugzilla::Constants;
|
||||
|
||||
|
@ -57,7 +58,9 @@ use DateTime;
|
|||
use DateTime::TimeZone;
|
||||
use Digest;
|
||||
use Email::Address;
|
||||
use List::Util qw(first);
|
||||
use Scalar::Util qw(tainted);
|
||||
use Template::Filters;
|
||||
use Text::Wrap;
|
||||
use Text::TabularDisplay::Utf8;
|
||||
|
||||
|
@ -78,26 +81,48 @@ sub trick_taint_copy {
|
|||
|
||||
sub detaint_natural {
|
||||
my $match = $_[0] =~ /^(\d+)$/;
|
||||
$_[0] = $match ? $1 : undef;
|
||||
$_[0] = $match ? int($1) : undef;
|
||||
return (defined($_[0]));
|
||||
}
|
||||
|
||||
sub detaint_signed {
|
||||
my $match = $_[0] =~ /^([-+]?\d+)$/;
|
||||
$_[0] = $match ? $1 : undef;
|
||||
# Remove any leading plus sign.
|
||||
if (defined($_[0]) && $_[0] =~ /^\+(\d+)$/) {
|
||||
$_[0] = $1;
|
||||
}
|
||||
# The "int()" call removes any leading plus sign.
|
||||
$_[0] = $match ? int($1) : undef;
|
||||
return (defined($_[0]));
|
||||
}
|
||||
|
||||
# Bug 120030: Override html filter to obscure the '@' in user
|
||||
# visible strings.
|
||||
# Bug 319331: Handle BiDi disruptions.
|
||||
sub html_quote {
|
||||
my ($var) = (@_);
|
||||
$var =~ s/\&/\&/g;
|
||||
$var =~ s/</\</g;
|
||||
$var =~ s/>/\>/g;
|
||||
$var =~ s/\"/\"/g;
|
||||
my ($var) = Template::Filters::html_filter(@_);
|
||||
# Obscure '@'.
|
||||
$var =~ s/\@/\@/g;
|
||||
if (Bugzilla->params->{'utf8'}) {
|
||||
# Remove the following characters because they're
|
||||
# influencing BiDi:
|
||||
# --------------------------------------------------------
|
||||
# |Code |Name |UTF-8 representation|
|
||||
# |------|--------------------------|--------------------|
|
||||
# |U+202a|Left-To-Right Embedding |0xe2 0x80 0xaa |
|
||||
# |U+202b|Right-To-Left Embedding |0xe2 0x80 0xab |
|
||||
# |U+202c|Pop Directional Formatting|0xe2 0x80 0xac |
|
||||
# |U+202d|Left-To-Right Override |0xe2 0x80 0xad |
|
||||
# |U+202e|Right-To-Left Override |0xe2 0x80 0xae |
|
||||
# --------------------------------------------------------
|
||||
#
|
||||
# The following are characters influencing BiDi, too, but
|
||||
# they can be spared from filtering because they don't
|
||||
# influence more than one character right or left:
|
||||
# --------------------------------------------------------
|
||||
# |Code |Name |UTF-8 representation|
|
||||
# |------|--------------------------|--------------------|
|
||||
# |U+200e|Left-To-Right Mark |0xe2 0x80 0x8e |
|
||||
# |U+200f|Right-To-Left Mark |0xe2 0x80 0x8f |
|
||||
# --------------------------------------------------------
|
||||
$var =~ s/[\x{202a}-\x{202e}]//g;
|
||||
}
|
||||
return $var;
|
||||
}
|
||||
|
||||
|
@ -109,12 +134,7 @@ sub html_light_quote {
|
|||
dfn samp kbd big small sub sup tt dd dt dl ul li ol
|
||||
fieldset legend);
|
||||
|
||||
# Are HTML::Scrubber and HTML::Parser installed?
|
||||
eval { require HTML::Scrubber;
|
||||
require HTML::Parser;
|
||||
};
|
||||
|
||||
if ($@) { # Package(s) not installed.
|
||||
if (!Bugzilla->feature('html_desc')) {
|
||||
my $safe = join('|', @allow);
|
||||
my $chr = chr(1);
|
||||
|
||||
|
@ -129,7 +149,7 @@ sub html_light_quote {
|
|||
$text =~ s#$chr($safe)$chr#<$1>#go;
|
||||
return $text;
|
||||
}
|
||||
else { # Packages installed.
|
||||
else {
|
||||
# We can be less restrictive. We can accept elements with attributes.
|
||||
push(@allow, qw(a blockquote q span));
|
||||
|
||||
|
@ -207,7 +227,7 @@ sub url_quote {
|
|||
|
||||
sub css_class_quote {
|
||||
my ($toencode) = (@_);
|
||||
$toencode =~ s/ /_/g;
|
||||
$toencode =~ s#[ /]#_#g;
|
||||
$toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("&#x%x;",ord($1))/eg;
|
||||
return $toencode;
|
||||
}
|
||||
|
@ -249,66 +269,51 @@ sub i_am_cgi {
|
|||
return exists $ENV{'SERVER_SOFTWARE'} ? 1 : 0;
|
||||
}
|
||||
|
||||
sub ssl_require_redirect {
|
||||
my $method = shift;
|
||||
# This exists as a separate function from Bugzilla::CGI::redirect_to_https
|
||||
# because we don't want to create a CGI object during XML-RPC calls
|
||||
# (doing so can mess up XML-RPC).
|
||||
sub do_ssl_redirect_if_required {
|
||||
return if !i_am_cgi();
|
||||
return if !Bugzilla->params->{'ssl_redirect'};
|
||||
|
||||
# If currently not in a protected SSL
|
||||
# connection, determine if a redirection is
|
||||
# needed based on value in Bugzilla->params->{ssl}.
|
||||
# If we are already in a protected connection or
|
||||
# sslbase is not set then no action is required.
|
||||
if (uc($ENV{'HTTPS'}) ne 'ON'
|
||||
&& $ENV{'SERVER_PORT'} != 443
|
||||
&& Bugzilla->params->{'sslbase'} ne '')
|
||||
{
|
||||
# System is configured to never require SSL
|
||||
# so no redirection is needed.
|
||||
return 0
|
||||
if Bugzilla->params->{'ssl'} eq 'never';
|
||||
my $sslbase = Bugzilla->params->{'sslbase'};
|
||||
|
||||
# System is configured to always require a SSL
|
||||
# connection so we need to redirect.
|
||||
return 1
|
||||
if Bugzilla->params->{'ssl'} eq 'always';
|
||||
|
||||
# System is configured such that if we are inside
|
||||
# of an authenticated session, then we need to make
|
||||
# sure that all of the connections are over SSL. Non
|
||||
# authenticated sessions SSL is not mandatory.
|
||||
# For XMLRPC requests, if the method is User.login
|
||||
# then we always want the connection to be over SSL
|
||||
# if the system is configured for authenticated
|
||||
# sessions since the user's username and password
|
||||
# will be passed before the user is logged in.
|
||||
return 1
|
||||
if Bugzilla->params->{'ssl'} eq 'authenticated sessions'
|
||||
&& (Bugzilla->user->id
|
||||
|| (defined $method && $method eq 'User.login'));
|
||||
}
|
||||
|
||||
return 0;
|
||||
# If we're already running under SSL, never redirect.
|
||||
return if uc($ENV{HTTPS} || '') eq 'ON';
|
||||
# Never redirect if there isn't an sslbase.
|
||||
return if !$sslbase;
|
||||
Bugzilla->cgi->redirect_to_https();
|
||||
}
|
||||
|
||||
sub correct_urlbase
|
||||
{
|
||||
sub correct_urlbase {
|
||||
if ($Bugzilla::CustisLocalBugzillas::HackIntoCorrectUrlbase)
|
||||
{
|
||||
# Отправка почты заказчикам со ссылками на свои багзиллы
|
||||
return $Bugzilla::CustisLocalBugzillas::HackIntoCorrectUrlbase;
|
||||
}
|
||||
my $ssl = Bugzilla->params->{'ssl'};
|
||||
return Bugzilla->params->{'urlbase'} if $ssl eq 'never';
|
||||
|
||||
my $ssl = Bugzilla->params->{'ssl_redirect'};
|
||||
my $urlbase = Bugzilla->params->{'urlbase'};
|
||||
my $sslbase = Bugzilla->params->{'sslbase'};
|
||||
if ($sslbase) {
|
||||
return $sslbase if $ssl eq 'always';
|
||||
# Authenticated Sessions
|
||||
return $sslbase if Bugzilla->user->id;
|
||||
}
|
||||
|
||||
# Set to "authenticated sessions" but nobody's logged in, or
|
||||
# sslbase isn't set.
|
||||
return Bugzilla->params->{'urlbase'};
|
||||
if (!$sslbase) {
|
||||
return $urlbase;
|
||||
}
|
||||
elsif ($ssl) {
|
||||
return $sslbase;
|
||||
}
|
||||
else {
|
||||
# Return what the user currently uses.
|
||||
return (uc($ENV{HTTPS} || '') eq 'ON') ? $sslbase : $urlbase;
|
||||
}
|
||||
}
|
||||
|
||||
sub remote_ip {
|
||||
my $ip = $ENV{'REMOTE_ADDR'} || '127.0.0.1';
|
||||
my @proxies = split(/[\s,]+/, Bugzilla->params->{'inbound_proxies'});
|
||||
if (first { $_ eq $ip } @proxies) {
|
||||
$ip = $ENV{'HTTP_X_FORWARDED_FOR'} if $ENV{'HTTP_X_FORWARDED_FOR'};
|
||||
}
|
||||
return $ip;
|
||||
}
|
||||
|
||||
sub use_attachbase {
|
||||
|
@ -391,7 +396,7 @@ sub wrap_comment
|
|||
}
|
||||
if (length $line)
|
||||
{
|
||||
# If the line starts with ">", don't wrap it. Otherwise, wrap.
|
||||
# If the line starts with ">", don't wrap it. Otherwise, wrap.
|
||||
if ($line !~ /^>/so)
|
||||
{
|
||||
my $n = scalar($line =~ s/(\t+)/$1/gso);
|
||||
|
@ -401,15 +406,15 @@ sub wrap_comment
|
|||
$table = Text::TabularDisplay::Utf8->new;
|
||||
$table->add(split /\t+/, $line);
|
||||
next;
|
||||
}
|
||||
}
|
||||
unless ($line =~ /^[│─┌┐└┘├┴┬┤┼].*[│─┌┐└┘├┴┬┤┼]$/iso)
|
||||
{
|
||||
$line =~ s/\t/ /gso;
|
||||
while (length($line) > $cols && $line =~ s/$re//)
|
||||
{
|
||||
$wrappedcomment .= $1 . "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$wrappedcomment .= $line . "\n" if length $line;
|
||||
}
|
||||
|
@ -458,7 +463,9 @@ sub format_time {
|
|||
|
||||
# If $format is not set, try to guess the correct date format.
|
||||
if (!$format) {
|
||||
if ($date =~ m/^(\d{4})[-\.](\d{2})[-\.](\d{2}) (\d{2}):(\d{2})(:(\d{2}))?$/) {
|
||||
if (!ref $date
|
||||
&& $date =~ /^(\d{4})[-\.](\d{2})[-\.](\d{2}) (\d{2}):(\d{2})(:(\d{2}))?$/)
|
||||
{
|
||||
my $sec = $7;
|
||||
if (defined $sec) {
|
||||
$format = "%Y-%m-%d %T %Z";
|
||||
|
@ -471,44 +478,52 @@ sub format_time {
|
|||
}
|
||||
}
|
||||
|
||||
# strptime($date) returns an empty array if $date has an invalid date format.
|
||||
my $dt = ref $date ? $date : datetime_from($date, $timezone);
|
||||
$date = defined $dt ? $dt->strftime($format) : '';
|
||||
return trim($date);
|
||||
}
|
||||
|
||||
sub datetime_from {
|
||||
my ($date, $timezone) = @_;
|
||||
|
||||
# In the database, this is the "0" date.
|
||||
return undef if $date =~ /^0000/;
|
||||
|
||||
# strptime($date) returns an empty array if $date has an invalid
|
||||
# date format.
|
||||
my @time = strptime($date);
|
||||
|
||||
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.
|
||||
# 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;
|
||||
return undef if !@time;
|
||||
|
||||
# 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});
|
||||
# strptime() counts years from 1900, and months from 0 (January).
|
||||
# We have to fix both values.
|
||||
my $dt = DateTime->new({
|
||||
year => $time[5] + 1900,
|
||||
month => $time[4] + 1,
|
||||
day => $time[3],
|
||||
hour => $time[2],
|
||||
minute => $time[1],
|
||||
# DateTime doesn't like fractional seconds.
|
||||
# Also, sometimes seconds are undef.
|
||||
second => int($time[0] || 0),
|
||||
# If a timezone was specified, use it. Otherwise, use the
|
||||
# local timezone.
|
||||
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!
|
||||
$date = '';
|
||||
}
|
||||
return trim($date);
|
||||
# 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);
|
||||
return $dt;
|
||||
}
|
||||
|
||||
sub format_time_decimal {
|
||||
|
@ -557,12 +572,12 @@ sub bz_crypt {
|
|||
|
||||
my $crypted_password;
|
||||
if (!$algorithm) {
|
||||
# Wide characters cause crypt to die
|
||||
if (Bugzilla->params->{'utf8'}) {
|
||||
utf8::encode($password) if utf8::is_utf8($password);
|
||||
}
|
||||
# Wide characters cause crypt to die
|
||||
if (Bugzilla->params->{'utf8'}) {
|
||||
utf8::encode($password) if utf8::is_utf8($password);
|
||||
}
|
||||
|
||||
# Crypt the password.
|
||||
# Crypt the password.
|
||||
$crypted_password = crypt($password, $salt);
|
||||
|
||||
# HACK: Perl has bug where returned crypted password is considered
|
||||
|
@ -662,23 +677,9 @@ sub load_cached_fielddescs_template
|
|||
# что приводит к ужасной производительности. например, на баге с 703
|
||||
# комментами в 10-15 раз ухудшение по сравнению с Bugzilla 2.x.
|
||||
# Избавляемся от этого.
|
||||
sub get_term
|
||||
{
|
||||
my ($term) = @_;
|
||||
my $tt = load_cached_fielddescs_template();
|
||||
return $tt->stash->get(['terms', 0, $term, 0]);
|
||||
}
|
||||
|
||||
sub get_fielddesc
|
||||
{
|
||||
my ($field) = @_;
|
||||
my $tt = load_cached_fielddescs_template();
|
||||
return $tt->stash->get(['field_descs', 0, $field, 0]);
|
||||
}
|
||||
|
||||
# CustIS Bug 40933 ФАКМОЙМОЗГ! ВРОТМНЕНОГИ! КТО ТАК ПИШЕТ?!!!!
|
||||
# ВОТ он, антипаттерн разработки на TT, ведущий к тормозам...
|
||||
# ALSO CustIS Bug3 52322
|
||||
# ALSO CustIS Bug 52322
|
||||
sub get_text {
|
||||
my ($name, $vars) = @_;
|
||||
my $template = Bugzilla->template_inner;
|
||||
|
@ -694,25 +695,24 @@ sub get_text {
|
|||
return $message;
|
||||
}
|
||||
|
||||
sub get_netaddr {
|
||||
my $ipaddr = shift;
|
||||
|
||||
# Check for a valid IPv4 addr which we know how to parse
|
||||
if (!$ipaddr || $ipaddr !~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) {
|
||||
return undef;
|
||||
sub template_var {
|
||||
my $name = shift;
|
||||
my $cache = Bugzilla->request_cache->{util_template_var} ||= {};
|
||||
my $template = Bugzilla->template_inner;
|
||||
my $lang = Bugzilla->request_cache->{language};
|
||||
return $cache->{$lang}->{$name} if defined $cache->{$lang};
|
||||
my %vars;
|
||||
# Note: If we suddenly start needing a lot of template_var variables,
|
||||
# they should move into their own template, not field-descs.
|
||||
my $result = $template->process('global/field-descs.none.tmpl',
|
||||
{ vars => \%vars, in_template_var => 1 });
|
||||
# Bugzilla::Error can't be "use"d in Bugzilla::Util.
|
||||
if (!$result) {
|
||||
require Bugzilla::Error;
|
||||
Bugzilla::Error::ThrowTemplateError($template->error);
|
||||
}
|
||||
|
||||
my $addr = unpack("N", pack("CCCC", split(/\./, $ipaddr)));
|
||||
|
||||
my $maskbits = Bugzilla->params->{'loginnetmask'};
|
||||
|
||||
# Make Bugzilla ignore the IP address if loginnetmask is set to 0
|
||||
return "0.0.0.0" if ($maskbits == 0);
|
||||
|
||||
$addr >>= (32-$maskbits);
|
||||
|
||||
$addr <<= (32-$maskbits);
|
||||
return join(".", unpack("CCCC", pack("N", $addr)));
|
||||
$cache->{$lang} = \%vars;
|
||||
return $vars{$name};
|
||||
}
|
||||
|
||||
sub disable_utf8 {
|
||||
|
@ -750,6 +750,18 @@ sub stem_text
|
|||
return join '', @$text;
|
||||
}
|
||||
|
||||
sub intersect
|
||||
{
|
||||
my $values = shift;
|
||||
my %chk;
|
||||
while (my $next = shift)
|
||||
{
|
||||
%chk = map { $_ => 1 } @$next;
|
||||
@$values = grep { $chk{$_} } @$values;
|
||||
}
|
||||
return $values;
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
@ -778,7 +790,6 @@ Bugzilla::Util - Generic utility functions for bugzilla
|
|||
|
||||
# Functions that tell you about your environment
|
||||
my $is_cgi = i_am_cgi();
|
||||
my $net_addr = get_netaddr($ip_addr);
|
||||
my $urlbase = correct_urlbase();
|
||||
|
||||
# Functions for searching
|
||||
|
@ -793,6 +804,7 @@ Bugzilla::Util - Generic utility functions for bugzilla
|
|||
|
||||
# Functions for formatting time
|
||||
format_time($time);
|
||||
datetime_from($time, $timezone);
|
||||
|
||||
# Functions for dealing with files
|
||||
$time = file_mod_time($filename);
|
||||
|
@ -860,8 +872,9 @@ be done in the template where possible.
|
|||
|
||||
=item C<html_quote($val)>
|
||||
|
||||
Returns a value quoted for use in HTML, with &, E<lt>, E<gt>, and E<34> being
|
||||
replaced with their appropriate HTML entities.
|
||||
Returns a value quoted for use in HTML, with &, E<lt>, E<gt>, E<34> and @ being
|
||||
replaced with their appropriate HTML entities. Also, Unicode BiDi controls are
|
||||
deleted.
|
||||
|
||||
=item C<html_light_quote($val)>
|
||||
|
||||
|
@ -876,7 +889,7 @@ Quotes characters so that they may be included as part of a url.
|
|||
=item C<css_class_quote($val)>
|
||||
|
||||
Quotes characters so that they may be used as CSS class names. Spaces
|
||||
are replaced by underscores.
|
||||
and forward slashes are replaced by underscores.
|
||||
|
||||
=item C<xml_quote($val)>
|
||||
|
||||
|
@ -908,17 +921,10 @@ Tells you whether or not you are being run as a CGI script in a web
|
|||
server. For example, it would return false if the caller is running
|
||||
in a command-line script.
|
||||
|
||||
=item C<get_netaddr($ipaddr)>
|
||||
|
||||
Given an IP address, this returns the associated network address, using
|
||||
C<Bugzilla->params->{'loginnetmask'}> as the netmask. This can be used
|
||||
to obtain data in order to restrict weak authentication methods (such as
|
||||
cookies) to only some addresses.
|
||||
|
||||
=item C<correct_urlbase()>
|
||||
|
||||
Returns either the C<sslbase> or C<urlbase> parameter, depending on the
|
||||
current setting for the C<ssl> parameter.
|
||||
current setting for the C<ssl_redirect> parameter.
|
||||
|
||||
=item C<use_attachbase()>
|
||||
|
||||
|
@ -1031,6 +1037,14 @@ A string.
|
|||
|
||||
=back
|
||||
|
||||
|
||||
=item C<template_var>
|
||||
|
||||
This is a method of getting the value of a variable from a template in
|
||||
Perl code. The available variables are in the C<global/field-descs.none.tmpl>
|
||||
template. Just pass in the name of the variable that you want the value of.
|
||||
|
||||
|
||||
=back
|
||||
|
||||
=head2 Formatting Time
|
||||
|
@ -1052,6 +1066,15 @@ This routine is mainly called from templates to filter dates, see
|
|||
Returns a number with 2 digit precision, unless the last digit is a 0. Then it
|
||||
returns only 1 digit precision.
|
||||
|
||||
=item C<datetime_from($time, $timezone)>
|
||||
|
||||
Returns a DateTime object given a date string. If the string is not in some
|
||||
valid date format that C<strptime> understands, we return C<undef>.
|
||||
|
||||
You can optionally specify a timezone for the returned date. If not
|
||||
specified, defaults to the currently-logged-in user's timezone, or
|
||||
the Bugzilla server's local timezone if there isn't a logged-in user.
|
||||
|
||||
=back
|
||||
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
#
|
||||
# Contributor(s): Tiago R. Mello <timello@async.com.br>
|
||||
# Max Kanat-Alexander <mkanat@bugzilla.org>
|
||||
# Frédéric Buclin <LpSolit@gmail.com>
|
||||
|
||||
use strict;
|
||||
|
||||
|
@ -32,6 +33,10 @@ use Bugzilla::Error;
|
|||
use constant DEFAULT_VERSION => 'unspecified';
|
||||
|
||||
use constant DB_TABLE => 'versions';
|
||||
use constant NAME_FIELD => 'value';
|
||||
# This is "id" because it has to be filled in and id is probably the fastest.
|
||||
# We do a custom sort in new_from_list below.
|
||||
use constant LIST_ORDER => 'id';
|
||||
|
||||
use constant DB_COLUMNS => qw(
|
||||
id
|
||||
|
@ -39,10 +44,26 @@ use constant DB_COLUMNS => qw(
|
|||
product_id
|
||||
);
|
||||
|
||||
use constant NAME_FIELD => 'value';
|
||||
# This is "id" because it has to be filled in and id is probably the fastest.
|
||||
# We do a custom sort in new_from_list below.
|
||||
use constant LIST_ORDER => 'id';
|
||||
use constant REQUIRED_CREATE_FIELDS => qw(
|
||||
name
|
||||
product
|
||||
);
|
||||
|
||||
use constant UPDATE_COLUMNS => qw(
|
||||
value
|
||||
);
|
||||
|
||||
use constant VALIDATORS => {
|
||||
product => \&_check_product,
|
||||
};
|
||||
|
||||
use constant UPDATE_VALIDATORS => {
|
||||
value => \&_check_value,
|
||||
};
|
||||
|
||||
################################
|
||||
# Methods
|
||||
################################
|
||||
|
||||
sub new {
|
||||
my $class = shift;
|
||||
|
@ -79,6 +100,18 @@ sub new_from_list {
|
|||
return [sort { vers_cmp(lc($a->name), lc($b->name)) } @$list];
|
||||
}
|
||||
|
||||
sub run_create_validators {
|
||||
my $class = shift;
|
||||
my $params = $class->SUPER::run_create_validators(@_);
|
||||
|
||||
my $product = delete $params->{product};
|
||||
$params->{product_id} = $product->id;
|
||||
$params->{value} = $class->_check_value($params->{name}, $product);
|
||||
delete $params->{name};
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
sub bug_count {
|
||||
my $self = shift;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
@ -92,6 +125,19 @@ sub bug_count {
|
|||
return $self->{'bug_count'};
|
||||
}
|
||||
|
||||
sub update {
|
||||
my $self = shift;
|
||||
my ($changes, $old_self) = $self->SUPER::update(@_);
|
||||
|
||||
if (exists $changes->{value}) {
|
||||
my $dbh = Bugzilla->dbh;
|
||||
$dbh->do('UPDATE bugs SET version = ?
|
||||
WHERE version = ? AND product_id = ?',
|
||||
undef, ($self->name, $old_self->name, $self->product_id));
|
||||
}
|
||||
return $changes;
|
||||
}
|
||||
|
||||
sub remove_from_db {
|
||||
my $self = shift;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
@ -101,78 +147,49 @@ sub remove_from_db {
|
|||
if ($self->bug_count) {
|
||||
ThrowUserError("version_has_bugs", { nb => $self->bug_count });
|
||||
}
|
||||
|
||||
$dbh->do(q{DELETE FROM versions WHERE product_id = ? AND value = ?},
|
||||
undef, ($self->product_id, $self->name));
|
||||
}
|
||||
|
||||
sub update {
|
||||
my $self = shift;
|
||||
my ($name, $product) = @_;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
$name || ThrowUserError('version_not_specified');
|
||||
|
||||
# Remove unprintable characters
|
||||
$name = clean_text($name);
|
||||
|
||||
return 0 if ($name eq $self->name);
|
||||
my $version = new Bugzilla::Version({ product => $product, name => $name });
|
||||
|
||||
if ($version) {
|
||||
ThrowUserError('version_already_exists',
|
||||
{'name' => $version->name,
|
||||
'product' => $product->name});
|
||||
}
|
||||
|
||||
trick_taint($name);
|
||||
$dbh->do("UPDATE bugs SET version = ?
|
||||
WHERE version = ? AND product_id = ?", undef,
|
||||
($name, $self->name, $self->product_id));
|
||||
|
||||
$dbh->do("UPDATE versions SET value = ?
|
||||
WHERE product_id = ? AND value = ?", undef,
|
||||
($name, $self->product_id, $self->name));
|
||||
|
||||
$self->{'value'} = $name;
|
||||
|
||||
return 1;
|
||||
$self->SUPER::remove_from_db();
|
||||
}
|
||||
|
||||
###############################
|
||||
##### Accessors ####
|
||||
###############################
|
||||
|
||||
sub name { return $_[0]->{'value'}; }
|
||||
sub product_id { return $_[0]->{'product_id'}; }
|
||||
|
||||
###############################
|
||||
##### Subroutines ###
|
||||
###############################
|
||||
sub product {
|
||||
my $self = shift;
|
||||
|
||||
sub create {
|
||||
my ($name, $product) = @_;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
require Bugzilla::Product;
|
||||
$self->{'product'} ||= new Bugzilla::Product($self->product_id);
|
||||
return $self->{'product'};
|
||||
}
|
||||
|
||||
# Cleanups and validity checks
|
||||
################################
|
||||
# Validators
|
||||
################################
|
||||
|
||||
sub set_name { $_[0]->set('value', $_[1]); }
|
||||
|
||||
sub _check_value {
|
||||
my ($invocant, $name, $product) = @_;
|
||||
|
||||
$name = trim($name);
|
||||
$name || ThrowUserError('version_blank_name');
|
||||
|
||||
# Remove unprintable characters
|
||||
$name = clean_text($name);
|
||||
|
||||
$product = $invocant->product if (ref $invocant);
|
||||
my $version = new Bugzilla::Version({ product => $product, name => $name });
|
||||
if ($version) {
|
||||
ThrowUserError('version_already_exists',
|
||||
{'name' => $version->name,
|
||||
'product' => $product->name});
|
||||
if ($version && (!ref $invocant || $version->id != $invocant->id)) {
|
||||
ThrowUserError('version_already_exists', { name => $version->name,
|
||||
product => $product->name });
|
||||
}
|
||||
return $name;
|
||||
}
|
||||
|
||||
# Add the new version
|
||||
trick_taint($name);
|
||||
$dbh->do(q{INSERT INTO versions (value, product_id)
|
||||
VALUES (?, ?)}, undef, ($name, $product->id));
|
||||
|
||||
return new Bugzilla::Version($dbh->bz_last_key('versions', 'id'));
|
||||
sub _check_product {
|
||||
my ($invocant, $product) = @_;
|
||||
return Bugzilla->user->check_can_admin_product($product->name);
|
||||
}
|
||||
|
||||
1;
|
||||
|
@ -187,37 +204,33 @@ Bugzilla::Version - Bugzilla product version class.
|
|||
|
||||
use Bugzilla::Version;
|
||||
|
||||
my $version = new Bugzilla::Version(1, 'version_value');
|
||||
my $version = new Bugzilla::Version({ name => $name, product => $product });
|
||||
|
||||
my $value = $version->name;
|
||||
my $product_id = $version->product_id;
|
||||
my $value = $version->value;
|
||||
my $product = $version->product;
|
||||
|
||||
my $version = Bugzilla::Version->create(
|
||||
{ name => $name, product => $product });
|
||||
|
||||
$version->set_name($new_name);
|
||||
$version->update();
|
||||
|
||||
$version->remove_from_db;
|
||||
|
||||
my $updated = $version->update($version_name, $product);
|
||||
|
||||
my $version = $hash_ref->{'version_value'};
|
||||
|
||||
my $version = Bugzilla::Version::create($version_name, $product);
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
Version.pm represents a Product Version object.
|
||||
Version.pm represents a Product Version 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::Version> are listed
|
||||
below.
|
||||
|
||||
=head1 METHODS
|
||||
|
||||
=over
|
||||
|
||||
=item C<new($product_id, $value)>
|
||||
|
||||
Description: The constructor is used to load an existing version
|
||||
by passing a product id and a version value.
|
||||
|
||||
Params: $product_id - Integer with a product id.
|
||||
$value - String with a version value.
|
||||
|
||||
Returns: A Bugzilla::Version object.
|
||||
|
||||
=item C<bug_count()>
|
||||
|
||||
Description: Returns the total of bugs that belong to the version.
|
||||
|
@ -226,38 +239,6 @@ Version.pm represents a Product Version object.
|
|||
|
||||
Returns: Integer with the number of bugs.
|
||||
|
||||
=item C<remove_from_db()>
|
||||
|
||||
Description: Removes the version from the database.
|
||||
|
||||
Params: none.
|
||||
|
||||
Retruns: none.
|
||||
|
||||
=item C<update($name, $product)>
|
||||
|
||||
Description: Update the value of the version.
|
||||
|
||||
Params: $name - String with the new version value.
|
||||
$product - Bugzilla::Product object the version belongs to.
|
||||
|
||||
Returns: An integer - 1 if the version has been updated, else 0.
|
||||
|
||||
=back
|
||||
|
||||
=head1 SUBROUTINES
|
||||
|
||||
=over
|
||||
|
||||
=item C<create($version_name, $product)>
|
||||
|
||||
Description: Create a new version for the given product.
|
||||
|
||||
Params: $version_name - String with a version value.
|
||||
$product - A Bugzilla::Product object.
|
||||
|
||||
Returns: A Bugzilla::Version object.
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
|
|
@ -14,46 +14,44 @@
|
|||
#
|
||||
# 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 Date::Parse;
|
||||
use Bugzilla::WebService::Server;
|
||||
|
||||
use XMLRPC::Lite;
|
||||
|
||||
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;
|
||||
}
|
||||
# Used by the JSON-RPC server to convert incoming date fields apprpriately.
|
||||
use constant DATE_FIELDS => {};
|
||||
|
||||
# For some methods, we shouldn't call Bugzilla->login before we call them
|
||||
use constant LOGIN_EXEMPT => { };
|
||||
|
||||
sub login_exempt {
|
||||
my ($class, $method) = @_;
|
||||
|
||||
return $class->LOGIN_EXEMPT->{$method};
|
||||
}
|
||||
|
||||
sub type {
|
||||
my ($self, $type, $value) = @_;
|
||||
if ($type eq 'dateTime') {
|
||||
$value = $self->datetime_format($value);
|
||||
$value = $self->datetime_format_outbound($value);
|
||||
}
|
||||
return XMLRPC::Data->type($type)->value($value);
|
||||
}
|
||||
|
||||
# This is the XML-RPC implementation, see the README in Bugzilla/WebService/.
|
||||
# Our "base" implementation is in Bugzilla::WebService::Server.
|
||||
sub datetime_format_outbound {
|
||||
my $self = shift;
|
||||
my $value = Bugzilla::WebService::Server->datetime_format_outbound(@_);
|
||||
# XML-RPC uses an ISO-8601 format that doesn't have any hyphens.
|
||||
$value =~ s/-//g;
|
||||
return $value;
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
@ -67,87 +65,126 @@ Bugzilla::WebService - The Web Service interface to Bugzilla
|
|||
This is the standard API for external programs that want to interact
|
||||
with Bugzilla. It provides various methods in various modules.
|
||||
|
||||
Currently the only method of accessing the API is via XML-RPC. The XML-RPC
|
||||
standard is described here: L<http://www.xmlrpc.com/spec>
|
||||
|
||||
The endpoint for Bugzilla WebServices is the C<xmlrpc.cgi> script in
|
||||
your Bugzilla installation. For example, if your Bugzilla is at
|
||||
C<bugzilla.yourdomain.com>, then your XML-RPC client would access the
|
||||
API via: C<http://bugzilla.yourdomain.com/xmlrpc.cgi>
|
||||
You can interact with this API via
|
||||
L<XML-RPC|Bugzilla::WebService::Server::XMLRPC> or
|
||||
L<JSON-RPC|Bugzilla::WebService::Server::JSONRPC>.
|
||||
|
||||
=head1 CALLING METHODS
|
||||
|
||||
Methods are called in the normal XML-RPC fashion. Bugzilla does not currently
|
||||
implement any extensions to the standard method of XML-RPC method calling.
|
||||
|
||||
Methods are grouped into "packages", like C<Bug> for
|
||||
L<Bugzilla::WebService::Bug>. So, for example,
|
||||
L<Bugzilla::WebService::Bug/get>, is called as C<Bug.get> in XML-RPC.
|
||||
L<Bugzilla::WebService::Bug/get>, is called as C<Bug.get>.
|
||||
|
||||
=head1 PARAMETERS
|
||||
|
||||
In addition to the standard parameter types like C<int>, C<string>, etc.,
|
||||
XML-RPC has two data structures, a C<< <struct> >> and an C<< <array> >>.
|
||||
The Bugzilla API takes the following various types of parameters:
|
||||
|
||||
=head2 Structs
|
||||
=over
|
||||
|
||||
In Perl, we call a C<< <struct> >> a "hash" or a "hashref". You may see
|
||||
us refer to it that way in the API documentation.
|
||||
=item C<int>
|
||||
|
||||
In example code, you will see the characters C<{> and C<}> used to represent
|
||||
the beginning and end of structs.
|
||||
Integer. May be null.
|
||||
|
||||
For example, here's a struct in XML-RPC:
|
||||
=item C<double>
|
||||
|
||||
<struct>
|
||||
<member>
|
||||
<name>fruit</name>
|
||||
<value><string>oranges</string></value>
|
||||
</member>
|
||||
<member>
|
||||
<name>vegetable</name>
|
||||
<value><string>lettuce</string></value>
|
||||
</member>
|
||||
</struct>
|
||||
A floating-point number. May be null.
|
||||
|
||||
In our example code in these API docs, that would look like:
|
||||
=item C<string>
|
||||
|
||||
{ fruit => 'oranges', vegetable => 'lettuce' }
|
||||
A string. May be null.
|
||||
|
||||
=head2 Arrays
|
||||
=item C<dateTime>
|
||||
|
||||
A date/time. Represented differently in different interfaces to this API.
|
||||
May be null.
|
||||
|
||||
=item C<boolean>
|
||||
|
||||
True or false.
|
||||
|
||||
=item C<array>
|
||||
|
||||
An array. There may be mixed types in an array.
|
||||
|
||||
In example code, you will see the characters C<[> and C<]> used to
|
||||
represent the beginning and end of arrays.
|
||||
|
||||
For example, here's an array in XML-RPC:
|
||||
|
||||
<array>
|
||||
<data>
|
||||
<value><i4>1</i4></value>
|
||||
<value><i4>2</i4></value>
|
||||
<value><i4>3</i4></value>
|
||||
</data>
|
||||
</array>
|
||||
|
||||
In our example code in these API docs, that would look like:
|
||||
In our example code in these API docs, an array that contains the numbers
|
||||
1, 2, and 3 would look like:
|
||||
|
||||
[1, 2, 3]
|
||||
|
||||
=item C<struct>
|
||||
|
||||
A mapping of keys to values. Called a "hash", "dict", or "map" in some
|
||||
other programming languages. We sometimes call this a "hash" in the API
|
||||
documentation.
|
||||
|
||||
The keys are strings, and the values can be any type.
|
||||
|
||||
In example code, you will see the characters C<{> and C<}> used to represent
|
||||
the beginning and end of structs.
|
||||
|
||||
For example, a struct with an "fruit" key whose value is "oranges",
|
||||
and a "vegetable" key whose value is "lettuce" would look like:
|
||||
|
||||
{ fruit => 'oranges', vegetable => 'lettuce' }
|
||||
|
||||
=back
|
||||
|
||||
=head2 How Bugzilla WebService Methods Take Parameters
|
||||
|
||||
B<All> Bugzilla WebServices functions take their parameters in
|
||||
a C<< <struct> >>. Another way of saying this would be: All functions
|
||||
take a single argument, a C<< <struct> >> that contains all parameters.
|
||||
The names of the parameters listed in the API docs for each function are
|
||||
the C<name> element for the struct C<member>s.
|
||||
B<All> Bugzilla WebService functions use I<named> parameters.
|
||||
The individual C<Bugzilla::WebService::Server> modules explain
|
||||
how this is implemented for those frontends.
|
||||
|
||||
=head1 LOGGING IN
|
||||
|
||||
There are various ways to log in:
|
||||
|
||||
=over
|
||||
|
||||
=item C<User.login>
|
||||
|
||||
You can use L<Bugzilla::WebService::User/login> to log in as a Bugzilla
|
||||
user. This issues standard HTTP cookies that you must then use in future
|
||||
calls, so your XML-RPC client must be capable of receiving and transmitting
|
||||
calls, so your client must be capable of receiving and transmitting
|
||||
cookies.
|
||||
|
||||
=item C<Bugzilla_login> and C<Bugzilla_password>
|
||||
|
||||
B<Added in Bugzilla 3.6>
|
||||
|
||||
You can specify C<Bugzilla_login> and C<Bugzilla_password> as arguments
|
||||
to any WebService method, and you will be logged in as that user if your
|
||||
credentials are correct. Here are the arguments you can specify to any
|
||||
WebService method to perform a login:
|
||||
|
||||
=over
|
||||
|
||||
=item C<Bugzilla_login> (string) - A user's login name.
|
||||
|
||||
=item C<Bugzilla_password> (string) - That user's password.
|
||||
|
||||
=item C<Bugzilla_restrictlogin> (boolean) - Optional. If true,
|
||||
then your login will only be valid for your IP address.
|
||||
|
||||
=item C<Bugzilla_rememberlogin> (boolean) - Optional. If true,
|
||||
then the cookie sent back to you with the method response will
|
||||
not expire.
|
||||
|
||||
=back
|
||||
|
||||
The C<Bugzilla_restrictlogin> and C<Bugzilla_rememberlogin> options
|
||||
are only used when you have also specified C<Bugzilla_login> and
|
||||
C<Bugzilla_password>.
|
||||
|
||||
Note that Bugzilla will return HTTP cookies along with the method
|
||||
response when you use these arguments (just like the C<User.login> method
|
||||
above).
|
||||
|
||||
=back
|
||||
|
||||
=head1 STABLE, EXPERIMENTAL, and UNSTABLE
|
||||
|
||||
Methods are marked B<STABLE> if you can expect their parameters and
|
||||
|
@ -168,18 +205,17 @@ Bugzilla versions.
|
|||
|
||||
=head1 ERRORS
|
||||
|
||||
If a particular webservice call fails, it will throw a standard XML-RPC
|
||||
error. There will be a numeric error code, and then the description
|
||||
field will contain descriptive text of the error. Each error that Bugzilla
|
||||
can throw has a specific code that will not change between versions of
|
||||
Bugzilla.
|
||||
If a particular webservice call fails, it will throw an error in the
|
||||
appropriate format for the frontend that you are using. For all frontends,
|
||||
there is at least a numeric error code and descriptive text for the error.
|
||||
|
||||
The various errors that functions can throw are specified by the
|
||||
documentation of those functions.
|
||||
|
||||
If your code needs to know what error Bugzilla threw, use the numeric
|
||||
code. Don't try to parse the description, because that may change
|
||||
from version to version of Bugzilla.
|
||||
Each error that Bugzilla can throw has a specific numeric code that will
|
||||
not change between versions of Bugzilla. If your code needs to know what
|
||||
error Bugzilla threw, use the numeric code. Don't try to parse the
|
||||
description, because that may change from version to version of Bugzilla.
|
||||
|
||||
Note that if you display the error to the user in an HTML program, make
|
||||
sure that you properly escape the error, as it will not be HTML-escaped.
|
||||
|
@ -264,30 +300,28 @@ would return something like:
|
|||
|
||||
=back
|
||||
|
||||
=head1 SEE ALSO
|
||||
|
||||
=head1 EXTENSIONS TO THE XML-RPC STANDARD
|
||||
=head2 Server Types
|
||||
|
||||
=head2 Undefined Values
|
||||
=over
|
||||
|
||||
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).
|
||||
=item L<Bugzilla::WebService::Server::XMLRPC>
|
||||
|
||||
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.
|
||||
=item L<Bugzilla::WebService::Server::JSONRPC>
|
||||
|
||||
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>.
|
||||
=back
|
||||
|
||||
=begin private
|
||||
=head2 WebService Methods
|
||||
|
||||
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.
|
||||
=over
|
||||
|
||||
=end private
|
||||
=item L<Bugzilla::WebService::Bug>
|
||||
|
||||
=item L<Bugzilla::WebService::Bugzilla>
|
||||
|
||||
=item L<Bugzilla::WebService::Product>
|
||||
|
||||
=item L<Bugzilla::WebService::User>
|
||||
|
||||
=back
|
||||
|
|
|
@ -17,12 +17,14 @@
|
|||
# Mads Bondo Dydensborg <mbd@dbc.dk>
|
||||
# Tsahi Asher <tsahi_75@yahoo.com>
|
||||
# Noura Elhawary <nelhawar@redhat.com>
|
||||
# Frank Becker <Frank@Frank-Becker.de>
|
||||
|
||||
package Bugzilla::WebService::Bug;
|
||||
|
||||
use strict;
|
||||
use base qw(Bugzilla::WebService);
|
||||
|
||||
use Bugzilla::Comment;
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::Error;
|
||||
use Bugzilla::Field;
|
||||
|
@ -30,32 +32,22 @@ use Bugzilla::WebService::Constants;
|
|||
use Bugzilla::WebService::Util qw(filter validate);
|
||||
use Bugzilla::Bug;
|
||||
use Bugzilla::BugMail;
|
||||
use Bugzilla::Util qw(trim);
|
||||
use Bugzilla::Util qw(trick_taint trim);
|
||||
use Bugzilla::Version;
|
||||
use Bugzilla::Milestone;
|
||||
use Bugzilla::Status;
|
||||
|
||||
#############
|
||||
# Constants #
|
||||
#############
|
||||
|
||||
# This maps the names of internal Bugzilla bug fields to things that would
|
||||
# make sense to somebody who's not intimately familiar with the inner workings
|
||||
# of Bugzilla. (These are the field names that the WebService uses.)
|
||||
use constant FIELD_MAP => {
|
||||
creation_time => 'creation_ts',
|
||||
description => 'comment',
|
||||
id => 'bug_id',
|
||||
last_change_time => 'delta_ts',
|
||||
platform => 'rep_platform',
|
||||
severity => 'bug_severity',
|
||||
status => 'bug_status',
|
||||
summary => 'short_desc',
|
||||
url => 'bug_file_loc',
|
||||
whiteboard => 'status_whiteboard',
|
||||
limit => 'LIMIT',
|
||||
offset => 'OFFSET',
|
||||
};
|
||||
|
||||
use constant PRODUCT_SPECIFIC_FIELDS => qw(version target_milestone component);
|
||||
|
||||
use constant DATE_FIELDS => {
|
||||
comments => ['new_since'],
|
||||
search => ['last_change_time', 'creation_time'],
|
||||
};
|
||||
|
||||
######################################################
|
||||
# Add aliases here for old method name compatibility #
|
||||
######################################################
|
||||
|
@ -71,6 +63,155 @@ BEGIN {
|
|||
# Methods #
|
||||
###########
|
||||
|
||||
sub fields {
|
||||
my ($self, $params) = validate(@_, 'ids', 'names');
|
||||
|
||||
my @fields;
|
||||
if (defined $params->{ids}) {
|
||||
my $ids = $params->{ids};
|
||||
foreach my $id (@$ids) {
|
||||
my $loop_field = Bugzilla::Field->check({ id => $id });
|
||||
push(@fields, $loop_field);
|
||||
}
|
||||
}
|
||||
|
||||
if (defined $params->{names}) {
|
||||
my $names = $params->{names};
|
||||
foreach my $field_name (@$names) {
|
||||
my $loop_field = Bugzilla::Field->check($field_name);
|
||||
# Don't push in duplicate fields if we also asked for this field
|
||||
# in "ids".
|
||||
if (!grep($_->id == $loop_field->id, @fields)) {
|
||||
push(@fields, $loop_field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!defined $params->{ids} and !defined $params->{names}) {
|
||||
@fields = Bugzilla->get_fields({ obsolete => 0 });
|
||||
}
|
||||
|
||||
my @fields_out;
|
||||
foreach my $field (@fields) {
|
||||
my $visibility_field = $field->visibility_field
|
||||
? $field->visibility_field->name : undef;
|
||||
my $vis_value = $field->visibility_value;
|
||||
my $value_field = $field->value_field
|
||||
? $field->value_field->name : undef;
|
||||
|
||||
my (@values, $has_values);
|
||||
if ( ($field->is_select and $field->name ne 'product')
|
||||
or grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS))
|
||||
{
|
||||
$has_values = 1;
|
||||
@values = @{ $self->_legal_field_values({ field => $field }) };
|
||||
}
|
||||
|
||||
if (grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS)) {
|
||||
$value_field = 'product';
|
||||
}
|
||||
|
||||
my %field_data = (
|
||||
id => $self->type('int', $field->id),
|
||||
type => $self->type('int', $field->type),
|
||||
is_custom => $self->type('boolean', $field->custom),
|
||||
name => $self->type('string', $field->name),
|
||||
display_name => $self->type('string', $field->description),
|
||||
is_on_bug_entry => $self->type('boolean', $field->enter_bug),
|
||||
visibility_field => $self->type('string', $visibility_field),
|
||||
visibility_values => [
|
||||
defined $vis_value ? $self->type('string', $vis_value->name)
|
||||
: ()
|
||||
],
|
||||
);
|
||||
if ($has_values) {
|
||||
$field_data{value_field} = $self->type('string', $value_field);
|
||||
$field_data{values} = \@values;
|
||||
};
|
||||
push(@fields_out, filter $params, \%field_data);
|
||||
}
|
||||
|
||||
return { fields => \@fields_out };
|
||||
}
|
||||
|
||||
sub _legal_field_values {
|
||||
my ($self, $params) = @_;
|
||||
my $field = $params->{field};
|
||||
my $field_name = $field->name;
|
||||
my $user = Bugzilla->user;
|
||||
|
||||
my @result;
|
||||
if (grep($_ eq $field_name, PRODUCT_SPECIFIC_FIELDS)) {
|
||||
my @list;
|
||||
if ($field_name eq 'version') {
|
||||
@list = Bugzilla::Version->get_all;
|
||||
}
|
||||
elsif ($field_name eq 'component') {
|
||||
@list = Bugzilla::Component->get_all;
|
||||
}
|
||||
else {
|
||||
@list = Bugzilla::Milestone->get_all;
|
||||
}
|
||||
|
||||
foreach my $value (@list) {
|
||||
my $sortkey = $field_name eq 'target_milestone'
|
||||
? $value->sortkey : 0;
|
||||
# XXX This is very slow for large numbers of values.
|
||||
my $product_name = $value->product->name;
|
||||
if ($user->can_see_product($product_name)) {
|
||||
push(@result, {
|
||||
name => $self->type('string', $value->name),
|
||||
sortkey => $self->type('int', $sortkey),
|
||||
visibility_values => [$self->type('string', $product_name)],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
elsif ($field_name eq 'bug_status') {
|
||||
my @status_all = Bugzilla::Status->get_all;
|
||||
foreach my $status (@status_all) {
|
||||
my @can_change_to;
|
||||
foreach my $change_to (@{ $status->can_change_to }) {
|
||||
# There's no need to note that a status can transition
|
||||
# to itself.
|
||||
next if $change_to->id == $status->id;
|
||||
my %change_to_hash = (
|
||||
name => $self->type('string', $change_to->name),
|
||||
comment_required => $self->type('boolean',
|
||||
$change_to->comment_required_on_change_from($status)),
|
||||
);
|
||||
push(@can_change_to, \%change_to_hash);
|
||||
}
|
||||
|
||||
push (@result, {
|
||||
name => $self->type('string', $status->name),
|
||||
is_open => $self->type('boolean', $status->is_open),
|
||||
sortkey => $self->type('int', $status->sortkey),
|
||||
can_change_to => \@can_change_to,
|
||||
visibility_values => [],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
my @values = Bugzilla::Field::Choice->type($field)->get_all();
|
||||
foreach my $value (@values) {
|
||||
my $vis_val = $value->visibility_value;
|
||||
push(@result, {
|
||||
name => $self->type('string', $value->name),
|
||||
sortkey => $self->type('int' , $value->sortkey),
|
||||
visibility_values => [
|
||||
defined $vis_val ? $self->type('string', $vis_val->name)
|
||||
: ()
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return \@result;
|
||||
}
|
||||
|
||||
sub comments {
|
||||
my ($self, $params) = validate(@_, 'ids', 'comment_ids');
|
||||
|
||||
|
@ -90,12 +231,12 @@ sub comments {
|
|||
foreach my $bug_id (@$bug_ids) {
|
||||
my $bug = Bugzilla::Bug->check($bug_id);
|
||||
# We want the API to always return comments in the same order.
|
||||
my $comments = Bugzilla::Bug::GetComments(
|
||||
$bug->id, 'oldest_to_newest', $params->{new_since});
|
||||
|
||||
my $comments = $bug->comments({ order => 'oldest_to_newest',
|
||||
after => $params->{new_since} });
|
||||
my @result;
|
||||
foreach my $comment (@$comments) {
|
||||
next if $comment->{isprivate} && !$user->is_insider;
|
||||
$comment->{bug_id} = $bug->id;
|
||||
next if $comment->is_private && !$user->is_insider;
|
||||
push(@result, $self->_translate_comment($comment, $params));
|
||||
}
|
||||
$bugs{$bug->id}{'comments'} = \@result;
|
||||
|
@ -104,15 +245,10 @@ sub comments {
|
|||
my %comments;
|
||||
if (scalar @$comment_ids) {
|
||||
my @ids = map { trim($_) } @$comment_ids;
|
||||
my @sql_ids = map { $dbh->quote($_) } @ids;
|
||||
my $comment_data = $dbh->selectall_arrayref(
|
||||
'SELECT comment_id AS id, bug_id, who, bug_when AS time,
|
||||
isprivate, thetext AS body, type, extra_data
|
||||
FROM longdescs WHERE ' . $dbh->sql_in('comment_id', \@sql_ids),
|
||||
{Slice=>{}});
|
||||
my $comment_data = Bugzilla::Comment->new_from_list(\@ids);
|
||||
|
||||
# See if we were passed any invalid comment ids.
|
||||
my %got_ids = map { $_->{id} => 1 } @$comment_data;
|
||||
my %got_ids = map { $_->id => 1 } @$comment_data;
|
||||
foreach my $comment_id (@ids) {
|
||||
if (!$got_ids{$comment_id}) {
|
||||
ThrowUserError('comment_id_invalid', { id => $comment_id });
|
||||
|
@ -120,16 +256,14 @@ sub comments {
|
|||
}
|
||||
|
||||
# Now make sure that we can see all the associated bugs.
|
||||
my %got_bug_ids = map { $_->{bug_id} => 1 } @$comment_data;
|
||||
my %got_bug_ids = map { $_->bug_id => 1 } @$comment_data;
|
||||
Bugzilla::Bug->check($_) foreach (keys %got_bug_ids);
|
||||
|
||||
foreach my $comment (@$comment_data) {
|
||||
if ($comment->{isprivate} && !$user->is_insider) {
|
||||
ThrowUserError('comment_is_private', { id => $comment->{id} });
|
||||
if ($comment->is_private && !$user->is_insider) {
|
||||
ThrowUserError('comment_is_private', { id => $comment->id });
|
||||
}
|
||||
$comment->{author} = new Bugzilla::User($comment->{who});
|
||||
$comment->{body} = Bugzilla::Bug::format_comment($comment);
|
||||
$comments{$comment->{id}} =
|
||||
$comments{$comment->id} =
|
||||
$self->_translate_comment($comment, $params);
|
||||
}
|
||||
}
|
||||
|
@ -140,13 +274,16 @@ sub comments {
|
|||
# Helper for Bug.comments
|
||||
sub _translate_comment {
|
||||
my ($self, $comment, $filters) = @_;
|
||||
my $attach_id = $comment->is_about_attachment ? $comment->extra_data
|
||||
: undef;
|
||||
return filter $filters, {
|
||||
id => $self->type('int', $comment->{id}),
|
||||
bug_id => $self->type('int', $comment->{bug_id}),
|
||||
author => $self->type('string', $comment->{author}->login),
|
||||
time => $self->type('dateTime', $comment->{'time'}),
|
||||
is_private => $self->type('boolean', $comment->{isprivate}),
|
||||
text => $self->type('string', $comment->{body}),
|
||||
id => $self->type('int', $comment->id),
|
||||
bug_id => $self->type('int', $comment->bug_id),
|
||||
author => $self->type('string', $comment->author->login),
|
||||
time => $self->type('dateTime', $comment->creation_ts),
|
||||
is_private => $self->type('boolean', $comment->is_private),
|
||||
text => $self->type('string', $comment->body_full),
|
||||
attachment_id => $self->type('int', $attach_id),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -203,8 +340,7 @@ sub history {
|
|||
my @history;
|
||||
foreach my $changeset (@$activity) {
|
||||
my %bug_history;
|
||||
$bug_history{when} = $self->type('dateTime',
|
||||
$self->datetime_format($changeset->{when}));
|
||||
$bug_history{when} = $self->type('dateTime', $changeset->{when});
|
||||
$bug_history{who} = $self->type('string', $changeset->{who});
|
||||
$bug_history{changes} = [];
|
||||
foreach my $change (@{ $changeset->{changes} }) {
|
||||
|
@ -216,9 +352,6 @@ sub history {
|
|||
$change->{added} = $self->type('string', $change->{added});
|
||||
$change->{field_name} = $self->type('string',
|
||||
delete $change->{fieldname});
|
||||
# This is going to go away in the future from GetBugActivity
|
||||
# so we shouldn't put it in the API.
|
||||
delete $change->{field};
|
||||
push (@{$bug_history{changes}}, $change);
|
||||
}
|
||||
|
||||
|
@ -253,7 +386,7 @@ sub search {
|
|||
{ param => 'limit', function => 'Bug.search()' });
|
||||
}
|
||||
|
||||
$params = _map_fields($params);
|
||||
$params = Bugzilla::Bug::map_fields($params);
|
||||
delete $params->{WHERE};
|
||||
|
||||
# Do special search types for certain fields.
|
||||
|
@ -287,29 +420,25 @@ sub search {
|
|||
|
||||
sub create {
|
||||
my ($self, $params) = @_;
|
||||
|
||||
Bugzilla->login(LOGIN_REQUIRED);
|
||||
|
||||
$params = _map_fields($params);
|
||||
# WebService users can't set the creation date of a bug.
|
||||
delete $params->{'creation_ts'};
|
||||
|
||||
$params = Bugzilla::Bug::map_fields($params);
|
||||
my $bug = Bugzilla::Bug->create($params);
|
||||
|
||||
Bugzilla::BugMail::Send($bug->bug_id, { changer => $bug->reporter->login });
|
||||
|
||||
return { id => $self->type('int', $bug->bug_id) };
|
||||
}
|
||||
|
||||
sub legal_values {
|
||||
my ($self, $params) = @_;
|
||||
my $field = FIELD_MAP->{$params->{field}} || $params->{field};
|
||||
my $field = Bugzilla::Bug::FIELD_MAP->{$params->{field}}
|
||||
|| $params->{field};
|
||||
|
||||
my @global_selects = Bugzilla->get_fields(
|
||||
{type => [FIELD_TYPE_SINGLE_SELECT, FIELD_TYPE_MULTI_SELECT]});
|
||||
|
||||
my $values;
|
||||
if (grep($_->name eq $field, @global_selects)) {
|
||||
# The field is a valid one.
|
||||
trick_taint($field);
|
||||
$values = get_legal_field_values($field);
|
||||
}
|
||||
elsif (grep($_ eq $field, PRODUCT_SPECIFIC_FIELDS)) {
|
||||
|
@ -317,7 +446,7 @@ sub legal_values {
|
|||
defined $id || ThrowCodeError('param_required',
|
||||
{ function => 'Bug.legal_values', param => 'product_id' });
|
||||
grep($_->id eq $id, @{Bugzilla->user->get_accessible_products})
|
||||
|| ThrowUserError('product_access_denied', { product => $id });
|
||||
|| ThrowUserError('product_access_denied', { id => $id });
|
||||
|
||||
my $product = new Bugzilla::Product($id);
|
||||
my @objects;
|
||||
|
@ -363,8 +492,12 @@ sub add_comment {
|
|||
Bugzilla->user->can_edit_product($bug->product_id)
|
||||
|| ThrowUserError("product_edit_denied", {product => $bug->product});
|
||||
|
||||
# Backwards-compatibility for versions before 3.6
|
||||
if (defined $params->{private}) {
|
||||
$params->{is_private} = delete $params->{private};
|
||||
}
|
||||
# Append comment
|
||||
$bug->add_comment($comment, { isprivate => $params->{private},
|
||||
$bug->add_comment($comment, { isprivate => $params->{is_private},
|
||||
work_time => $params->{work_time} });
|
||||
|
||||
# Capture the call to bug->update (which creates the new comment) in
|
||||
|
@ -432,6 +565,45 @@ sub update_see_also {
|
|||
return { changes => \%changes };
|
||||
}
|
||||
|
||||
sub attachments {
|
||||
my ($self, $params) = validate(@_, 'ids', 'attachment_ids');
|
||||
|
||||
if (!(defined $params->{ids}
|
||||
or defined $params->{attachment_ids}))
|
||||
{
|
||||
ThrowCodeError('param_required',
|
||||
{ function => 'Bug.attachments',
|
||||
params => ['ids', 'attachment_ids'] });
|
||||
}
|
||||
|
||||
my $ids = $params->{ids} || [];
|
||||
my $attach_ids = $params->{attachment_ids} || [];
|
||||
|
||||
my %bugs;
|
||||
foreach my $bug_id (@$ids) {
|
||||
my $bug = Bugzilla::Bug->check($bug_id);
|
||||
$bugs{$bug->id} = [];
|
||||
foreach my $attach (@{$bug->attachments}) {
|
||||
push @{$bugs{$bug->id}},
|
||||
$self->_attachment_to_hash($attach, $params);
|
||||
}
|
||||
}
|
||||
|
||||
my %attachments;
|
||||
foreach my $attach (@{Bugzilla::Attachment->new_from_list($attach_ids)}) {
|
||||
Bugzilla::Bug->check($attach->bug_id);
|
||||
if ($attach->isprivate && !Bugzilla->user->is_insider) {
|
||||
ThrowUserError('auth_failure', {action => 'access',
|
||||
object => 'attachment',
|
||||
attach_id => $attach->id});
|
||||
}
|
||||
$attachments{$attach->id} =
|
||||
$self->_attachment_to_hash($attach, $params);
|
||||
}
|
||||
|
||||
return { bugs => \%bugs, attachments => \%attachments };
|
||||
}
|
||||
|
||||
##############################
|
||||
# Private Helper Subroutines #
|
||||
##############################
|
||||
|
@ -483,22 +655,28 @@ sub _bug_to_hash {
|
|||
return \%item;
|
||||
}
|
||||
|
||||
# Convert WebService API field names to internal DB field names.
|
||||
# Used by create() and search().
|
||||
sub _map_fields {
|
||||
my ($params) = @_;
|
||||
sub _attachment_to_hash {
|
||||
my ($self, $attach, $filters) = @_;
|
||||
|
||||
my %field_values;
|
||||
foreach my $field (keys %$params) {
|
||||
my $field_name = FIELD_MAP->{$field} || $field;
|
||||
$field_values{$field_name} = $params->{$field};
|
||||
}
|
||||
# Skipping attachment flags for now.
|
||||
delete $attach->{flags};
|
||||
|
||||
unless (Bugzilla->user->is_timetracker) {
|
||||
delete @field_values{qw(estimated_time remaining_time deadline)};
|
||||
}
|
||||
my $attacher = new Bugzilla::User($attach->attacher->id);
|
||||
|
||||
return \%field_values;
|
||||
return filter $filters, {
|
||||
creation_time => $self->type('dateTime', $attach->attached),
|
||||
last_change_time => $self->type('dateTime', $attach->modification_time),
|
||||
id => $self->type('int', $attach->id),
|
||||
bug_id => $self->type('int', $attach->bug->id),
|
||||
file_name => $self->type('string', $attach->filename),
|
||||
description => $self->type('string', $attach->description),
|
||||
content_type => $self->type('string', $attach->contenttype),
|
||||
is_private => $self->type('int', $attach->isprivate),
|
||||
is_obsolete => $self->type('int', $attach->isobsolete),
|
||||
is_url => $self->type('int', $attach->isurl),
|
||||
is_patch => $self->type('int', $attach->ispatch),
|
||||
attacher => $self->type('string', $attacher->login)
|
||||
};
|
||||
}
|
||||
|
||||
1;
|
||||
|
@ -524,9 +702,198 @@ and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean.
|
|||
|
||||
=over
|
||||
|
||||
=item C<fields>
|
||||
|
||||
B<UNSTABLE>
|
||||
|
||||
=over
|
||||
|
||||
=item B<Description>
|
||||
|
||||
Get information about valid bug fields, including the lists of legal values
|
||||
for each field.
|
||||
|
||||
=item B<Params>
|
||||
|
||||
You can pass either field ids or field names.
|
||||
|
||||
B<Note>: If neither C<ids> nor C<names> is specified, then all
|
||||
non-obsolete fields 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 integer field ids.
|
||||
|
||||
=item C<names> (array) - An array of strings representing field names.
|
||||
|
||||
=back
|
||||
|
||||
=item B<Returns>
|
||||
|
||||
A hash containing a single element, C<fields>. This is an array of hashes,
|
||||
containing the following keys:
|
||||
|
||||
=over
|
||||
|
||||
=item C<id>
|
||||
|
||||
C<int> An integer id uniquely idenfifying this field in this installation only.
|
||||
|
||||
=item C<type>
|
||||
|
||||
C<int> The number of the fieldtype. The following values are defined:
|
||||
|
||||
=over
|
||||
|
||||
=item C<0> Unknown
|
||||
|
||||
=item C<1> Free Text
|
||||
|
||||
=item C<2> Drop Down
|
||||
|
||||
=item C<3> Multiple-Selection Box
|
||||
|
||||
=item C<4> Large Text Box
|
||||
|
||||
=item C<5> Date/Time
|
||||
|
||||
=item C<6> Bug Id
|
||||
|
||||
=item C<7> Bug URLs ("See Also")
|
||||
|
||||
=back
|
||||
|
||||
=item C<is_custom>
|
||||
|
||||
C<boolean> True when this is a custom field, false otherwise.
|
||||
|
||||
=item C<name>
|
||||
|
||||
C<string> The internal name of this field. This is a unique identifier for
|
||||
this field. If this is not a custom field, then this name will be the same
|
||||
across all Bugzilla installations.
|
||||
|
||||
=item C<display_name>
|
||||
|
||||
C<string> The name of the field, as it is shown in the user interface.
|
||||
|
||||
=item C<is_on_bug_entry>
|
||||
|
||||
C<boolean> For custom fields, this is true if the field is shown when you
|
||||
enter a new bug. For standard fields, this is currently always false,
|
||||
even if the field shows up when entering a bug. (To know whether or not
|
||||
a standard field is valid on bug entry, see L</create>.)
|
||||
|
||||
=item C<visibility_field>
|
||||
|
||||
C<string> The name of a field that controls the visibility of this field
|
||||
in the user interface. This field only appears in the user interface when
|
||||
the named field is equal to one of the values in C<visibility_values>.
|
||||
Can be null.
|
||||
|
||||
=item C<visibility_values>
|
||||
|
||||
C<array> of C<string>s This field is only shown when C<visibility_field>
|
||||
matches one of these values. When C<visibility_field> is null,
|
||||
then this is an empty array.
|
||||
|
||||
=item C<value_field>
|
||||
|
||||
C<string> The name of the field that controls whether or not particular
|
||||
values of the field are shown in the user interface. Can be null.
|
||||
|
||||
=item C<values>
|
||||
|
||||
This is an array of hashes, representing the legal values for
|
||||
select-type (drop-down and multiple-selection) fields. This is also
|
||||
populated for the C<component>, C<version>, and C<target_milestone>
|
||||
fields, but not for the C<product> field (you must use
|
||||
L<Product.get_accessible_products|Bugzilla::WebService::Product/get_accessible_products>
|
||||
for that.
|
||||
|
||||
For fields that aren't select-type fields, this will simply be an empty
|
||||
array.
|
||||
|
||||
Each hash has the following keys:
|
||||
|
||||
=over
|
||||
|
||||
=item C<name>
|
||||
|
||||
C<string> The actual value--this is what you would specify for this
|
||||
field in L</create>, etc.
|
||||
|
||||
=item C<sortkey>
|
||||
|
||||
C<int> Values, when displayed in a list, are sorted first by this integer
|
||||
and then secondly by their name.
|
||||
|
||||
=item C<visibility_values>
|
||||
|
||||
If C<value_field> is defined for this field, then this value is only shown
|
||||
if the C<value_field> is set to one of the values listed in this array.
|
||||
Note that for per-product fields, C<value_field> is set to C<'product'>
|
||||
and C<visibility_values> will reflect which product(s) this value appears in.
|
||||
|
||||
=item C<is_open>
|
||||
|
||||
C<boolean> For C<bug_status> values, determines whether this status
|
||||
specifies that the bug is "open" (true) or "closed" (false). This item
|
||||
is only included for the C<bug_status> field.
|
||||
|
||||
=item C<can_change_to>
|
||||
|
||||
For C<bug_status> values, this is an array of hashes that determines which
|
||||
statuses you can transition to from this status. (This item is only included
|
||||
for the C<bug_status> field.)
|
||||
|
||||
Each hash contains the following items:
|
||||
|
||||
=over
|
||||
|
||||
=item C<name>
|
||||
|
||||
the name of the new status
|
||||
|
||||
=item C<comment_required>
|
||||
|
||||
this C<boolean> True if a comment is required when you change a bug into
|
||||
this status using this transition.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
||||
=item B<Errors>
|
||||
|
||||
=over
|
||||
|
||||
=item 51 (Invalid Field Name or Id)
|
||||
|
||||
You specified an invalid field name or id.
|
||||
|
||||
=back
|
||||
|
||||
=item B<History>
|
||||
|
||||
=over
|
||||
|
||||
=item Added in Bugzilla B<3.6>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
||||
|
||||
=item C<legal_values>
|
||||
|
||||
B<EXPERIMENTAL>
|
||||
B<DEPRECATED> - Use L</fields> instead.
|
||||
|
||||
=over
|
||||
|
||||
|
@ -576,9 +943,162 @@ You specified a field that doesn't exist or isn't a drop-down field.
|
|||
=over
|
||||
|
||||
|
||||
=item C<attachments>
|
||||
|
||||
B<EXPERIMENTAL>
|
||||
|
||||
=over
|
||||
|
||||
=item B<Description>
|
||||
|
||||
It allows you to get data about attachments, given a list of bugs
|
||||
and/or attachment ids.
|
||||
|
||||
B<Note>: Private attachments will only be returned if you are in the
|
||||
insidergroup or if you are the submitter of the attachment.
|
||||
|
||||
=item B<Params>
|
||||
|
||||
B<Note>: At least one of C<ids> or C<attachment_ids> is required.
|
||||
|
||||
=over
|
||||
|
||||
=item C<ids>
|
||||
|
||||
See the description of the C<ids> parameter in the L</get> method.
|
||||
|
||||
=item C<attachment_ids>
|
||||
|
||||
C<array> An array of integer attachment ids.
|
||||
|
||||
=back
|
||||
|
||||
=item B<Returns>
|
||||
|
||||
A hash containing two elements: C<bugs> and C<attachments>. The return
|
||||
value looks like this:
|
||||
|
||||
{
|
||||
bugs => {
|
||||
1345 => {
|
||||
attachments => [
|
||||
{ (attachment) },
|
||||
{ (attachment) }
|
||||
]
|
||||
},
|
||||
9874 => {
|
||||
attachments => [
|
||||
{ (attachment) },
|
||||
{ (attachment) }
|
||||
]
|
||||
|
||||
},
|
||||
},
|
||||
|
||||
attachments => {
|
||||
234 => { (attachment) },
|
||||
123 => { (attachment) },
|
||||
}
|
||||
}
|
||||
|
||||
The attachments of any bugs that you specified in the C<ids> argument in
|
||||
input are returned in C<bugs> on output. C<bugs> is a hash that has integer
|
||||
bug IDs for keys and contains a single key, C<attachments>. That key points
|
||||
to an arrayref that contains attachments as a hash. (Fields for attachments
|
||||
are described below.)
|
||||
|
||||
For any attachments that you specified directly in C<attachment_ids>, they
|
||||
are returned in C<attachments> on output. This is a hash where the attachment
|
||||
ids point directly to hashes describing the individual attachment.
|
||||
|
||||
The fields for each attachment (where it says C<(attachment)> in the
|
||||
diagram above) are:
|
||||
|
||||
=over
|
||||
|
||||
=item C<creation_time>
|
||||
|
||||
C<dateTime> The time the attachment was created.
|
||||
|
||||
=item C<last_change_time>
|
||||
|
||||
C<dateTime> The last time the attachment was modified.
|
||||
|
||||
=item C<id>
|
||||
|
||||
C<int> The numeric id of the attachment.
|
||||
|
||||
=item C<bug_id>
|
||||
|
||||
C<int> The numeric id of the bug that the attachment is attached to.
|
||||
|
||||
=item C<file_name>
|
||||
|
||||
C<string> The file name of the attachment.
|
||||
|
||||
=item C<description>
|
||||
|
||||
C<string> The description for the attachment.
|
||||
|
||||
=item C<content_type>
|
||||
|
||||
C<string> The MIME type of the attachment.
|
||||
|
||||
=item C<is_private>
|
||||
|
||||
C<boolean> True if the attachment is private (only visible to a certain
|
||||
group called the "insidergroup"), False otherwise.
|
||||
|
||||
=item C<is_obsolete>
|
||||
|
||||
C<boolean> True if the attachment is obsolete, False otherwise.
|
||||
|
||||
=item C<is_url>
|
||||
|
||||
C<boolean> True if the attachment is a URL instead of actual data,
|
||||
False otherwise. Note that such attachments only happen when the
|
||||
Bugzilla installation has at some point had the C<allow_attach_url>
|
||||
parameter enabled.
|
||||
|
||||
=item C<is_patch>
|
||||
|
||||
C<boolean> True if the attachment is a patch, False otherwise.
|
||||
|
||||
=item C<attacher>
|
||||
|
||||
C<string> The login name of the user that created the attachment.
|
||||
|
||||
=back
|
||||
|
||||
=item B<Errors>
|
||||
|
||||
This method can throw all the same errors as L</get>. In addition,
|
||||
it can also throw the following error:
|
||||
|
||||
=over
|
||||
|
||||
=item 304 (Auth Failure, Attachment is Private)
|
||||
|
||||
You specified the id of a private attachment in the C<attachment_ids>
|
||||
argument, and you are not in the "insider group" that can see
|
||||
private attachments.
|
||||
|
||||
=back
|
||||
|
||||
=item B<History>
|
||||
|
||||
=over
|
||||
|
||||
=item Added in Bugzilla B<3.6>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
||||
|
||||
=item C<comments>
|
||||
|
||||
B<UNSTABLE>
|
||||
B<STABLE>
|
||||
|
||||
=over
|
||||
|
||||
|
@ -656,6 +1176,11 @@ C<int> The globally unique ID for the comment.
|
|||
|
||||
C<int> The ID of the bug that this comment is on.
|
||||
|
||||
=item attachment_id
|
||||
|
||||
C<int> If the comment was made on an attachment, this will be the
|
||||
ID of that attachment. Otherwise it will be null.
|
||||
|
||||
=item text
|
||||
|
||||
C<string> The actual text of the comment.
|
||||
|
@ -696,12 +1221,22 @@ that id.
|
|||
|
||||
=back
|
||||
|
||||
=item B<History>
|
||||
|
||||
=over
|
||||
|
||||
=item Added in Bugzilla B<3.4>.
|
||||
|
||||
=item C<attachment_id> was added to the return value in Bugzilla B<3.6>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
||||
|
||||
=item C<get>
|
||||
|
||||
B<EXPERIMENTAL>
|
||||
B<STABLE>
|
||||
|
||||
=over
|
||||
|
||||
|
@ -728,7 +1263,7 @@ Note that it's possible for aliases to be disabled in Bugzilla, in which
|
|||
case you will be told that you have specified an invalid bug_id if you
|
||||
try to specify an alias. (It will be error 100.)
|
||||
|
||||
=item C<permissive> B<UNSTABLE>
|
||||
=item C<permissive> B<EXPERIMENTAL>
|
||||
|
||||
C<boolean> Normally, if you request any inaccessible or invalid bug ids,
|
||||
Bug.get will throw an error. If this parameter is True, instead of throwing an
|
||||
|
@ -777,12 +1312,14 @@ isn't a duplicate of any bug, this will be an empty int.
|
|||
|
||||
C<int> The numeric bug_id of this bug.
|
||||
|
||||
=item internals B<UNSTABLE>
|
||||
=item internals B<DEPRECATED>
|
||||
|
||||
A hash. The internals of a L<Bugzilla::Bug> object. This is extremely
|
||||
unstable, and you should only rely on this if you absolutely have to. The
|
||||
structure of the hash may even change between point releases of Bugzilla.
|
||||
|
||||
This will be disappearing in a future version of Bugzilla.
|
||||
|
||||
=item is_open
|
||||
|
||||
C<boolean> Returns true (1) if this bug is open, false (0) if it is closed.
|
||||
|
@ -817,7 +1354,7 @@ C<string> The summary of this bug.
|
|||
|
||||
=back
|
||||
|
||||
=item C<faults> B<UNSTABLE>
|
||||
=item C<faults> B<EXPERIMENTAL>
|
||||
|
||||
An array of hashes that contains invalid bug ids with error messages
|
||||
returned for them. Each hash contains the following items:
|
||||
|
@ -909,7 +1446,7 @@ in Bugzilla B<3.4>:
|
|||
|
||||
=item C<history>
|
||||
|
||||
B<UNSTABLE>
|
||||
B<EXPERIMENTAL>
|
||||
|
||||
=over
|
||||
|
||||
|
@ -1209,7 +1746,7 @@ for that value.
|
|||
|
||||
=item C<create>
|
||||
|
||||
B<EXPERIMENTAL>
|
||||
B<STABLE>
|
||||
|
||||
=over
|
||||
|
||||
|
@ -1352,7 +1889,7 @@ B<Required>, due to a bug in Bugzilla.
|
|||
|
||||
=item C<add_comment>
|
||||
|
||||
B<EXPERIMENTAL>
|
||||
B<STABLE>
|
||||
|
||||
=over
|
||||
|
||||
|
@ -1371,8 +1908,8 @@ comment to.
|
|||
If this is empty or all whitespace, an error will be thrown saying that
|
||||
you did not set the C<comment> parameter.
|
||||
|
||||
=item C<private> (boolean) - If set to true, the comment is private, otherwise
|
||||
it is assumed to be public.
|
||||
=item C<is_private> (boolean) - If set to true, the comment is private,
|
||||
otherwise it is assumed to be public.
|
||||
|
||||
=item C<work_time> (double) - Adds this many hours to the "Hours Worked"
|
||||
on the bug. If you are not in the time tracking group, this value will
|
||||
|
@ -1389,6 +1926,11 @@ A hash with one element, C<id> whose value is the id of the newly-created commen
|
|||
|
||||
=over
|
||||
|
||||
=item 54 (Hours Worked Too Large)
|
||||
|
||||
You specified a C<work_time> larger than the maximum allowed value of
|
||||
C<99999.99>.
|
||||
|
||||
=item 100 (Invalid Bug Alias)
|
||||
|
||||
If you specified an alias and either: (a) the Bugzilla you're querying
|
||||
|
@ -1406,6 +1948,11 @@ You did not have the necessary rights to edit the bug.
|
|||
|
||||
You tried to add a private comment, but don't have the necessary rights.
|
||||
|
||||
=item 114 (Comment Too Long)
|
||||
|
||||
You tried to add a comment longer than the maximum allowed length
|
||||
(65,535 characters).
|
||||
|
||||
=back
|
||||
|
||||
=item B<History>
|
||||
|
@ -1419,6 +1966,13 @@ You tried to add a private comment, but don't have the necessary rights.
|
|||
=item Modified to throw an error if you try to add a private comment
|
||||
but can't, in Bugzilla B<3.4>.
|
||||
|
||||
=item Before Bugzilla B<3.6>, the C<is_private> argument was called
|
||||
C<private>, and you can still call it C<private> for backwards-compatibility
|
||||
purposes if you wish.
|
||||
|
||||
=item Before Bugzilla B<3.6>, error 54 and error 114 had a generic error
|
||||
code of 32000.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
@ -1426,7 +1980,7 @@ but can't, in Bugzilla B<3.4>.
|
|||
|
||||
=item C<update_see_also>
|
||||
|
||||
B<UNSTABLE>
|
||||
B<EXPERIMENTAL>
|
||||
|
||||
=over
|
||||
|
||||
|
@ -1517,6 +2071,11 @@ You did not have the necessary rights to edit the bug.
|
|||
|
||||
One of the URLs you provided did not look like a valid bug URL.
|
||||
|
||||
=item 115 (See Also Edit Denied)
|
||||
|
||||
You did not have the necessary rights to edit the See Also field for
|
||||
this bug.
|
||||
|
||||
=back
|
||||
|
||||
=item B<History>
|
||||
|
@ -1525,6 +2084,8 @@ One of the URLs you provided did not look like a valid bug URL.
|
|||
|
||||
=item Added in Bugzilla B<3.4>.
|
||||
|
||||
=item Before Bugzilla B<3.6>, error 115 had a generic error code of 32000.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
|
|
@ -21,7 +21,7 @@ package Bugzilla::WebService::Bugzilla;
|
|||
use strict;
|
||||
use base qw(Bugzilla::WebService);
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::Hook;
|
||||
use Bugzilla::Util qw(datetime_from);
|
||||
|
||||
use DateTime;
|
||||
|
||||
|
@ -38,45 +38,39 @@ sub 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} =
|
||||
$self->type('string', $info->{$data});
|
||||
}
|
||||
|
||||
my %retval;
|
||||
foreach my $extension (@{ Bugzilla->extensions }) {
|
||||
my $version = $extension->VERSION || 0;
|
||||
my $name = $extension->NAME;
|
||||
$retval{$name}->{version} = $self->type('string', $version);
|
||||
}
|
||||
return { extensions => $extensions };
|
||||
return { extensions => \%retval };
|
||||
}
|
||||
|
||||
sub timezone {
|
||||
my $self = shift;
|
||||
my $offset = Bugzilla->local_timezone->offset_for_datetime(DateTime->now());
|
||||
$offset = (($offset / 60) / 60) * 100;
|
||||
$offset = sprintf('%+05d', $offset);
|
||||
return { timezone => $self->type('string', $offset) };
|
||||
# All Webservices return times in UTC; Use UTC here for backwards compat.
|
||||
return { timezone => $self->type('string', "+0000") };
|
||||
}
|
||||
|
||||
sub time {
|
||||
my ($self) = @_;
|
||||
# All Webservices return times in UTC; Use UTC here for backwards compat.
|
||||
# Hardcode values where appropriate
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
my $db_time = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
|
||||
$db_time = datetime_from($db_time, 'UTC');
|
||||
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 => $self->type('dateTime', $now_utc),
|
||||
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),
|
||||
tz_name => $self->type('string', 'UTC'),
|
||||
tz_offset => $self->type('string', '+0000'),
|
||||
tz_short_name => $self->type('string', 'UTC'),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -135,10 +129,21 @@ in this Bugzilla.
|
|||
|
||||
=item B<Returns>
|
||||
|
||||
A hash with a single item, C<extesions>. This points to a hash. I<That> hash
|
||||
contains the names of extensions as keys, and information about the extension
|
||||
as values. One of the values that must be returned is the 'version' of the
|
||||
extension
|
||||
A hash with a single item, C<extensions>. This points to a hash. I<That> hash
|
||||
contains the names of extensions as keys, and the values are a hash.
|
||||
That hash contains a single key C<version>, which is the version of the
|
||||
extension, or C<0> if the extension hasn't defined a version.
|
||||
|
||||
The return value looks something like this:
|
||||
|
||||
extensions => {
|
||||
Example => {
|
||||
version => '3.6',
|
||||
},
|
||||
BmpConvert => {
|
||||
version => '1.0',
|
||||
},
|
||||
}
|
||||
|
||||
=item B<History>
|
||||
|
||||
|
@ -146,6 +151,10 @@ extension
|
|||
|
||||
=item Added in Bugzilla B<3.2>.
|
||||
|
||||
=item As of Bugzilla B<3.6>, the names of extensions are canonical names
|
||||
that the extensions define themselves. Before 3.6, the names of the
|
||||
extensions depended on the directory they were in on the Bugzilla server.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
@ -159,9 +168,7 @@ Use L</time> instead.
|
|||
|
||||
=item B<Description>
|
||||
|
||||
Returns the timezone of the server Bugzilla is running on. This is
|
||||
important because all dates/times that the webservice interface
|
||||
returns will be in this timezone.
|
||||
Returns the timezone that Bugzilla expects dates and times in.
|
||||
|
||||
=item B<Params> (none)
|
||||
|
||||
|
@ -170,12 +177,21 @@ returns will be in this timezone.
|
|||
A hash with a single item, C<timezone>, that is the timezone offset as a
|
||||
string in (+/-)XXXX (RFC 2822) format.
|
||||
|
||||
=item B<History>
|
||||
|
||||
=over
|
||||
|
||||
=item As of Bugzilla B<3.6>, the timezone returned is always C<+0000>
|
||||
(the UTC timezone).
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
||||
|
||||
=item C<time>
|
||||
|
||||
B<UNSTABLE>
|
||||
B<STABLE>
|
||||
|
||||
=over
|
||||
|
||||
|
@ -194,8 +210,8 @@ A struct with the following items:
|
|||
|
||||
=item C<db_time>
|
||||
|
||||
C<dateTime> The current time in Bugzilla's B<local time zone>, according
|
||||
to the Bugzilla I<database server>.
|
||||
C<dateTime> The current time in UTC, 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
|
||||
|
@ -204,8 +220,8 @@ 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>.
|
||||
C<dateTime> This is the current time in UTC, 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
|
||||
|
@ -214,26 +230,23 @@ 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.
|
||||
Identical to C<web_time>. (Exists only for backwards-compatibility with
|
||||
versions of Bugzilla before 3.6.)
|
||||
|
||||
=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>
|
||||
C<string> The literal string C<UTC>. (Exists only for backwards-compatibility
|
||||
with versions of Bugzilla before 3.6.)
|
||||
|
||||
=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>.
|
||||
C<string> The literal string C<UTC>. (Exists only for backwards-compatibility
|
||||
with versions of Bugzilla before 3.6.)
|
||||
|
||||
=item C<tz_offset>
|
||||
|
||||
C<string> The timezone offset as a string in (+/-)XXXX (RFC 2822) format.
|
||||
C<string> The literal string C<+0000>. (Exists only for backwards-compatibility
|
||||
with versions of Bugzilla before 3.6.)
|
||||
|
||||
=back
|
||||
|
||||
|
@ -243,6 +256,10 @@ C<string> The timezone offset as a string in (+/-)XXXX (RFC 2822) format.
|
|||
|
||||
=item Added in Bugzilla B<3.4>.
|
||||
|
||||
=item As of Bugzilla B<3.6>, this method returns all data as though the server
|
||||
were in the UTC timezone, instead of returning information in the server's
|
||||
local timezone.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
|
|
@ -24,7 +24,6 @@ our @EXPORT = qw(
|
|||
WS_ERROR_CODE
|
||||
ERROR_UNKNOWN_FATAL
|
||||
ERROR_UNKNOWN_TRANSIENT
|
||||
ERROR_AUTH_NODATA
|
||||
|
||||
WS_DISPATCH
|
||||
);
|
||||
|
@ -54,8 +53,9 @@ use constant WS_ERROR_CODE => {
|
|||
params_required => 50,
|
||||
object_does_not_exist => 51,
|
||||
param_must_be_numeric => 52,
|
||||
xmlrpc_invalid_value => 52,
|
||||
number_not_numeric => 52,
|
||||
param_invalid => 53,
|
||||
number_too_large => 54,
|
||||
# Bug errors usually occupy the 100-200 range.
|
||||
improper_bug_id_field_value => 100,
|
||||
bug_id_does_not_exist => 101,
|
||||
|
@ -86,11 +86,15 @@ use constant WS_ERROR_CODE => {
|
|||
# Comment-related errors
|
||||
comment_is_private => 110,
|
||||
comment_id_invalid => 111,
|
||||
comment_too_long => 114,
|
||||
# See Also errors
|
||||
bug_url_invalid => 112,
|
||||
bug_url_too_long => 112,
|
||||
# Insidergroup Errors
|
||||
user_not_insider => 113,
|
||||
# Note: 114 is above in the Comment-related section.
|
||||
# Bug update errors
|
||||
illegal_change => 115,
|
||||
|
||||
# Authentication errors are usually 300-400.
|
||||
invalid_username_or_password => 300,
|
||||
|
@ -99,20 +103,26 @@ use constant WS_ERROR_CODE => {
|
|||
extern_id_conflict => -303,
|
||||
auth_failure => 304,
|
||||
|
||||
# Except, historically, AUTH_NODATA, which is 410.
|
||||
login_required => 410,
|
||||
|
||||
# User errors are 500-600.
|
||||
account_exists => 500,
|
||||
illegal_email_address => 501,
|
||||
account_creation_disabled => 501,
|
||||
account_creation_restricted => 501,
|
||||
password_too_short => 502,
|
||||
password_too_long => 503,
|
||||
# Error 503 password_too_long no longer exists.
|
||||
invalid_username => 504,
|
||||
# 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,
|
||||
# Fatal errors (must be negative).
|
||||
|
||||
# RPC Server Errors. See the following URL:
|
||||
# http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php
|
||||
xmlrpc_invalid_value => -32600,
|
||||
unknown_method => -32601,
|
||||
};
|
||||
|
||||
|
@ -120,7 +130,6 @@ use constant WS_ERROR_CODE => {
|
|||
use constant ERROR_UNKNOWN_FATAL => -32000;
|
||||
use constant ERROR_UNKNOWN_TRANSIENT => 32000;
|
||||
|
||||
use constant ERROR_AUTH_NODATA => 410;
|
||||
use constant ERROR_GENERAL => 999;
|
||||
|
||||
sub WS_DISPATCH {
|
||||
|
@ -139,5 +148,4 @@ sub WS_DISPATCH {
|
|||
return $dispatch;
|
||||
};
|
||||
|
||||
|
||||
1;
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
The class structure of these files is a little strange, and this README
|
||||
explains it.
|
||||
|
||||
Our goal is to make JSON::RPC and XMLRPC::Lite both work with the same code.
|
||||
(That is, we want to have one WebService API, and have two frontends for it.)
|
||||
|
||||
The problem is that these both pass different things for $self to WebService
|
||||
methods.
|
||||
|
||||
When XMLRPC::Lite calls a method, $self is the name of the *class* the
|
||||
method is in. For example, if we call Bugzilla.version(), the first argument
|
||||
is Bugzilla::WebService::Bugzilla. So in order to have $self
|
||||
(our first argument) act correctly in XML-RPC, we make all WebService
|
||||
classes use base qw(Bugzilla::WebService).
|
||||
|
||||
When JSON::RPC calls a method, $self is the JSON-RPC *server object*. In other
|
||||
words, it's an instance of Bugzilla::WebService::Server::JSONRPC. So we have
|
||||
Bugzilla::WebService::Server::JSONRPC inherit from Bugzilla::WebService.
|
|
@ -17,28 +17,42 @@
|
|||
|
||||
package Bugzilla::WebService::Server;
|
||||
use strict;
|
||||
use Bugzilla::Util qw(ssl_require_redirect);
|
||||
|
||||
use Bugzilla::Error;
|
||||
use Bugzilla::Util qw(datetime_from);
|
||||
|
||||
use Scalar::Util qw(blessed);
|
||||
|
||||
sub handle_login {
|
||||
my ($self, $class, $method, $full_method) = @_;
|
||||
eval "require $class";
|
||||
ThrowCodeError('unknown_method', {method => $full_method}) if $@;
|
||||
return if $class->login_exempt($method);
|
||||
return if ($class->login_exempt($method)
|
||||
and !defined Bugzilla->input_params->{Bugzilla_login});
|
||||
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);
|
||||
sub datetime_format_inbound {
|
||||
my ($self, $time) = @_;
|
||||
|
||||
my $converted = datetime_from($time, Bugzilla->local_timezone);
|
||||
$time = $converted->ymd() . ' ' . $converted->hms();
|
||||
return $time
|
||||
}
|
||||
|
||||
sub datetime_format_outbound {
|
||||
my ($self, $date) = @_;
|
||||
|
||||
my $time = $date;
|
||||
if (blessed($date)) {
|
||||
# We expect this to mean we were sent a datetime object
|
||||
$time->set_time_zone('UTC');
|
||||
} else {
|
||||
# We always send our time in UTC, for consistency.
|
||||
# passed in value is likely a string, create a datetime object
|
||||
$time = datetime_from($date, 'UTC');
|
||||
}
|
||||
return $time->iso8601();
|
||||
}
|
||||
|
||||
1;
|
||||
|
|
|
@ -0,0 +1,337 @@
|
|||
# -*- 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 JSON Webservices Interface.
|
||||
#
|
||||
# The Initial Developer of the Original Code is the San Jose State
|
||||
# University Foundation. 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::Server::JSONRPC;
|
||||
|
||||
use strict;
|
||||
use base qw(JSON::RPC::Server::CGI Bugzilla::WebService::Server);
|
||||
|
||||
use Bugzilla::Error;
|
||||
use Bugzilla::WebService::Constants;
|
||||
use Bugzilla::WebService::Util qw(taint_data);
|
||||
|
||||
sub new {
|
||||
my $class = shift;
|
||||
my $self = $class->SUPER::new(@_);
|
||||
Bugzilla->_json_server($self);
|
||||
$self->dispatch(WS_DISPATCH);
|
||||
$self->return_die_message(1);
|
||||
return $self;
|
||||
}
|
||||
|
||||
sub create_json_coder {
|
||||
my $self = shift;
|
||||
my $json = $self->SUPER::create_json_coder(@_);
|
||||
$json->allow_blessed(1);
|
||||
$json->convert_blessed(1);
|
||||
# This may seem a little backwards, but what this really means is
|
||||
# "don't convert our utf8 into byte strings, just leave it as a
|
||||
# utf8 string."
|
||||
$json->utf8(0) if Bugzilla->params->{'utf8'};
|
||||
return $json;
|
||||
}
|
||||
|
||||
# Override the JSON::RPC method to return our CGI object instead of theirs.
|
||||
sub cgi { return Bugzilla->cgi; }
|
||||
|
||||
# Override the JSON::RPC method to use $cgi->header properly instead of
|
||||
# just printing text directly. This fixes various problems, including
|
||||
# sending Bugzilla's cookies properly.
|
||||
sub response {
|
||||
my ($self, $response) = @_;
|
||||
my $headers = $response->headers;
|
||||
my @header_args;
|
||||
foreach my $name ($headers->header_field_names) {
|
||||
my @values = $headers->header($name);
|
||||
$name =~ s/-/_/g;
|
||||
foreach my $value (@values) {
|
||||
push(@header_args, "-$name", $value);
|
||||
}
|
||||
}
|
||||
my $cgi = $self->cgi;
|
||||
print $cgi->header(-status => $response->code, @header_args);
|
||||
print $response->content;
|
||||
}
|
||||
|
||||
sub type {
|
||||
my ($self, $type, $value) = @_;
|
||||
|
||||
# This is the only type that does something special with undef.
|
||||
if ($type eq 'boolean') {
|
||||
return $value ? JSON::true : JSON::false;
|
||||
}
|
||||
|
||||
return JSON::null if !defined $value;
|
||||
|
||||
my $retval = $value;
|
||||
|
||||
if ($type eq 'int') {
|
||||
$retval = int($value);
|
||||
}
|
||||
if ($type eq 'double') {
|
||||
$retval = 0.0 + $value;
|
||||
}
|
||||
elsif ($type eq 'string') {
|
||||
# Forces string context, so that JSON will make it a string.
|
||||
$retval = "$value";
|
||||
}
|
||||
elsif ($type eq 'dateTime') {
|
||||
# ISO-8601 "YYYYMMDDTHH:MM:SS" with a literal T
|
||||
$retval = $self->datetime_format_outbound($value);
|
||||
}
|
||||
# XXX Will have to implement base64 if Bugzilla starts using it.
|
||||
|
||||
return $retval;
|
||||
}
|
||||
|
||||
sub datetime_format_outbound {
|
||||
my $self = shift;
|
||||
# YUI expects ISO8601 in UTC time; including TZ specifier
|
||||
return $self->SUPER::datetime_format_outbound(@_) . 'Z';
|
||||
}
|
||||
|
||||
|
||||
# Store the ID of the current call, because Bugzilla::Error will need it.
|
||||
sub _handle {
|
||||
my $self = shift;
|
||||
my ($obj) = @_;
|
||||
$self->{_bz_request_id} = $obj->{id};
|
||||
return $self->SUPER::_handle(@_);
|
||||
}
|
||||
|
||||
# Make all error messages returned by JSON::RPC go into the 100000
|
||||
# range, and bring down all our errors into the normal range.
|
||||
sub _error {
|
||||
my ($self, $id, $code) = (shift, shift, shift);
|
||||
# All JSON::RPC errors are less than 1000.
|
||||
if ($code < 1000) {
|
||||
$code += 100000;
|
||||
}
|
||||
# Bugzilla::Error adds 100,000 to all *our* errors, so
|
||||
# we know they came from us.
|
||||
elsif ($code > 100000) {
|
||||
$code -= 100000;
|
||||
}
|
||||
|
||||
# We can't just set $_[1] because it's not always settable,
|
||||
# in JSON::RPC::Server.
|
||||
unshift(@_, $id, $code);
|
||||
my $json = $self->SUPER::_error(@_);
|
||||
|
||||
# We want to always send the JSON-RPC 1.1 error format, although
|
||||
# If we're not in JSON-RPC 1.1, we don't need the silly "name" parameter.
|
||||
if (!$self->version or $self->version ne '1.1') {
|
||||
my $object = $self->json->decode($json);
|
||||
my $message = $object->{error};
|
||||
# Just assure that future versions of JSON::RPC don't change the
|
||||
# JSON-RPC 1.0 error format.
|
||||
if (!ref $message) {
|
||||
$object->{error} = {
|
||||
code => $code,
|
||||
message => $message,
|
||||
};
|
||||
$json = $self->json->encode($object);
|
||||
}
|
||||
}
|
||||
return $json;
|
||||
}
|
||||
|
||||
##################
|
||||
# Login Handling #
|
||||
##################
|
||||
|
||||
# This handles dispatching our calls to the appropriate class based on
|
||||
# the name of the method.
|
||||
sub _find_procedure {
|
||||
my $self = shift;
|
||||
|
||||
# This is also a good place to deny GET requests, since we can
|
||||
# safely call ThrowUserError at this point.
|
||||
if ($self->request->method ne 'POST') {
|
||||
ThrowUserError('json_rpc_post_only');
|
||||
}
|
||||
|
||||
my $method = shift;
|
||||
$self->{_bz_method_name} = $method;
|
||||
|
||||
# This tricks SUPER::_find_procedure into finding the right class.
|
||||
$method =~ /^(\S+)\.(\S+)$/;
|
||||
$self->path_info($1);
|
||||
unshift(@_, $2);
|
||||
|
||||
return $self->SUPER::_find_procedure(@_);
|
||||
}
|
||||
|
||||
# This is a hacky way to do something right before methods are called.
|
||||
# This is the last thing that JSON::RPC::Server::_handle calls right before
|
||||
# the method is actually called.
|
||||
sub _argument_type_check {
|
||||
my $self = shift;
|
||||
my $params = $self->SUPER::_argument_type_check(@_);
|
||||
|
||||
# JSON-RPC 1.0 requires all parameters to be passed as an array, so
|
||||
# we just pull out the first item and assume it's an object.
|
||||
my $params_is_array;
|
||||
if (ref $params eq 'ARRAY') {
|
||||
$params = $params->[0];
|
||||
$params_is_array = 1;
|
||||
}
|
||||
|
||||
taint_data($params);
|
||||
|
||||
# Now, convert dateTime fields on input.
|
||||
$self->_bz_method_name =~ /^(\S+)\.(\S+)$/;
|
||||
my ($class, $method) = ($1, $2);
|
||||
my $pkg = $self->{dispatch_path}->{$class};
|
||||
my @date_fields = @{ $pkg->DATE_FIELDS->{$method} || [] };
|
||||
foreach my $field (@date_fields) {
|
||||
if (defined $params->{$field}) {
|
||||
my $value = $params->{$field};
|
||||
if (ref $value eq 'ARRAY') {
|
||||
$params->{$field} =
|
||||
[ map { $self->datetime_format_inbound($_) } @$value ];
|
||||
}
|
||||
else {
|
||||
$params->{$field} = $self->datetime_format_inbound($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Bugzilla->input_params($params);
|
||||
|
||||
# This is the best time to do login checks.
|
||||
$self->handle_login();
|
||||
|
||||
# Bugzilla::WebService packages call internal methods like
|
||||
# $self->_some_private_method. So we have to inherit from
|
||||
# that class as well as this Server class.
|
||||
my $new_class = ref($self) . '::' . $pkg;
|
||||
my $isa_string = 'our @ISA = qw(' . ref($self) . " $pkg)";
|
||||
eval "package $new_class;$isa_string;";
|
||||
bless $self, $new_class;
|
||||
|
||||
if ($params_is_array) {
|
||||
$params = [$params];
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
sub handle_login {
|
||||
my $self = shift;
|
||||
|
||||
my $path = $self->path_info;
|
||||
my $class = $self->{dispatch_path}->{$path};
|
||||
my $full_method = $self->_bz_method_name;
|
||||
$full_method =~ /^\S+\.(\S+)/;
|
||||
my $method = $1;
|
||||
$self->SUPER::handle_login($class, $method, $full_method);
|
||||
}
|
||||
|
||||
# _bz_method_name is stored by _find_procedure for later use.
|
||||
sub _bz_method_name {
|
||||
return $_[0]->{_bz_method_name};
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::WebService::Server::JSONRPC - The JSON-RPC Interface to Bugzilla
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
This documentation describes things about the Bugzilla WebService that
|
||||
are specific to JSON-RPC. For a general overview of the Bugzilla WebServices,
|
||||
see L<Bugzilla::WebService>.
|
||||
|
||||
Please note that I<everything> about this JSON-RPC interface is
|
||||
B<EXPERIMENTAL>. If you want a fully stable API, please use the
|
||||
C<Bugzilla::WebService::Server::XMLRPC|XML-RPC> interface.
|
||||
|
||||
=head1 JSON-RPC
|
||||
|
||||
Bugzilla supports both JSON-RPC 1.0 and 1.1. We recommend that you use
|
||||
JSON-RPC 1.0 instead of 1.1, though, because 1.1 is deprecated.
|
||||
|
||||
At some point in the future, Bugzilla may also support JSON-RPC 2.0.
|
||||
|
||||
The JSON-RPC standards are described at L<http://json-rpc.org/>.
|
||||
|
||||
=head1 CONNECTING
|
||||
|
||||
The endpoint for the JSON-RPC interface is the C<jsonrpc.cgi> script in
|
||||
your Bugzilla installation. For example, if your Bugzilla is at
|
||||
C<bugzilla.yourdomain.com>, then your JSON-RPC client would access the
|
||||
API via: C<http://bugzilla.yourdomain.com/jsonrpc.cgi>
|
||||
|
||||
Bugzilla only allows JSON-RPC requests over C<POST>. C<GET> requests
|
||||
(or any other type of request, such as C<HEAD>) will be denied.
|
||||
|
||||
=head1 PARAMETERS
|
||||
|
||||
For JSON-RPC 1.0, the very first parameter should be an object containing
|
||||
the named parameters. For example, if you were passing two named parameters,
|
||||
one called C<foo> and the other called C<bar>, the C<params> element of
|
||||
your JSON-RPC call would look like:
|
||||
|
||||
"params": [{ "foo": 1, "bar": "something" }]
|
||||
|
||||
For JSON-RPC 1.1, you can pass parameters either in the above fashion
|
||||
or using the standard named-parameters mechanism of JSON-RPC 1.1.
|
||||
|
||||
C<dateTime> fields are strings in the standard ISO-8601 format:
|
||||
C<YYYY-MM-DDTHH:MM:SSZ>, where C<T> and C<Z> are a literal T and Z,
|
||||
respectively. The "Z" means that all times are in UTC timezone--times are
|
||||
always returned in UTC, and should be passed in as UTC. (Note: The JSON-RPC
|
||||
interface currently also accepts non-UTC times for any values passed in, if
|
||||
they include a time-zone specifier that follows the ISO-8601 standard, instead
|
||||
of "Z" at the end. This behavior is expected to continue into the future, but
|
||||
to be fully safe for forward-compatibility with all future versions of
|
||||
Bugzilla, it is safest to pass in all times as UTC with the "Z" timezone
|
||||
specifier.)
|
||||
|
||||
All other types are standard JSON types.
|
||||
|
||||
=head1 ERRORS
|
||||
|
||||
JSON-RPC 1.0 and JSON-RPC 1.1 both return an C<error> element when they
|
||||
throw an error. In Bugzilla, the error contents look like:
|
||||
|
||||
{ message: 'Some message here', code: 123 }
|
||||
|
||||
So, for example, in JSON-RPC 1.0, an error response would look like:
|
||||
|
||||
{
|
||||
result: null,
|
||||
error: { message: 'Some message here', code: 123 },
|
||||
id: 1
|
||||
}
|
||||
|
||||
Every error has a "code", as described in L<Bugzilla::WebService/ERRORS>.
|
||||
Errors with a numeric C<code> higher than 100000 are errors thrown by
|
||||
the JSON-RPC library that Bugzilla uses, not by Bugzilla.
|
||||
|
||||
=head1 SEE ALSO
|
||||
|
||||
L<Bugzilla::WebService>
|
|
@ -51,18 +51,6 @@ sub make_response {
|
|||
}
|
||||
}
|
||||
|
||||
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};
|
||||
|
@ -79,10 +67,23 @@ 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; };
|
||||
use XMLRPC::Lite;
|
||||
our @ISA = qw(XMLRPC::Deserializer);
|
||||
|
||||
use Bugzilla::Error;
|
||||
use Scalar::Util qw(tainted);
|
||||
|
||||
sub deserialize {
|
||||
my $self = shift;
|
||||
my ($xml) = @_;
|
||||
my $som = $self->SUPER::deserialize(@_);
|
||||
if (tainted($xml)) {
|
||||
$som->{_bz_do_taint} = 1;
|
||||
}
|
||||
bless $som, 'Bugzilla::XMLRPC::SOM';
|
||||
Bugzilla->input_params($som->paramsin || {});
|
||||
return $som;
|
||||
}
|
||||
|
||||
# Some method arguments need to be converted in some way, when they are input.
|
||||
sub decode_value {
|
||||
|
@ -108,10 +109,12 @@ sub decode_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";
|
||||
if ($value !~ /T.*[\-+Z]/i) {
|
||||
# The caller did not specify a timezone, so we assume UTC.
|
||||
# pass 'Z' specifier to datetime_from to force it
|
||||
$value = $value . 'Z';
|
||||
}
|
||||
$value = Bugzilla::WebService::Server::XMLRPC->datetime_format_inbound($value);
|
||||
}
|
||||
|
||||
return $value;
|
||||
|
@ -141,6 +144,25 @@ sub _validation_subs {
|
|||
|
||||
1;
|
||||
|
||||
package Bugzilla::XMLRPC::SOM;
|
||||
use strict;
|
||||
use XMLRPC::Lite;
|
||||
our @ISA = qw(XMLRPC::SOM);
|
||||
use Bugzilla::WebService::Util qw(taint_data);
|
||||
|
||||
sub paramsin {
|
||||
my $self = shift;
|
||||
return $self->{bz_params_in} if $self->{bz_params_in};
|
||||
my $params = $self->SUPER::paramsin(@_);
|
||||
if ($self->{_bz_do_taint}) {
|
||||
taint_data($params);
|
||||
}
|
||||
$self->{bz_params_in} = $params;
|
||||
return $self->{bz_params_in};
|
||||
}
|
||||
|
||||
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;
|
||||
|
@ -148,7 +170,7 @@ 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; };
|
||||
use XMLRPC::Lite;
|
||||
our @ISA = qw(XMLRPC::Serializer);
|
||||
|
||||
sub new {
|
||||
|
@ -222,7 +244,6 @@ sub _strip_undefs {
|
|||
return $initial;
|
||||
}
|
||||
|
||||
|
||||
sub BEGIN {
|
||||
no strict 'refs';
|
||||
for my $type (qw(double i4 int dateTime)) {
|
||||
|
@ -245,3 +266,80 @@ sub as_nil {
|
|||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::WebService::Server::XMLRPC - The XML-RPC Interface to Bugzilla
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
This documentation describes things about the Bugzilla WebService that
|
||||
are specific to XML-RPC. For a general overview of the Bugzilla WebServices,
|
||||
see L<Bugzilla::WebService>.
|
||||
|
||||
=head1 XML-RPC
|
||||
|
||||
The XML-RPC standard is described here: L<http://www.xmlrpc.com/spec>
|
||||
|
||||
=head1 CONNECTING
|
||||
|
||||
The endpoint for the XML-RPC interface is the C<xmlrpc.cgi> script in
|
||||
your Bugzilla installation. For example, if your Bugzilla is at
|
||||
C<bugzilla.yourdomain.com>, then your XML-RPC client would access the
|
||||
API via: C<http://bugzilla.yourdomain.com/xmlrpc.cgi>
|
||||
|
||||
=head1 PARAMETERS
|
||||
|
||||
C<dateTime> fields are the standard C<dateTime.iso8601> XML-RPC field. They
|
||||
should be in C<YYYY-MM-DDTHH:MM:SS> format (where C<T> is a literal T). As
|
||||
of Bugzilla B<3.6>, Bugzilla always expects C<dateTime> fields to be in the
|
||||
UTC timezone, and all returned C<dateTime> values are in the UTC timezone.
|
||||
|
||||
All other fields are standard XML-RPC types.
|
||||
|
||||
=head2 How XML-RPC WebService Methods Take Parameters
|
||||
|
||||
All functions take a single argument, a C<< <struct> >> that contains all parameters.
|
||||
The names of the parameters listed in the API docs for each function are the
|
||||
C<< <name> >> element for the struct C<< <member> >>s.
|
||||
|
||||
=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 accepts a timezone specifier at the end of C<dateTime.iso8601>
|
||||
fields that are specified as method arguments. The format of the timezone
|
||||
specifier is specified in the ISO-8601 standard. If no timezone specifier
|
||||
is included, the passed-in time is assumed to be in the UTC timezone.
|
||||
Bugzilla will never output a timezone specifier on returned data, because
|
||||
doing so would violate the XML-RPC specification. All returned times are in
|
||||
the UTC timezone.
|
||||
|
||||
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
|
||||
|
||||
=head1 SEE ALSO
|
||||
|
||||
L<Bugzilla::WebService>
|
||||
|
|
|
@ -61,12 +61,12 @@ sub login {
|
|||
}
|
||||
|
||||
# Make sure the CGI user info class works if necessary.
|
||||
my $cgi = Bugzilla->cgi;
|
||||
$cgi->param('Bugzilla_login', $params->{login});
|
||||
$cgi->param('Bugzilla_password', $params->{password});
|
||||
$cgi->param('Bugzilla_remember', $remember);
|
||||
my $input_params = Bugzilla->input_params;
|
||||
$input_params->{'Bugzilla_login'} = $params->{login};
|
||||
$input_params->{'Bugzilla_password'} = $params->{password};
|
||||
$input_params->{'Bugzilla_remember'} = $remember;
|
||||
|
||||
Bugzilla->login;
|
||||
Bugzilla->login();
|
||||
return { id => $self->type('int', Bugzilla->user->id) };
|
||||
}
|
||||
|
||||
|
@ -396,7 +396,7 @@ An account with that email address already exists in Bugzilla.
|
|||
|
||||
=item C<create>
|
||||
|
||||
B<EXPERIMENTAL>
|
||||
B<STABLE>
|
||||
|
||||
=over
|
||||
|
||||
|
@ -445,10 +445,13 @@ the function may also throw:
|
|||
The password specified is too short. (Usually, this means the
|
||||
password is under three characters.)
|
||||
|
||||
=item 503 (Password Too Long)
|
||||
=back
|
||||
|
||||
The password specified is too long. (Usually, this means the
|
||||
password is over ten characters.)
|
||||
=item B<History>
|
||||
|
||||
=over
|
||||
|
||||
=item Error 503 (Password Too Long) removed in Bugzilla B<3.6>.
|
||||
|
||||
=back
|
||||
|
||||
|
@ -462,7 +465,7 @@ password is over ten characters.)
|
|||
|
||||
=item C<get>
|
||||
|
||||
B<UNSTABLE>
|
||||
B<STABLE>
|
||||
|
||||
=over
|
||||
|
||||
|
|
|
@ -21,10 +21,17 @@
|
|||
|
||||
package Bugzilla::WebService::Util;
|
||||
use strict;
|
||||
|
||||
use base qw(Exporter);
|
||||
|
||||
our @EXPORT_OK = qw(filter validate);
|
||||
# We have to "require", not "use" this, because otherwise it tries to
|
||||
# use features of Test::More during import().
|
||||
require Test::Taint;
|
||||
|
||||
our @EXPORT_OK = qw(
|
||||
filter
|
||||
taint_data
|
||||
validate
|
||||
);
|
||||
|
||||
sub filter ($$) {
|
||||
my ($params, $hash) = @_;
|
||||
|
@ -44,6 +51,32 @@ sub filter ($$) {
|
|||
return \%newhash;
|
||||
}
|
||||
|
||||
sub taint_data {
|
||||
my $params = shift;
|
||||
return if !$params;
|
||||
# Though this is a private function, it hasn't changed since 2004 and
|
||||
# should be safe to use, and prevents us from having to write it ourselves
|
||||
# or require another module to do it.
|
||||
Test::Taint::_deeply_traverse(\&_delete_bad_keys, $params);
|
||||
Test::Taint::taint_deeply($params);
|
||||
}
|
||||
|
||||
sub _delete_bad_keys {
|
||||
foreach my $item (@_) {
|
||||
next if ref $item ne 'HASH';
|
||||
foreach my $key (keys %$item) {
|
||||
# Making something a hash key always untaints it, in Perl.
|
||||
# However, we need to validate our argument names in some way.
|
||||
# We know that all hash keys passed in to the WebService will
|
||||
# match \w+, so we delete any key that doesn't match that.
|
||||
if ($key !~ /^\w+$/) {
|
||||
delete $item->{$key};
|
||||
}
|
||||
}
|
||||
}
|
||||
return @_;
|
||||
}
|
||||
|
||||
sub validate {
|
||||
my ($self, $params, @keys) = @_;
|
||||
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
# -*- 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 Eric Black.
|
||||
# Portions created by the Initial Developer are Copyright (C) 2009
|
||||
# Eric Black. All Rights Reserved.
|
||||
#
|
||||
# Contributor(s): Eric Black <black.eric@gmail.com>
|
||||
|
||||
package Bugzilla::Whine::Query;
|
||||
|
||||
use strict;
|
||||
|
||||
use base qw(Bugzilla::Object);
|
||||
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::Search::Saved;
|
||||
|
||||
#############
|
||||
# Constants #
|
||||
#############
|
||||
|
||||
use constant DB_TABLE => 'whine_queries';
|
||||
|
||||
use constant DB_COLUMNS => qw(
|
||||
id
|
||||
eventid
|
||||
query_name
|
||||
sortkey
|
||||
onemailperbug
|
||||
title
|
||||
);
|
||||
|
||||
use constant NAME_FIELD => 'id';
|
||||
use constant LIST_ORDER => 'sortkey';
|
||||
|
||||
####################
|
||||
# Simple Accessors #
|
||||
####################
|
||||
sub eventid { return $_[0]->{'eventid'}; }
|
||||
sub sortkey { return $_[0]->{'sortkey'}; }
|
||||
sub one_email_per_bug { return $_[0]->{'onemailperbug'}; }
|
||||
sub title { return $_[0]->{'title'}; }
|
||||
sub name { return $_[0]->{'query_name'}; }
|
||||
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::Whine::Query - A query object used by L<Bugzilla::Whine>.
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
use Bugzilla::Whine::Query;
|
||||
|
||||
my $query = new Bugzilla::Whine::Query($id);
|
||||
|
||||
my $event_id = $query->eventid;
|
||||
my $id = $query->id;
|
||||
my $query_name = $query->name;
|
||||
my $sortkey = $query->sortkey;
|
||||
my $one_email_per_bug = $query->one_email_per_bug;
|
||||
my $title = $query->title;
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
This module exists to represent a query for a L<Bugzilla::Whine::Event>.
|
||||
Each event, which are groups of schedules and queries based on how the
|
||||
user configured the event, may have zero or more queries associated
|
||||
with it. Additionally, the queries are selected from the user's saved
|
||||
searches, or L<Bugzilla::Search::Saved> object with a matching C<name>
|
||||
attribute for the user.
|
||||
|
||||
This is an implementation of L<Bugzilla::Object>, and so has all the
|
||||
same methods available as L<Bugzilla::Object>, in addition to what is
|
||||
documented below.
|
||||
|
||||
=head1 METHODS
|
||||
|
||||
=head2 Constructors
|
||||
|
||||
=over
|
||||
|
||||
=item C<new>
|
||||
|
||||
Does not accept a bare C<name> argument. Instead, accepts only an id.
|
||||
|
||||
See also: L<Bugzilla::Object/new>.
|
||||
|
||||
=back
|
||||
|
||||
|
||||
=head2 Accessors
|
||||
|
||||
These return data about the object, without modifying the object.
|
||||
|
||||
=over
|
||||
|
||||
=item C<event_id>
|
||||
|
||||
The L<Bugzilla::Whine::Event> object id for this object.
|
||||
|
||||
=item C<name>
|
||||
|
||||
The L<Bugzilla::Search::Saved> query object name for this object.
|
||||
|
||||
=item C<sortkey>
|
||||
|
||||
The relational sorting key as compared with other L<Bugzilla::Whine::Query>
|
||||
objects.
|
||||
|
||||
=item C<one_email_per_bug>
|
||||
|
||||
Returns a numeric 1(C<true>) or 0(C<false>) to represent whether this
|
||||
L<Bugzilla::Whine::Query> object is supposed to be mailed as a list of
|
||||
bugs or one email per bug.
|
||||
|
||||
=item C<title>
|
||||
|
||||
The title of this object as it appears in the user forms and emails.
|
||||
|
||||
=back
|
|
@ -0,0 +1,172 @@
|
|||
# -*- 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 Eric Black.
|
||||
# Portions created by the Initial Developer are Copyright (C) 2009
|
||||
# Eric Black. All Rights Reserved.
|
||||
#
|
||||
# Contributor(s): Eric Black <black.eric@gmail.com>
|
||||
|
||||
use strict;
|
||||
|
||||
package Bugzilla::Whine::Schedule;
|
||||
|
||||
use base qw(Bugzilla::Object);
|
||||
|
||||
use Bugzilla::Constants;
|
||||
|
||||
#############
|
||||
# Constants #
|
||||
#############
|
||||
|
||||
use constant DB_TABLE => 'whine_schedules';
|
||||
|
||||
use constant DB_COLUMNS => qw(
|
||||
id
|
||||
eventid
|
||||
run_day
|
||||
run_time
|
||||
run_next
|
||||
mailto
|
||||
mailto_type
|
||||
);
|
||||
|
||||
use constant REQUIRED_CREATE_FIELDS => qw(eventid mailto mailto_type);
|
||||
|
||||
use constant UPDATE_COLUMNS => qw(
|
||||
eventid
|
||||
run_day
|
||||
run_time
|
||||
run_next
|
||||
mailto
|
||||
mailto_type
|
||||
);
|
||||
use constant NAME_FIELD => 'id';
|
||||
use constant LIST_ORDER => 'id';
|
||||
|
||||
####################
|
||||
# Simple Accessors #
|
||||
####################
|
||||
sub eventid { return $_[0]->{'eventid'}; }
|
||||
sub run_day { return $_[0]->{'run_day'}; }
|
||||
sub run_time { return $_[0]->{'run_time'}; }
|
||||
sub mailto_is_group { return $_[0]->{'mailto_type'}; }
|
||||
|
||||
sub mailto {
|
||||
my $self = shift;
|
||||
|
||||
return $self->{mailto_object} if exists $self->{mailto_object};
|
||||
my $id = $self->{'mailto'};
|
||||
|
||||
if ($self->mailto_is_group) {
|
||||
$self->{mailto_object} = Bugzilla::Group->new($id);
|
||||
} else {
|
||||
$self->{mailto_object} = Bugzilla::User->new($id);
|
||||
}
|
||||
return $self->{mailto_object};
|
||||
}
|
||||
|
||||
sub mailto_users {
|
||||
my $self = shift;
|
||||
return $self->{mailto_users} if exists $self->{mailto_users};
|
||||
my $object = $self->mailto;
|
||||
|
||||
if ($self->mailto_is_group) {
|
||||
$self->{mailto_users} = $object->members_non_inherited if $object->is_active;
|
||||
} else {
|
||||
$self->{mailto_users} = $object;
|
||||
}
|
||||
return $self->{mailto_users};
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::Whine::Schedule - A schedule object used by L<Bugzilla::Whine>.
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
use Bugzilla::Whine::Schedule;
|
||||
|
||||
my $schedule = new Bugzilla::Whine::Schedule($schedule_id);
|
||||
|
||||
my $event_id = $schedule->eventid;
|
||||
my $run_day = $schedule->run_day;
|
||||
my $run_time = $schedule->run_time;
|
||||
my $is_group = $schedule->mailto_is_group;
|
||||
my $object = $schedule->mailto;
|
||||
my $array_ref = $schedule->mailto_users;
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
This module exists to represent a L<Bugzilla::Whine> event schedule.
|
||||
|
||||
This is an implementation of L<Bugzilla::Object>, and so has all the
|
||||
same methods available as L<Bugzilla::Object>, in addition to what is
|
||||
documented below.
|
||||
|
||||
=head1 METHODS
|
||||
|
||||
=head2 Constructors
|
||||
|
||||
=over
|
||||
|
||||
=item C<new>
|
||||
|
||||
Does not accept a bare C<name> argument. Instead, accepts only an id.
|
||||
|
||||
See also: L<Bugzilla::Object/new>.
|
||||
|
||||
=back
|
||||
|
||||
|
||||
=head2 Accessors
|
||||
|
||||
These return data about the object, without modifying the object.
|
||||
|
||||
=over
|
||||
|
||||
=item C<event_id>
|
||||
|
||||
The L<Bugzilla::Whine> event object id for this object.
|
||||
|
||||
=item C<run_day>
|
||||
|
||||
The day or day pattern that a L<Bugzilla::Whine> event is scheduled to run.
|
||||
|
||||
=item C<run_time>
|
||||
|
||||
The time or time pattern that a L<Bugzilla::Whine> event is scheduled to run.
|
||||
|
||||
=item C<mailto_is_group>
|
||||
|
||||
Returns a numeric 1 (C<group>) or 0 (C<user>) to represent whether
|
||||
L</mailto> is a group or user.
|
||||
|
||||
=item C<mailto>
|
||||
|
||||
This is either a L<Bugzilla::User> or L<Bugzilla::Group> object to represent
|
||||
the user or group this scheduled event is set to be mailed to.
|
||||
|
||||
=item C<mailto_users>
|
||||
|
||||
Returns an array reference of L<Bugzilla::User>s. This is derived from the
|
||||
L<Bugzilla::Group> stored in L</mailto> if L</mailto_is_group> is true and
|
||||
the group is still active, otherwise it will contain a single array element
|
||||
for the L<Bugzilla::User> in L</mailto>.
|
||||
|
||||
=back
|
|
@ -60,6 +60,7 @@ sub _column_length
|
|||
return $len;
|
||||
}
|
||||
|
||||
undef &Text::TabularDisplay::_column_length;
|
||||
*Text::TabularDisplay::_column_length = \&_column_length;
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
|
|
556
attachment.cgi
556
attachment.cgi
|
@ -39,6 +39,7 @@ use strict;
|
|||
use lib qw(. lib);
|
||||
|
||||
use Bugzilla;
|
||||
use Bugzilla::BugMail;
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::Error;
|
||||
use Bugzilla::Flag;
|
||||
|
@ -77,21 +78,13 @@ my $action = $cgi->param('action') || 'view';
|
|||
# You must use the appropriate urlbase/sslbase param when doing anything
|
||||
# but viewing an attachment.
|
||||
if ($action ne 'view') {
|
||||
my $urlbase = Bugzilla->params->{'urlbase'};
|
||||
my $sslbase = Bugzilla->params->{'sslbase'};
|
||||
my $path_regexp = $sslbase ? qr/^(\Q$urlbase\E|\Q$sslbase\E)/ : qr/^\Q$urlbase\E/;
|
||||
if (use_attachbase() && $cgi->self_url !~ /$path_regexp/) {
|
||||
do_ssl_redirect_if_required();
|
||||
if ($cgi->url_is_attachment_base) {
|
||||
$cgi->redirect_to_urlbase;
|
||||
}
|
||||
Bugzilla->login();
|
||||
}
|
||||
|
||||
# Determine if PatchReader is installed
|
||||
eval {
|
||||
require PatchReader;
|
||||
$vars->{'patchviewerinstalled'} = 1;
|
||||
};
|
||||
|
||||
# When viewing an attachment, do not request credentials if we are on
|
||||
# the alternate host. Let view() decide when to call Bugzilla->login.
|
||||
if ($action eq "view")
|
||||
|
@ -249,10 +242,6 @@ sub view {
|
|||
|
||||
if (use_attachbase()) {
|
||||
$attachment = validateID(undef, 1);
|
||||
# Replace %bugid% by the ID of the bug the attachment belongs to, if present.
|
||||
my $attachbase = Bugzilla->params->{'attachment_base'};
|
||||
my $bug_id = $attachment->bug_id;
|
||||
$attachbase =~ s/%bugid%/$bug_id/;
|
||||
my $path = 'attachment.cgi?id=' . $attachment->id;
|
||||
# The user is allowed to override the content type of the attachment.
|
||||
if (defined $cgi->param('content_type')) {
|
||||
|
@ -260,23 +249,8 @@ sub view {
|
|||
}
|
||||
|
||||
# Make sure the attachment is served from the correct server.
|
||||
if ($cgi->self_url !~ /^\Q$attachbase\E/) {
|
||||
# We couldn't call Bugzilla->login earlier as we first had to make sure
|
||||
# we were not going to request credentials on the alternate host.
|
||||
Bugzilla->login();
|
||||
if (attachmentIsPublic($attachment)) {
|
||||
# No need for a token; redirect to attachment base.
|
||||
print $cgi->redirect(-location => $attachbase . $path);
|
||||
exit;
|
||||
} else {
|
||||
# Make sure the user can view the attachment.
|
||||
check_can_access($attachment);
|
||||
# Create a token and redirect.
|
||||
my $token = url_quote(issue_session_token($attachment->id));
|
||||
print $cgi->redirect(-location => $attachbase . "$path&t=$token");
|
||||
exit;
|
||||
}
|
||||
} else {
|
||||
my $bug_id = $attachment->bug_id;
|
||||
if ($cgi->url_is_attachment_base($bug_id)) {
|
||||
# No need to validate the token for public attachments. We cannot request
|
||||
# credentials as we are on the alternate host.
|
||||
if (!attachmentIsPublic($attachment)) {
|
||||
|
@ -296,7 +270,36 @@ sub view {
|
|||
delete_token($token);
|
||||
}
|
||||
}
|
||||
elsif ($cgi->url_is_attachment_base) {
|
||||
# If we come here, this means that each bug has its own host
|
||||
# for attachments, and that we are trying to view one attachment
|
||||
# using another bug's host. That's not desired.
|
||||
$cgi->redirect_to_urlbase;
|
||||
}
|
||||
else {
|
||||
# We couldn't call Bugzilla->login earlier as we first had to
|
||||
# make sure we were not going to request credentials on the
|
||||
# alternate host.
|
||||
Bugzilla->login();
|
||||
my $attachbase = Bugzilla->params->{'attachment_base'};
|
||||
# Replace %bugid% by the ID of the bug the attachment
|
||||
# belongs to, if present.
|
||||
$attachbase =~ s/\%bugid\%/$bug_id/;
|
||||
if (attachmentIsPublic($attachment)) {
|
||||
# No need for a token; redirect to attachment base.
|
||||
print $cgi->redirect(-location => $attachbase . $path);
|
||||
exit;
|
||||
} else {
|
||||
# Make sure the user can view the attachment.
|
||||
check_can_access($attachment);
|
||||
# Create a token and redirect.
|
||||
my $token = url_quote(issue_session_token($attachment->id));
|
||||
print $cgi->redirect(-location => $attachbase . "$path&t=$token");
|
||||
exit;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
do_ssl_redirect_if_required();
|
||||
# No alternate host is used. Request credentials if required.
|
||||
Bugzilla->login();
|
||||
$attachment = validateID();
|
||||
|
@ -308,12 +311,8 @@ sub view {
|
|||
|
||||
# Bug 111522: allow overriding content-type manually in the posted form
|
||||
# params.
|
||||
if (defined $cgi->param('content_type'))
|
||||
{
|
||||
$cgi->param('contenttypemethod', 'manual');
|
||||
$cgi->param('contenttypeentry', $cgi->param('content_type'));
|
||||
Bugzilla::Attachment->validate_content_type(THROW_ERROR);
|
||||
$contenttype = $cgi->param('content_type');
|
||||
if (defined $cgi->param('content_type')) {
|
||||
$contenttype = $attachment->_check_content_type($cgi->param('content_type'));
|
||||
}
|
||||
|
||||
# Return the appropriate HTTP response headers.
|
||||
|
@ -337,6 +336,14 @@ sub view {
|
|||
Encode::from_to($filename, 'utf-8', 'cp1251');
|
||||
}
|
||||
|
||||
# Don't send a charset header with attachments--they might not be UTF-8.
|
||||
# However, we do allow people to explicitly specify a charset if they
|
||||
# want.
|
||||
if ($contenttype !~ /\bcharset=/i) {
|
||||
# In order to prevent Apache from adding a charset, we have to send a
|
||||
# charset that's a single space.
|
||||
$cgi->charset(' ');
|
||||
}
|
||||
print $cgi->header(-type=>"$contenttype; name=\"$filename\"",
|
||||
-content_disposition=> "$disposition; filename=\"$filename\"",
|
||||
-content_length => $attachment->datasize);
|
||||
|
@ -392,39 +399,40 @@ sub viewall {
|
|||
|
||||
# Display a form for entering a new attachment.
|
||||
sub enter {
|
||||
# Retrieve and validate parameters
|
||||
my $bug = Bugzilla::Bug->check(scalar $cgi->param('bugid'));
|
||||
my $bugid = $bug->id;
|
||||
Bugzilla->user->can_edit_bug($bug, THROW_ERROR);
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $user = Bugzilla->user;
|
||||
# Retrieve and validate parameters
|
||||
my $bug = Bugzilla::Bug->check(scalar $cgi->param('bugid'));
|
||||
my $bugid = $bug->id;
|
||||
Bugzilla::Attachment->_check_bug($bug);
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $user = Bugzilla->user;
|
||||
|
||||
# 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 = "";
|
||||
if (!$user->in_group('editbugs', $bug->product_id)) {
|
||||
$canEdit = "AND submitter_id = " . $user->id;
|
||||
}
|
||||
my $attach_ids = $dbh->selectcol_arrayref("SELECT attach_id FROM attachments
|
||||
# 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 = "";
|
||||
if (!$user->in_group('editbugs', $bug->product_id)) {
|
||||
$canEdit = "AND submitter_id = " . $user->id;
|
||||
}
|
||||
my $attach_ids = $dbh->selectcol_arrayref("SELECT attach_id FROM attachments
|
||||
WHERE bug_id = ? AND isobsolete = 0 $canEdit
|
||||
ORDER BY attach_id", undef, $bugid);
|
||||
|
||||
# Define the variables and functions that will be passed to the UI template.
|
||||
$vars->{'bug'} = $bug;
|
||||
$vars->{'attachments'} = Bugzilla::Attachment->new_from_list($attach_ids);
|
||||
# Define the variables and functions that will be passed to the UI template.
|
||||
$vars->{'bug'} = $bug;
|
||||
$vars->{'attachments'} = Bugzilla::Attachment->new_from_list($attach_ids);
|
||||
|
||||
my $flag_types = Bugzilla::FlagType::match({'target_type' => 'attachment',
|
||||
'product_id' => $bug->product_id,
|
||||
'component_id' => $bug->component_id});
|
||||
$vars->{'flag_types'} = $flag_types;
|
||||
$vars->{'any_flags_requesteeble'} = grep($_->is_requesteeble, @$flag_types);
|
||||
$vars->{'token'} = issue_session_token('createattachment:');
|
||||
my $flag_types = Bugzilla::FlagType::match({'target_type' => 'attachment',
|
||||
'product_id' => $bug->product_id,
|
||||
'component_id' => $bug->component_id});
|
||||
$vars->{'flag_types'} = $flag_types;
|
||||
$vars->{'any_flags_requesteeble'} =
|
||||
grep { $_->is_requestable && $_->is_requesteeble } @$flag_types;
|
||||
$vars->{'token'} = issue_session_token('create_attachment:');
|
||||
|
||||
print $cgi->header();
|
||||
print $cgi->header();
|
||||
|
||||
# Generate and return the UI (HTML page) from the appropriate template.
|
||||
$template->process("attachment/create.html.tmpl", $vars)
|
||||
|| ThrowTemplateError($template->error());
|
||||
# Generate and return the UI (HTML page) from the appropriate template.
|
||||
$template->process("attachment/create.html.tmpl", $vars)
|
||||
|| ThrowTemplateError($template->error());
|
||||
}
|
||||
|
||||
# Insert a new attachment into the database.
|
||||
|
@ -437,26 +445,25 @@ sub insert {
|
|||
# Retrieve and validate parameters
|
||||
my $bug = Bugzilla::Bug->check(scalar $cgi->param('bugid'));
|
||||
my $bugid = $bug->id;
|
||||
Bugzilla->user->can_edit_bug($bug, THROW_ERROR);
|
||||
my ($timestamp) = Bugzilla->dbh->selectrow_array("SELECT NOW()");
|
||||
my ($timestamp) = $dbh->selectrow_array("SELECT NOW()");
|
||||
|
||||
# Detect if the user already used the same form to submit an attachment
|
||||
my $token = trim($cgi->param('token'));
|
||||
if ($token)
|
||||
{
|
||||
if ($token) {
|
||||
my ($creator_id, $date, $old_attach_id) = Bugzilla::Token::GetTokenData($token);
|
||||
unless ($creator_id && ($creator_id == $user->id) &&
|
||||
($old_attach_id =~ "^createattachment:"))
|
||||
unless ($creator_id
|
||||
&& ($creator_id == $user->id)
|
||||
&& ($old_attach_id =~ "^create_attachment:"))
|
||||
{
|
||||
# The token is invalid.
|
||||
ThrowUserError('token_does_not_exist');
|
||||
}
|
||||
|
||||
$old_attach_id =~ s/^createattachment://;
|
||||
if ($old_attach_id)
|
||||
{
|
||||
$vars->{bugid} = $bugid;
|
||||
$vars->{attachid} = $old_attach_id;
|
||||
$old_attach_id =~ s/^create_attachment://;
|
||||
|
||||
if ($old_attach_id) {
|
||||
$vars->{'bugid'} = $bugid;
|
||||
$vars->{'attachid'} = $old_attach_id;
|
||||
print $cgi->header();
|
||||
$template->process("attachment/cancel-create-dupe.html.tmpl", $vars)
|
||||
|| ThrowTemplateError($template->error());
|
||||
|
@ -464,68 +471,117 @@ sub insert {
|
|||
}
|
||||
}
|
||||
|
||||
my $attachment =
|
||||
Bugzilla::Attachment->create(THROW_ERROR, $bug, $user, $timestamp, $vars);
|
||||
# Check attachments the user tries to mark as obsolete.
|
||||
my @obsolete_attachments;
|
||||
if ($cgi->param('obsolete')) {
|
||||
my @obsolete = $cgi->param('obsolete');
|
||||
@obsolete_attachments = Bugzilla::Attachment->validate_obsolete($bug, \@obsolete);
|
||||
}
|
||||
|
||||
# Must be called before create() as it may alter $cgi->param('ispatch').
|
||||
my $content_type = Bugzilla::Attachment::get_content_type();
|
||||
|
||||
my $data = scalar $cgi->param('attachurl') || $cgi->upload('data');
|
||||
my $filename = '';
|
||||
$filename = scalar $cgi->upload('data') || $cgi->param('filename') unless $cgi->param('attachurl');
|
||||
if (scalar $cgi->param('text_attachment') !~ /^\s*$/so)
|
||||
{
|
||||
$data = $cgi->param('text_attachment');
|
||||
$filename = $cgi->param('description');
|
||||
}
|
||||
|
||||
if (Bugzilla->params->{utf8})
|
||||
{
|
||||
# CGI::upload() will probably return non-UTF8 string, so set UTF8 flag on
|
||||
# utf8::decode() and Encode::_utf8_on() do not work on tainted scalars...
|
||||
$filename = trick_taint_copy($filename);
|
||||
Encode::_utf8_on($filename);
|
||||
}
|
||||
|
||||
my $store_in_file = $cgi->param('bigfile');
|
||||
if (Bugzilla->params->{force_attach_bigfile})
|
||||
{
|
||||
# Force uploading into files instead of DB when force_attach_bigfile = On
|
||||
$store_in_file = 1;
|
||||
}
|
||||
|
||||
my $attachment = Bugzilla::Attachment->create(
|
||||
{bug => $bug,
|
||||
creation_ts => $timestamp,
|
||||
data => $data,
|
||||
description => scalar $cgi->param('description'),
|
||||
filename => $filename,
|
||||
ispatch => scalar $cgi->param('ispatch'),
|
||||
isprivate => scalar $cgi->param('isprivate'),
|
||||
isurl => scalar $cgi->param('attachurl'),
|
||||
mimetype => $content_type,
|
||||
store_in_file => $store_in_file,
|
||||
});
|
||||
|
||||
foreach my $obsolete_attachment (@obsolete_attachments) {
|
||||
$obsolete_attachment->set_is_obsolete(1);
|
||||
$obsolete_attachment->update($timestamp);
|
||||
}
|
||||
|
||||
my ($flags, $new_flags) = Bugzilla::Flag->extract_flags_from_cgi(
|
||||
$bug, $attachment, $vars, SKIP_REQUESTEE_ON_ERROR);
|
||||
$attachment->set_flags($flags, $new_flags);
|
||||
$attachment->update($timestamp);
|
||||
|
||||
# Insert a comment about the new attachment into the database.
|
||||
my $comment =
|
||||
"Created an attachment (id=" . $attachment->id . ")\n" .
|
||||
$attachment->description . "\n";
|
||||
$comment .= "\n" . $cgi->param('comment') if defined $cgi->param('comment');
|
||||
my $comment = $cgi->param('comment');
|
||||
$bug->add_comment($comment, { isprivate => $attachment->isprivate,
|
||||
type => CMT_ATTACHMENT_CREATED,
|
||||
work_time => scalar $cgi->param('work_time'),
|
||||
extra_data => $attachment->id });
|
||||
|
||||
my $work_time = scalar $cgi->param('work_time');
|
||||
$bug->add_comment($comment, { isprivate => $attachment->isprivate, work_time => $work_time });
|
||||
# Assign the bug to the user, if they are allowed to take it
|
||||
my $owner = "";
|
||||
if ($cgi->param('takebug') && $user->in_group('editbugs', $bug->product_id)) {
|
||||
# When taking a bug, we have to follow the workflow.
|
||||
my $bug_status = $cgi->param('bug_status') || '';
|
||||
($bug_status) = grep {$_->name eq $bug_status} @{$bug->status->can_change_to};
|
||||
|
||||
# Assign the bug to the user, if they are allowed to take it
|
||||
my $owner = "";
|
||||
if ($cgi->param('takebug') && $user->in_group('editbugs', $bug->product_id))
|
||||
{
|
||||
# When taking a bug, we have to follow the workflow.
|
||||
my $bug_status = $cgi->param('bug_status') || '';
|
||||
($bug_status) = grep {$_->name eq $bug_status} @{$bug->status->can_change_to};
|
||||
if ($bug_status && $bug_status->is_open
|
||||
&& ($bug_status->name ne 'UNCONFIRMED'
|
||||
|| $bug->product_obj->allows_unconfirmed))
|
||||
{
|
||||
$bug->set_status($bug_status->name);
|
||||
$bug->clear_resolution();
|
||||
}
|
||||
# Make sure the person we are taking the bug from gets mail.
|
||||
$owner = $bug->assigned_to->login;
|
||||
$bug->set_assigned_to($user);
|
||||
}
|
||||
$bug->update($timestamp);
|
||||
|
||||
if ($bug_status && $bug_status->is_open
|
||||
&& ($bug_status->name ne 'UNCONFIRMED' || $bug->product_obj->votes_to_confirm))
|
||||
{
|
||||
$bug->set_status($bug_status->name);
|
||||
$bug->clear_resolution();
|
||||
}
|
||||
# Make sure the person we are taking the bug from gets mail.
|
||||
$owner = $bug->assigned_to->login;
|
||||
$bug->set_assigned_to($user);
|
||||
}
|
||||
$bug->update($timestamp);
|
||||
if ($token) {
|
||||
trick_taint($token);
|
||||
$dbh->do('UPDATE tokens SET eventdata = ? WHERE token = ?', undef,
|
||||
("create_attachment:" . $attachment->id, $token));
|
||||
}
|
||||
|
||||
if ($token) {
|
||||
trick_taint($token);
|
||||
$dbh->do('UPDATE tokens SET eventdata = ? WHERE token = ?', undef,
|
||||
("createattachment:" . $attachment->id, $token));
|
||||
}
|
||||
$dbh->bz_commit_transaction;
|
||||
|
||||
$dbh->bz_commit_transaction;
|
||||
# Define the variables and functions that will be passed to the UI template.
|
||||
$vars->{'attachment'} = $attachment;
|
||||
# We cannot reuse the $bug object as delta_ts has eventually been updated
|
||||
# since the object was created.
|
||||
$vars->{'bugs'} = [new Bugzilla::Bug($bugid)];
|
||||
$vars->{'header_done'} = 1;
|
||||
$vars->{'contenttypemethod'} = $cgi->param('contenttypemethod');
|
||||
|
||||
# Define the variables and functions that will be passed to the UI template.
|
||||
$vars->{commentsilent} = $cgi->param('commentsilent');
|
||||
$vars->{mailrecipients} = {
|
||||
changer => $user->login,
|
||||
owner => $owner
|
||||
};
|
||||
$vars->{attachment} = $attachment;
|
||||
my $recipients = { 'changer' => $user->login, 'owner' => $owner };
|
||||
my $silent = $vars->{commentsilent} = $cgi->param('commentsilent') ? 1 : 0;
|
||||
$vars->{'sent_bugmail'} = Bugzilla::BugMail::Send($bugid, $recipients, $silent);
|
||||
|
||||
# We cannot reuse the $bug object as delta_ts has eventually been updated
|
||||
# since the object was created.
|
||||
$vars->{bugs} = [new Bugzilla::Bug($bugid)];
|
||||
$vars->{header_done} = 1;
|
||||
$vars->{contenttypemethod} = $cgi->param('contenttypemethod');
|
||||
$vars->{use_keywords} = 1 if Bugzilla::Keyword::keyword_count();
|
||||
|
||||
unless (Bugzilla->usage_mode == USAGE_MODE_EMAIL)
|
||||
{
|
||||
print $cgi->header();
|
||||
# Generate and return the UI (HTML page) from the appropriate template.
|
||||
$template->process("attachment/created.html.tmpl", $vars)
|
||||
|| ThrowTemplateError($template->error());
|
||||
}
|
||||
if (Bugzilla->usage_mode != USAGE_MODE_EMAIL)
|
||||
{
|
||||
print $cgi->header();
|
||||
# Generate and return the UI (HTML page) from the appropriate template.
|
||||
$template->process("attachment/created.html.tmpl", $vars)
|
||||
|| ThrowTemplateError($template->error());
|
||||
}
|
||||
}
|
||||
|
||||
# Displays a form for editing attachment properties.
|
||||
|
@ -533,66 +589,76 @@ sub insert {
|
|||
# is private and the user does not belong to the insider group.
|
||||
# Validations are done later when the user submits changes.
|
||||
sub edit {
|
||||
my $attachment = validateID();
|
||||
my $attachment = validateID();
|
||||
|
||||
my $bugattachments =
|
||||
Bugzilla::Attachment->get_attachments_by_bug($attachment->bug_id);
|
||||
# We only want attachment IDs.
|
||||
@$bugattachments = map { $_->id } @$bugattachments;
|
||||
my $bugattachments =
|
||||
Bugzilla::Attachment->get_attachments_by_bug($attachment->bug_id);
|
||||
# We only want attachment IDs.
|
||||
@$bugattachments = map { $_->id } @$bugattachments;
|
||||
|
||||
$vars->{'any_flags_requesteeble'} = grep($_->is_requesteeble, @{$attachment->flag_types});
|
||||
$vars->{'attachment'} = $attachment;
|
||||
$vars->{'attachments'} = $bugattachments;
|
||||
my $any_flags_requesteeble =
|
||||
grep { $_->is_requestable && $_->is_requesteeble } @{$attachment->flag_types};
|
||||
# Useful in case a flagtype is no longer requestable but a requestee
|
||||
# has been set before we turned off that bit.
|
||||
$any_flags_requesteeble ||= grep { $_->requestee_id } @{$attachment->flags};
|
||||
$vars->{'any_flags_requesteeble'} = $any_flags_requesteeble;
|
||||
$vars->{'attachment'} = $attachment;
|
||||
$vars->{'attachments'} = $bugattachments;
|
||||
|
||||
print $cgi->header();
|
||||
print $cgi->header();
|
||||
|
||||
# Generate and return the UI (HTML page) from the appropriate template.
|
||||
$template->process("attachment/edit.html.tmpl", $vars)
|
||||
|| ThrowTemplateError($template->error());
|
||||
# Generate and return the UI (HTML page) from the appropriate template.
|
||||
$template->process("attachment/edit.html.tmpl", $vars)
|
||||
|| ThrowTemplateError($template->error());
|
||||
}
|
||||
|
||||
# Updates an attachment record. Users with "editbugs" privileges, (or the
|
||||
# original attachment's submitter) can edit the attachment's description,
|
||||
# content type, ispatch and isobsolete flags, and statuses, and they can
|
||||
# also submit a comment that appears in the bug.
|
||||
# Updates an attachment record. Only users with "editbugs" privileges,
|
||||
# (or the original attachment's submitter) can edit the attachment.
|
||||
# Users cannot edit the content of the attachment itself.
|
||||
sub update {
|
||||
my $user = Bugzilla->user;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
# Start a transaction in preparation for updating the attachment.
|
||||
$dbh->bz_start_transaction();
|
||||
|
||||
# Retrieve and validate parameters
|
||||
my $attachment = validateID();
|
||||
my $bug = new Bugzilla::Bug($attachment->bug_id);
|
||||
$attachment->validate_can_edit($bug->product_id);
|
||||
Bugzilla->user->can_edit_bug($bug, THROW_ERROR);
|
||||
Bugzilla::Attachment->validate_description(THROW_ERROR);
|
||||
Bugzilla::Attachment->validate_is_patch(THROW_ERROR);
|
||||
Bugzilla::Attachment->validate_content_type(THROW_ERROR) unless $cgi->param('ispatch');
|
||||
$cgi->param('isobsolete', $cgi->param('isobsolete') ? 1 : 0);
|
||||
$cgi->param('isprivate', $cgi->param('isprivate') ? 1 : 0);
|
||||
my $bug = $attachment->bug;
|
||||
$attachment->_check_bug;
|
||||
my $can_edit = $attachment->validate_can_edit($bug->product_id);
|
||||
|
||||
# Now make sure the attachment has not been edited since we loaded the page.
|
||||
if (defined $cgi->param('delta_ts')
|
||||
&& $cgi->param('delta_ts') ne $attachment->modification_time)
|
||||
{
|
||||
($vars->{'operations'}) =
|
||||
Bugzilla::Bug::GetBugActivity($bug->id, $attachment->id, $cgi->param('delta_ts'));
|
||||
if ($can_edit) {
|
||||
$attachment->set_description(scalar $cgi->param('description'));
|
||||
$attachment->set_is_patch(scalar $cgi->param('ispatch'));
|
||||
$attachment->set_content_type(scalar $cgi->param('contenttypeentry'));
|
||||
$attachment->set_is_obsolete(scalar $cgi->param('isobsolete'));
|
||||
$attachment->set_is_private(scalar $cgi->param('isprivate'));
|
||||
$attachment->set_filename(scalar $cgi->param('filename'));
|
||||
|
||||
# The token contains the old modification_time. We need a new one.
|
||||
$cgi->param('token', issue_hash_token([$attachment->id, $attachment->modification_time]));
|
||||
# Now make sure the attachment has not been edited since we loaded the page.
|
||||
if (defined $cgi->param('delta_ts')
|
||||
&& $cgi->param('delta_ts') ne $attachment->modification_time)
|
||||
{
|
||||
($vars->{'operations'}) =
|
||||
Bugzilla::Bug::GetBugActivity($bug->id, $attachment->id, $cgi->param('delta_ts'));
|
||||
|
||||
# If the modification date changed but there is no entry in
|
||||
# the activity table, this means someone commented only.
|
||||
# In this case, there is no reason to midair.
|
||||
if (scalar(@{$vars->{'operations'}})) {
|
||||
$cgi->param('delta_ts', $attachment->modification_time);
|
||||
$vars->{'attachment'} = $attachment;
|
||||
# The token contains the old modification_time. We need a new one.
|
||||
$cgi->param('token', issue_hash_token([$attachment->id, $attachment->modification_time]));
|
||||
|
||||
print $cgi->header();
|
||||
# Warn the user about the mid-air collision and ask them what to do.
|
||||
$template->process("attachment/midair.html.tmpl", $vars)
|
||||
|| ThrowTemplateError($template->error());
|
||||
exit;
|
||||
# If the modification date changed but there is no entry in
|
||||
# the activity table, this means someone commented only.
|
||||
# In this case, there is no reason to midair.
|
||||
if (scalar(@{$vars->{'operations'}})) {
|
||||
$cgi->param('delta_ts', $attachment->modification_time);
|
||||
$vars->{'attachment'} = $attachment;
|
||||
|
||||
print $cgi->header();
|
||||
# Warn the user about the mid-air collision and ask them what to do.
|
||||
$template->process("attachment/midair.html.tmpl", $vars)
|
||||
|| ThrowTemplateError($template->error());
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -601,136 +667,45 @@ sub update {
|
|||
my $token = $cgi->param('token');
|
||||
check_hash_token($token, [$attachment->id, $attachment->modification_time]);
|
||||
|
||||
# If the submitter of the attachment is not in the insidergroup,
|
||||
# be sure that he cannot overwrite the private bit.
|
||||
# This check must be done before calling Bugzilla::Flag*::validate(),
|
||||
# because they will look at the private bit when checking permissions.
|
||||
# XXX - This is a ugly hack. Ideally, we shouldn't have to look at the
|
||||
# old private bit twice (first here, and then below again), but this is
|
||||
# the less risky change.
|
||||
unless ($user->is_insider) {
|
||||
$cgi->param('isprivate', $attachment->isprivate);
|
||||
}
|
||||
|
||||
# If the user submitted a comment while editing the attachment,
|
||||
# add the comment to the bug. Do this after having validated isprivate!
|
||||
if ($cgi->param('comment')) {
|
||||
# Prepend a string to the comment to let users know that the comment came
|
||||
# from the "edit attachment" screen.
|
||||
my $comment = "(From update of attachment " . $attachment->id . ")\n" .
|
||||
$cgi->param('comment');
|
||||
|
||||
$bug->add_comment($comment, { isprivate => $cgi->param('isprivate') });
|
||||
my $comment = $cgi->param('comment');
|
||||
if (trim($comment)) {
|
||||
$bug->add_comment($comment, { isprivate => $attachment->isprivate,
|
||||
type => CMT_ATTACHMENT_UPDATED,
|
||||
work_time => scalar $cgi->param('work_time'),
|
||||
extra_data => $attachment->id });
|
||||
}
|
||||
|
||||
# The order of these function calls is important, as Flag::validate
|
||||
# assumes User::match_field has ensured that the values in the
|
||||
# requestee fields are legitimate user email addresses.
|
||||
Bugzilla::User::match_field($cgi, {
|
||||
'^requestee(_type)?-(\d+)$' => { 'type' => 'multi' }
|
||||
});
|
||||
Bugzilla::Flag::validate($bug->id, $attachment->id);
|
||||
|
||||
# Start a transaction in preparation for updating the attachment.
|
||||
$dbh->bz_start_transaction();
|
||||
|
||||
# Quote the description and content type for use in the SQL UPDATE statement.
|
||||
my $description = $cgi->param('description');
|
||||
my $contenttype = $cgi->param('contenttype');
|
||||
my $filename = $cgi->param('filename');
|
||||
# we can detaint this way thanks to placeholders
|
||||
trick_taint($description);
|
||||
trick_taint($contenttype);
|
||||
trick_taint($filename);
|
||||
if ($can_edit) {
|
||||
my ($flags, $new_flags) =
|
||||
Bugzilla::Flag->extract_flags_from_cgi($bug, $attachment, $vars);
|
||||
$attachment->set_flags($flags, $new_flags);
|
||||
}
|
||||
|
||||
# Figure out when the changes were made.
|
||||
my ($timestamp) = $dbh->selectrow_array("SELECT NOW()");
|
||||
my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
|
||||
|
||||
# Update flags. We have to do this before committing changes
|
||||
# to attachments so that we can delete pending requests if the user
|
||||
# is obsoleting this attachment without deleting any requests
|
||||
# the user submits at the same time.
|
||||
Bugzilla::Flag->process($bug, $attachment, $timestamp, $vars);
|
||||
|
||||
# Update the attachment record in the database.
|
||||
$dbh->do("UPDATE attachments
|
||||
SET description = ?,
|
||||
mimetype = ?,
|
||||
filename = ?,
|
||||
ispatch = ?,
|
||||
isobsolete = ?,
|
||||
isprivate = ?,
|
||||
modification_time = ?
|
||||
WHERE attach_id = ?",
|
||||
undef, ($description, $contenttype, $filename,
|
||||
$cgi->param('ispatch'), $cgi->param('isobsolete'),
|
||||
$cgi->param('isprivate'), $timestamp, $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 ($can_edit) {
|
||||
my $changes = $attachment->update($timestamp);
|
||||
# If there are changes, we updated delta_ts in the DB. We have to
|
||||
# reflect this change in the bug object.
|
||||
$bug->{delta_ts} = $timestamp if scalar(keys %$changes);
|
||||
}
|
||||
|
||||
if ($updated) {
|
||||
$dbh->do("UPDATE bugs SET delta_ts = ? WHERE bug_id = ?", undef,
|
||||
$timestamp, $bug->id);
|
||||
}
|
||||
# Commit the comment, if any.
|
||||
$bug->update($timestamp);
|
||||
|
||||
# Commit the transaction now that we are finished updating the database.
|
||||
$dbh->bz_commit_transaction();
|
||||
|
||||
# Commit the comment, if any.
|
||||
$bug->update();
|
||||
|
||||
# Define the variables and functions that will be passed to the UI template.
|
||||
$vars->{commentsilent} = $cgi->param('commentsilent');
|
||||
$vars->{'mailrecipients'} = { 'changer' => Bugzilla->user->login };
|
||||
$vars->{'attachment'} = $attachment;
|
||||
# We cannot reuse the $bug object as delta_ts has eventually been updated
|
||||
# since the object was created.
|
||||
$vars->{'bugs'} = [new Bugzilla::Bug($bug->id)];
|
||||
$vars->{'bugs'} = [$bug];
|
||||
$vars->{'header_done'} = 1;
|
||||
$vars->{'use_keywords'} = 1 if Bugzilla::Keyword::keyword_count();
|
||||
my $silent = $vars->{commentsilent} = $cgi->param('commentsilent') ? 1 : 0;
|
||||
$vars->{'sent_bugmail'} =
|
||||
Bugzilla::BugMail::Send($bug->id, { 'changer' => $user->login }, $silent);
|
||||
|
||||
print $cgi->header();
|
||||
|
||||
|
@ -756,7 +731,7 @@ sub delete_attachment {
|
|||
|
||||
# Make sure the administrator is allowed to edit this attachment.
|
||||
my $attachment = validateID();
|
||||
Bugzilla->user->can_edit_bug($attachment->bug, THROW_ERROR);
|
||||
Bugzilla::Attachment->_check_bug($attachment->bug);
|
||||
|
||||
$attachment->datasize || ThrowUserError('attachment_removed');
|
||||
|
||||
|
@ -766,7 +741,7 @@ sub delete_attachment {
|
|||
my ($creator_id, $date, $event) = Bugzilla::Token::GetTokenData($token);
|
||||
unless ($creator_id
|
||||
&& ($creator_id == $user->id)
|
||||
&& ($event eq 'attachment' . $attachment->id))
|
||||
&& ($event eq 'delete_attachment' . $attachment->id))
|
||||
{
|
||||
# The token is invalid.
|
||||
ThrowUserError('token_does_not_exist');
|
||||
|
@ -779,8 +754,6 @@ sub delete_attachment {
|
|||
$vars->{'attachment'} = $attachment;
|
||||
$vars->{'date'} = $date;
|
||||
$vars->{'reason'} = clean_text($cgi->param('reason') || '');
|
||||
$vars->{commentsilent} = $cgi->param('commentsilent');
|
||||
$vars->{'mailrecipients'} = { 'changer' => $user->login };
|
||||
|
||||
$template->process("attachment/delete_reason.txt.tmpl", $vars, \$msg)
|
||||
|| ThrowTemplateError($template->error());
|
||||
|
@ -803,14 +776,17 @@ sub delete_attachment {
|
|||
# Required to display the bug the deleted attachment belongs to.
|
||||
$vars->{'bugs'} = [$bug];
|
||||
$vars->{'header_done'} = 1;
|
||||
$vars->{'use_keywords'} = 1 if Bugzilla::Keyword::keyword_count();
|
||||
|
||||
my $silent = $vars->{commentsilent} = $cgi->param('commentsilent') ? 1 : 0;
|
||||
$vars->{'sent_bugmail'} =
|
||||
Bugzilla::BugMail::Send($bug->id, { 'changer' => $user->login }, $silent);
|
||||
|
||||
$template->process("attachment/updated.html.tmpl", $vars)
|
||||
|| ThrowTemplateError($template->error());
|
||||
}
|
||||
else {
|
||||
# Create a token.
|
||||
$token = issue_session_token('attachment' . $attachment->id);
|
||||
$token = issue_session_token('delete_attachment' . $attachment->id);
|
||||
|
||||
$vars->{'a'} = $attachment;
|
||||
$vars->{'token'} = $token;
|
||||
|
|
97
buglist.cgi
97
buglist.cgi
|
@ -180,7 +180,6 @@ my $serverpush =
|
|||
|| $cgi->param('serverpush');
|
||||
|
||||
my $order = $cgi->param('order') || "";
|
||||
my $order_from_cookie = 0; # True if $order set using the LASTORDER cookie
|
||||
|
||||
# The params object to use for the actual query itself
|
||||
my $params;
|
||||
|
@ -660,7 +659,7 @@ if (trim($votes) && !grep($_ eq 'votes', @displaycolumns)) {
|
|||
|
||||
# Remove the timetracking columns if they are not a part of the group
|
||||
# (happens if a user had access to time tracking and it was revoked/disabled)
|
||||
if (!Bugzilla->user->in_group(Bugzilla->params->{"timetrackinggroup"})) {
|
||||
if (!Bugzilla->user->is_timetracker) {
|
||||
@displaycolumns = grep($_ ne 'estimated_time', @displaycolumns);
|
||||
@displaycolumns = grep($_ ne 'remaining_time', @displaycolumns);
|
||||
@displaycolumns = grep($_ ne 'actual_time', @displaycolumns);
|
||||
|
@ -684,12 +683,7 @@ if (grep('relevance', @displaycolumns) && !$fulltext) {
|
|||
# Severity, priority, resolution and status are required for buglist
|
||||
# CSS classes.
|
||||
my @selectcolumns = ("bug_id", "bug_severity", "priority", "bug_status",
|
||||
"resolution");
|
||||
|
||||
# if using classification, we also need to look in product.classification_id
|
||||
if (Bugzilla->params->{"useclassification"}) {
|
||||
push (@selectcolumns,"product");
|
||||
}
|
||||
"resolution", "product");
|
||||
|
||||
# remaining and actual_time are required for percentage_complete calculation:
|
||||
if (lsearch(\@displaycolumns, "percentage_complete") >= 0) {
|
||||
|
@ -710,13 +704,14 @@ foreach my $item (@realname_fields) {
|
|||
}
|
||||
|
||||
# Display columns are selected because otherwise we could not display them.
|
||||
push (@selectcolumns, @displaycolumns);
|
||||
foreach my $col (@displaycolumns) {
|
||||
push (@selectcolumns, $col) if !grep($_ eq $col, @selectcolumns);
|
||||
}
|
||||
|
||||
# If the user is editing multiple bugs, we also make sure to select the product
|
||||
# and status because the values of those fields determine what options the user
|
||||
# If the user is editing multiple bugs, we also make sure to select the
|
||||
# status, because the values of that field determines what options the user
|
||||
# has for modifying the bugs.
|
||||
if ($dotweak) {
|
||||
push(@selectcolumns, "product") if !grep($_ eq 'product', @selectcolumns);
|
||||
push(@selectcolumns, "bug_status") if !grep($_ eq 'bug_status', @selectcolumns);
|
||||
}
|
||||
|
||||
|
@ -766,8 +761,6 @@ if (!$order || $order =~ /^reuse/i) {
|
|||
# Cookies from early versions of Specific Search included this text,
|
||||
# which is now invalid.
|
||||
$order =~ s/ LIMIT 200//;
|
||||
|
||||
$order_from_cookie = 1;
|
||||
}
|
||||
else {
|
||||
$order = ''; # Remove possible "reuse" identifier as unnecessary
|
||||
|
@ -795,7 +788,8 @@ if ($order) {
|
|||
last ORDER;
|
||||
};
|
||||
do {
|
||||
my @order;
|
||||
my (@order, @invalid_fragments);
|
||||
|
||||
# A custom list of columns. Make sure each column is valid.
|
||||
foreach my $fragment (split(/,/, $order)) {
|
||||
$fragment = trim($fragment);
|
||||
|
@ -817,16 +811,14 @@ if ($order) {
|
|||
push(@order, "$column_name$direction");
|
||||
}
|
||||
else {
|
||||
my $vars = { fragment => $fragment };
|
||||
if ($order_from_cookie) {
|
||||
$cgi->remove_cookie('LASTORDER');
|
||||
ThrowCodeError("invalid_column_name_cookie", $vars);
|
||||
}
|
||||
else {
|
||||
ThrowCodeError("invalid_column_name_form", $vars);
|
||||
}
|
||||
push(@invalid_fragments, $fragment);
|
||||
}
|
||||
}
|
||||
if (scalar @invalid_fragments) {
|
||||
$vars->{'message'} = 'invalid_column_name';
|
||||
$vars->{'invalid_fragments'} = \@invalid_fragments;
|
||||
}
|
||||
|
||||
$order = join(",", @order);
|
||||
# Now that we have checked that all columns in the order are valid,
|
||||
# detaint the order string.
|
||||
|
@ -1042,6 +1034,17 @@ $vars->{'displaycolumns'} = \@displaycolumns;
|
|||
$vars->{'openstates'} = [BUG_STATE_OPEN];
|
||||
$vars->{'closedstates'} = [map {$_->name} closed_bug_statuses()];
|
||||
|
||||
# The iCal file needs priorities ordered from 1 to 9 (highest to lowest)
|
||||
# If there are more than 9 values, just make all the lower ones 9
|
||||
if ($format->{'extension'} eq 'ics') {
|
||||
my $n = 1;
|
||||
$vars->{'ics_priorities'} = {};
|
||||
my $priorities = get_legal_field_values('priority');
|
||||
foreach my $p (@$priorities) {
|
||||
$vars->{'ics_priorities'}->{$p} = ($n > 9) ? 9 : $n++;
|
||||
}
|
||||
}
|
||||
|
||||
# The list of query fields in URL query string format, used when creating
|
||||
# URLs to the same query results page with different parameters (such as
|
||||
# a different sort order or when taking some action on the set of query
|
||||
|
@ -1079,6 +1082,25 @@ $vars->{'splitheader'} = $cgi->cookie('SPLITHEADER') ? 1 : 0;
|
|||
$vars->{'quip'} = GetQuip();
|
||||
$vars->{'currenttime'} = localtime(time());
|
||||
|
||||
# See if there's only one product in all the results (or only one product
|
||||
# that we searched for), which allows us to provide more helpful links.
|
||||
my @products = keys %$bugproducts;
|
||||
my $one_product;
|
||||
if (scalar(@products) == 1) {
|
||||
$one_product = new Bugzilla::Product({ name => $products[0] });
|
||||
}
|
||||
# This is used in the "Zarroo Boogs" case.
|
||||
elsif (my @product_input = $cgi->param('product')) {
|
||||
if (scalar(@product_input) == 1 and $product_input[0] ne '') {
|
||||
$one_product = new Bugzilla::Product({ name => $cgi->param('product') });
|
||||
}
|
||||
}
|
||||
# We only want the template to use it if the user can actually
|
||||
# enter bugs against it.
|
||||
if ($one_product && Bugzilla->user->can_enter_product($one_product)) {
|
||||
$vars->{'one_product'} = $one_product;
|
||||
}
|
||||
|
||||
# The following variables are used when the user is making changes to multiple bugs.
|
||||
if ($dotweak && scalar @bugs) {
|
||||
if (!$vars->{'caneditbugs'}) {
|
||||
|
@ -1088,7 +1110,6 @@ if ($dotweak && scalar @bugs) {
|
|||
object => 'multiple_bugs'});
|
||||
}
|
||||
$vars->{'dotweak'} = 1;
|
||||
$vars->{'use_keywords'} = 1 if Bugzilla::Keyword::keyword_count();
|
||||
|
||||
# issue_session_token needs to write to the master DB.
|
||||
Bugzilla->switch_to_main_db();
|
||||
|
@ -1131,35 +1152,15 @@ if ($dotweak && scalar @bugs) {
|
|||
$vars->{'new_bug_statuses'} = Bugzilla::Status->new_from_list($bug_status_ids);
|
||||
|
||||
# The groups the user belongs to and which are editable for the given buglist.
|
||||
my @products = keys %$bugproducts;
|
||||
$vars->{'groups'} = GetGroups(\@products);
|
||||
|
||||
# If all bugs being changed are in the same product, the user can change
|
||||
# their version and component, so generate a list of products, a list of
|
||||
# versions for the product (if there is only one product on the list of
|
||||
# products), and a list of components for the product.
|
||||
# Generate lists of components, versions and milestones common for all selected products.
|
||||
$_ = Bugzilla::Product->new({name => $_}) for @products;
|
||||
my %h;
|
||||
$vars->{components} = [ map($_->name, @{$products[0]->components}) ];
|
||||
for my $i (1..$#products)
|
||||
{
|
||||
%h = map { $_->name => 1 } @{$products[$i]->components};
|
||||
@{$vars->{components}} = grep { $h{$_} } @{$vars->{components}};
|
||||
}
|
||||
$vars->{versions} = [ map($_->name, @{$products[0]->versions}) ];
|
||||
for my $i (1..$#products)
|
||||
{
|
||||
%h = map { $_->name => 1 } @{$products[$i]->versions};
|
||||
@{$vars->{versions}} = grep { $h{$_} } @{$vars->{versions}};
|
||||
}
|
||||
$vars->{components} = intersect(map { [ map { $_->name } @{ $_->components } ] } @products);
|
||||
$vars->{versions} = intersect(map { [ map { $_->name } @{ $_->versions } ] } @products);
|
||||
if (Bugzilla->params->{usetargetmilestone})
|
||||
{
|
||||
$vars->{targetmilestones} = [ map($_->name, @{$products[0]->milestones}) ];
|
||||
for my $i (1..$#products)
|
||||
{
|
||||
%h = map { $_->name => 1 } @{$products[$i]->milestones};
|
||||
@{$vars->{targetmilestones}} = grep { $h{$_} } @{$vars->{targetmilestones}};
|
||||
}
|
||||
$vars->{targetmilestones} = intersect(map { [ map { $_->name } @{ $_->milestones } ] } @products);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -43,6 +43,9 @@
|
|||
<!ELEMENT everconfirmed (#PCDATA)>
|
||||
<!ELEMENT cc (#PCDATA)>
|
||||
<!ELEMENT group (#PCDATA)>
|
||||
<!ATTLIST group
|
||||
id CDATA #REQUIRED
|
||||
>
|
||||
<!ELEMENT estimated_time (#PCDATA)>
|
||||
<!ELEMENT remaining_time (#PCDATA)>
|
||||
<!ELEMENT actual_time (#PCDATA)>
|
||||
|
@ -52,6 +55,7 @@
|
|||
encoding (base64) #IMPLIED
|
||||
isprivate (0|1) #IMPLIED
|
||||
>
|
||||
<!ELEMENT commentid (#PCDATA)>
|
||||
<!ELEMENT who (#PCDATA)>
|
||||
<!ELEMENT bug_when (#PCDATA)>
|
||||
<!ELEMENT work_time (#PCDATA)>
|
||||
|
@ -61,9 +65,11 @@
|
|||
isobsolete (0|1) #IMPLIED
|
||||
ispatch (0|1) #IMPLIED
|
||||
isprivate (0|1) #IMPLIED
|
||||
isurl (0|1) #IMPLIED
|
||||
>
|
||||
<!ELEMENT attachid (#PCDATA)>
|
||||
<!ELEMENT date (#PCDATA)>
|
||||
<!ELEMENT delta_ts (#PCDATA)>
|
||||
<!ELEMENT desc (#PCDATA)>
|
||||
<!ELEMENT filename (#PCDATA)>
|
||||
<!ELEMENT type (#PCDATA)>
|
||||
|
@ -75,6 +81,8 @@
|
|||
<!ELEMENT flag EMPTY>
|
||||
<!ATTLIST flag
|
||||
name CDATA #REQUIRED
|
||||
id CDATA #REQUIRED
|
||||
type_id CDATA
|
||||
status CDATA #REQUIRED
|
||||
setter CDATA #IMPLIED
|
||||
requestee CDATA #IMPLIED
|
||||
|
|
106
chart.cgi
106
chart.cgi
|
@ -20,6 +20,7 @@
|
|||
#
|
||||
# Contributor(s): Gervase Markham <gerv@gerv.net>
|
||||
# Lance Larsh <lance.larsh@oracle.com>
|
||||
# Frédéric Buclin <LpSolit@gmail.com>
|
||||
|
||||
# Glossary:
|
||||
# series: An individual, defined set of data plotted over time.
|
||||
|
@ -47,11 +48,13 @@ use lib qw(. lib);
|
|||
|
||||
use Bugzilla;
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::CGI;
|
||||
use Bugzilla::Error;
|
||||
use Bugzilla::Util;
|
||||
use Bugzilla::Chart;
|
||||
use Bugzilla::Series;
|
||||
use Bugzilla::User;
|
||||
use Bugzilla::Token;
|
||||
|
||||
# For most scripts we don't make $cgi and $template global variables. But
|
||||
# when preparing Bugzilla for mod_perl, this script used these
|
||||
|
@ -60,6 +63,13 @@ use Bugzilla::User;
|
|||
local our $cgi = Bugzilla->cgi;
|
||||
local our $template = Bugzilla->template;
|
||||
local our $vars = {};
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
my $user = Bugzilla->login(LOGIN_REQUIRED);
|
||||
|
||||
if (!Bugzilla->feature('new_charts')) {
|
||||
ThrowCodeError('feature_disabled', { feature => 'new_charts' });
|
||||
}
|
||||
|
||||
# Go back to query.cgi if we are adding a boolean chart parameter.
|
||||
if (grep(/^cmd-/, $cgi->param())) {
|
||||
|
@ -92,15 +102,13 @@ if ($action eq "search") {
|
|||
exit;
|
||||
}
|
||||
|
||||
my $user = Bugzilla->login(LOGIN_REQUIRED);
|
||||
|
||||
Bugzilla->user->in_group(Bugzilla->params->{"chartgroup"})
|
||||
$user->in_group(Bugzilla->params->{"chartgroup"})
|
||||
|| ThrowUserError("auth_failure", {group => Bugzilla->params->{"chartgroup"},
|
||||
action => "use",
|
||||
object => "charts"});
|
||||
|
||||
# Only admins may create public queries
|
||||
Bugzilla->user->in_group('admin') || $cgi->delete('public');
|
||||
$user->in_group('admin') || $cgi->delete('public');
|
||||
|
||||
# All these actions relate to chart construction.
|
||||
if ($action =~ /^(assemble|add|remove|sum|subscribe|unsubscribe)$/) {
|
||||
|
@ -141,33 +149,23 @@ elsif ($action eq "create") {
|
|||
|
||||
my $series = new Bugzilla::Series($cgi);
|
||||
|
||||
if (!$series->existsInDatabase()) {
|
||||
$series->writeToDatabase();
|
||||
$vars->{'message'} = "series_created";
|
||||
}
|
||||
else {
|
||||
ThrowUserError("series_already_exists", {'series' => $series});
|
||||
}
|
||||
ThrowUserError("series_already_exists", {'series' => $series})
|
||||
if $series->existsInDatabase;
|
||||
|
||||
$series->writeToDatabase();
|
||||
$vars->{'message'} = "series_created";
|
||||
$vars->{'series'} = $series;
|
||||
|
||||
print $cgi->header();
|
||||
$template->process("global/message.html.tmpl", $vars)
|
||||
|| ThrowTemplateError($template->error());
|
||||
my $chart = new Bugzilla::Chart($cgi);
|
||||
view($chart);
|
||||
}
|
||||
elsif ($action eq "edit") {
|
||||
detaint_natural($series_id) || ThrowCodeError("invalid_series_id");
|
||||
assertCanEdit($series_id);
|
||||
|
||||
my $series = new Bugzilla::Series($series_id);
|
||||
|
||||
my $series = assertCanEdit($series_id);
|
||||
edit($series);
|
||||
}
|
||||
elsif ($action eq "alter") {
|
||||
# This is the "commit" action for editing a series
|
||||
detaint_natural($series_id) || ThrowCodeError("invalid_series_id");
|
||||
assertCanEdit($series_id);
|
||||
|
||||
# XXX - This should be replaced by $series->set_foo() methods.
|
||||
my $series = new Bugzilla::Series($cgi);
|
||||
|
||||
# We need to check if there is _another_ series in the database with
|
||||
|
@ -186,6 +184,48 @@ elsif ($action eq "alter") {
|
|||
|
||||
edit($series);
|
||||
}
|
||||
elsif ($action eq "confirm-delete") {
|
||||
$vars->{'series'} = assertCanEdit($series_id);
|
||||
|
||||
print $cgi->header();
|
||||
$template->process("reports/delete-series.html.tmpl", $vars)
|
||||
|| ThrowTemplateError($template->error());
|
||||
}
|
||||
elsif ($action eq "delete") {
|
||||
my $series = assertCanEdit($series_id);
|
||||
my $token = $cgi->param('token');
|
||||
check_hash_token($token, [$series->id, $series->name]);
|
||||
|
||||
$dbh->bz_start_transaction();
|
||||
|
||||
$series->remove_from_db();
|
||||
# Remove (sub)categories which no longer have any series.
|
||||
foreach my $cat qw(category subcategory) {
|
||||
my $is_used = $dbh->selectrow_array("SELECT COUNT(*) FROM series WHERE $cat = ?",
|
||||
undef, $series->{"${cat}_id"});
|
||||
if (!$is_used) {
|
||||
$dbh->do('DELETE FROM series_categories WHERE id = ?',
|
||||
undef, $series->{"${cat}_id"});
|
||||
}
|
||||
}
|
||||
$dbh->bz_commit_transaction();
|
||||
|
||||
$vars->{'message'} = "series_deleted";
|
||||
$vars->{'series'} = $series;
|
||||
view();
|
||||
}
|
||||
elsif ($action eq "convert_search") {
|
||||
my $saved_search = $cgi->param('series_from_search') || '';
|
||||
my ($query) = grep { $_->name eq $saved_search } @{ $user->queries };
|
||||
my $url = '';
|
||||
if ($query) {
|
||||
my $params = new Bugzilla::CGI($query->edit_link);
|
||||
# These two parameters conflict with the one below.
|
||||
$url = $params->canonicalise_query('format', 'query_format');
|
||||
$url = '&' . html_quote($url);
|
||||
}
|
||||
print $cgi->redirect(-location => correct_urlbase() . "query.cgi?format=create-series$url");
|
||||
}
|
||||
else {
|
||||
ThrowCodeError("unknown_action");
|
||||
}
|
||||
|
@ -208,28 +248,29 @@ sub getSelectedLines {
|
|||
|
||||
# Check if the user is the owner of series_id or is an admin.
|
||||
sub assertCanEdit {
|
||||
my ($series_id) = @_;
|
||||
my $series_id = shift;
|
||||
my $user = Bugzilla->user;
|
||||
|
||||
return if $user->in_group('admin');
|
||||
my $series = new Bugzilla::Series($series_id)
|
||||
|| ThrowCodeError('invalid_series_id');
|
||||
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $iscreator = $dbh->selectrow_array("SELECT CASE WHEN creator = ? " .
|
||||
"THEN 1 ELSE 0 END FROM series " .
|
||||
"WHERE series_id = ?", undef,
|
||||
$user->id, $series_id);
|
||||
$iscreator || ThrowUserError("illegal_series_edit");
|
||||
if (!$user->in_group('admin') && $series->{creator_id} != $user->id) {
|
||||
ThrowUserError('illegal_series_edit');
|
||||
}
|
||||
|
||||
return $series;
|
||||
}
|
||||
|
||||
# Check if the user is permitted to create this series with these parameters.
|
||||
sub assertCanCreate {
|
||||
my ($cgi) = shift;
|
||||
my $user = Bugzilla->user;
|
||||
|
||||
Bugzilla->user->in_group("editbugs") || ThrowUserError("illegal_series_creation");
|
||||
$user->in_group("editbugs") || ThrowUserError("illegal_series_creation");
|
||||
|
||||
# Check permission for frequency
|
||||
my $min_freq = 7;
|
||||
if ($cgi->param('frequency') < $min_freq && !Bugzilla->user->in_group("admin")) {
|
||||
if ($cgi->param('frequency') < $min_freq && !$user->in_group("admin")) {
|
||||
ThrowUserError("illegal_frequency", { 'minimum' => $min_freq });
|
||||
}
|
||||
}
|
||||
|
@ -261,7 +302,6 @@ sub edit {
|
|||
my $series = shift;
|
||||
|
||||
$vars->{'category'} = Bugzilla::Chart::getVisibleSeries();
|
||||
$vars->{'creator'} = new Bugzilla::User($series->{'creator'});
|
||||
$vars->{'default'} = $series;
|
||||
|
||||
print $cgi->header();
|
||||
|
|
|
@ -53,21 +53,24 @@ BEGIN { chdir dirname($0); }
|
|||
use lib qw(. lib);
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::Install::Requirements;
|
||||
use Bugzilla::Install::Util qw(install_string get_version_and_os
|
||||
get_console_locale
|
||||
prevent_windows_dialog_boxes);
|
||||
use Bugzilla::Install::Util qw(install_string get_version_and_os init_console);
|
||||
|
||||
######################################################################
|
||||
# Live Code
|
||||
######################################################################
|
||||
|
||||
prevent_windows_dialog_boxes();
|
||||
# When we're running at the command line, we need to pick the right
|
||||
# language before ever displaying any string.
|
||||
$ENV{'HTTP_ACCEPT_LANGUAGE'} ||= get_console_locale();
|
||||
init_console();
|
||||
# Required for displaying strings from install_string, which are always
|
||||
# in UTF-8, in every language. For other scripts, Bugzilla::init_page
|
||||
# handles this, but here we just need to assume that checksetup.pl output
|
||||
# is always UTF-8 in order for install_string to work properly in other
|
||||
# languages.
|
||||
binmode STDOUT, ':utf8';
|
||||
|
||||
my %switch;
|
||||
GetOptions(\%switch, 'help|h|?', 'check-modules', 'no-templates|t',
|
||||
GetOptions(\%switch, 'help|h|?', 'check-modules', 'no-templates|t', 'no-chmod|r',
|
||||
'verbose|v|no-silent', 'make-admin=s',
|
||||
'reset-password=s', 'version|V');
|
||||
|
||||
|
@ -99,6 +102,7 @@ exit if $switch{'check-modules'};
|
|||
# get a cryptic perl error about the missing module.
|
||||
|
||||
require Bugzilla;
|
||||
require Bugzilla::User;
|
||||
|
||||
require Bugzilla::Config;
|
||||
import Bugzilla::Config qw(:admin);
|
||||
|
@ -166,7 +170,8 @@ Bugzilla::Template::precompile_templates(!$silent)
|
|||
# Set proper rights (--CHMOD--)
|
||||
###########################################################################
|
||||
|
||||
fix_all_file_permissions(!$silent);
|
||||
fix_all_file_permissions(!$silent)
|
||||
unless $switch{'no-chmod'};
|
||||
|
||||
###########################################################################
|
||||
# Check GraphViz setup
|
||||
|
@ -199,6 +204,9 @@ Bugzilla::Install::DB::update_table_definitions(\%old_params);
|
|||
|
||||
Bugzilla::Install::update_system_groups();
|
||||
|
||||
# "Log In" as the fake superuser who can do everything.
|
||||
Bugzilla->set_user(Bugzilla::User->super_user);
|
||||
|
||||
###########################################################################
|
||||
# Create --SETTINGS-- users can adjust
|
||||
###########################################################################
|
||||
|
@ -216,12 +224,12 @@ Bugzilla::Install::reset_password($switch{'reset-password'})
|
|||
if $switch{'reset-password'};
|
||||
|
||||
###########################################################################
|
||||
# Create default Product and Classification
|
||||
# Create default Product
|
||||
###########################################################################
|
||||
|
||||
Bugzilla::Install::create_default_product();
|
||||
|
||||
Bugzilla::Hook::process('install-before_final_checks', {'silent' => $silent });
|
||||
Bugzilla::Hook::process('install_before_final_checks', { silent => $silent });
|
||||
|
||||
###########################################################################
|
||||
# Final checks
|
||||
|
@ -405,6 +413,10 @@ from one version of Bugzilla to another.
|
|||
|
||||
The code for this is in L<Bugzilla::Install::DB/update_table_definitions>.
|
||||
|
||||
This includes creating the default Classification (using
|
||||
L<Bugzilla::Install/create_default_classification>) and setting up all
|
||||
the foreign keys for all tables, using L<Bugzilla::DB/bz_setup_foreign_keys>.
|
||||
|
||||
=item 14
|
||||
|
||||
Creates the system groups--the ones like C<editbugs>, C<admin>, and so on.
|
||||
|
@ -425,7 +437,7 @@ the C<--make-admin> switch.
|
|||
|
||||
=item 17
|
||||
|
||||
Creates the default Classification, Product, and Component, using
|
||||
Creates the default Product and Component, using
|
||||
L<Bugzilla::Install/create_default_product>.
|
||||
|
||||
=back
|
||||
|
|
|
@ -73,11 +73,13 @@ if (Bugzilla->params->{"useqacontact"}) {
|
|||
if (Bugzilla->params->{"usestatuswhiteboard"}) {
|
||||
push(@masterlist, "status_whiteboard");
|
||||
}
|
||||
if (Bugzilla::Keyword::keyword_count()) {
|
||||
if (Bugzilla::Keyword->any_exist) {
|
||||
push(@masterlist, "keywords");
|
||||
}
|
||||
|
||||
if (Bugzilla->user->in_group(Bugzilla->params->{"timetrackinggroup"})) {
|
||||
if (Bugzilla->has_flags) {
|
||||
push(@masterlist, "flagtypes.name");
|
||||
}
|
||||
if (Bugzilla->user->is_timetracker) {
|
||||
push(@masterlist, ("estimated_time", "remaining_time", "actual_time",
|
||||
"percentage_complete", "deadline"));
|
||||
}
|
||||
|
@ -88,7 +90,7 @@ my @custom_fields = grep { $_->type != FIELD_TYPE_MULTI_SELECT }
|
|||
Bugzilla->active_custom_fields;
|
||||
push(@masterlist, map { $_->name } @custom_fields);
|
||||
|
||||
Bugzilla::Hook::process("colchange-columns", {'columns' => \@masterlist} );
|
||||
Bugzilla::Hook::process('colchange_columns', {'columns' => \@masterlist} );
|
||||
|
||||
$vars->{'masterlist'} = \@masterlist;
|
||||
|
||||
|
@ -145,9 +147,9 @@ if (defined $cgi->param('rememberedquery')) {
|
|||
$search->update();
|
||||
}
|
||||
|
||||
my $params = new Bugzilla::CGI($cgi->param('rememberedquery'));
|
||||
$params->param('columnlist', join(",", @collist));
|
||||
$vars->{'redirect_url'} = "buglist.cgi?".$params->query_string();
|
||||
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
|
||||
|
|
|
@ -33,12 +33,9 @@
|
|||
use strict;
|
||||
use lib qw(. lib);
|
||||
|
||||
use AnyDBM_File;
|
||||
use IO::Handle;
|
||||
use List::Util qw(first);
|
||||
use Cwd;
|
||||
|
||||
|
||||
use Bugzilla;
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::Error;
|
||||
|
@ -160,8 +157,6 @@ my $tend = time;
|
|||
# Uncomment the following line for performance testing.
|
||||
#print "Total time taken " . delta_time($tstart, $tend) . "\n";
|
||||
|
||||
&calculate_dupes();
|
||||
|
||||
CollectSeriesData();
|
||||
|
||||
sub check_data_dir {
|
||||
|
@ -299,81 +294,6 @@ sub get_old_data {
|
|||
return @data;
|
||||
}
|
||||
|
||||
sub calculate_dupes {
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $rows = $dbh->selectall_arrayref("SELECT dupe_of, dupe FROM duplicates");
|
||||
|
||||
my %dupes;
|
||||
my %count;
|
||||
my $key;
|
||||
my $changed = 1;
|
||||
|
||||
my $today = &today_dash;
|
||||
|
||||
# Save % count here in a date-named file
|
||||
# so we can read it back in to do changed counters
|
||||
# First, delete it if it exists, so we don't add to the contents of an old file
|
||||
my $datadir = bz_locations()->{'datadir'};
|
||||
|
||||
if (my @files = <$datadir/duplicates/dupes$today*>) {
|
||||
map { trick_taint($_) } @files;
|
||||
unlink @files;
|
||||
}
|
||||
|
||||
dbmopen(%count, "$datadir/duplicates/dupes$today", 0644) || die "Can't open DBM dupes file: $!";
|
||||
|
||||
# Create a hash with key "a bug number", value "bug which that bug is a
|
||||
# direct dupe of" - straight from the duplicates table.
|
||||
foreach my $row (@$rows) {
|
||||
my ($dupe_of, $dupe) = @$row;
|
||||
$dupes{$dupe} = $dupe_of;
|
||||
}
|
||||
|
||||
# Total up the number of bugs which are dupes of a given bug
|
||||
# count will then have key = "bug number",
|
||||
# value = "number of immediate dupes of that bug".
|
||||
foreach $key (keys(%dupes))
|
||||
{
|
||||
my $dupe_of = $dupes{$key};
|
||||
|
||||
if (!defined($count{$dupe_of})) {
|
||||
$count{$dupe_of} = 0;
|
||||
}
|
||||
|
||||
$count{$dupe_of}++;
|
||||
}
|
||||
|
||||
# Now we collapse the dupe tree by iterating over %count until
|
||||
# there is no further change.
|
||||
while ($changed == 1)
|
||||
{
|
||||
$changed = 0;
|
||||
foreach $key (keys(%count)) {
|
||||
# if this bug is actually itself a dupe, and has a count...
|
||||
if (defined($dupes{$key}) && $count{$key} > 0) {
|
||||
# add that count onto the bug it is a dupe of,
|
||||
# and zero the count; the check is to avoid
|
||||
# loops
|
||||
if ($count{$dupes{$key}} != 0) {
|
||||
$count{$dupes{$key}} += $count{$key};
|
||||
$count{$key} = 0;
|
||||
$changed = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Remove the values for which the count is zero
|
||||
foreach $key (keys(%count))
|
||||
{
|
||||
if ($count{$key} == 0) {
|
||||
delete $count{$key};
|
||||
}
|
||||
}
|
||||
|
||||
dbmclose(%count);
|
||||
}
|
||||
|
||||
# This regenerates all statistics from the database.
|
||||
sub regenerate_stats {
|
||||
my ($dir, $product, $bug_resolution, $bug_status, $removed) = @_;
|
||||
|
|
57
config.cgi
57
config.cgi
|
@ -20,6 +20,7 @@
|
|||
#
|
||||
# Contributor(s): Terry Weissman <terry@mozilla.org>
|
||||
# Myk Melez <myk@mozilla.org>
|
||||
# Frank Becker <Frank@Frank-Becker.de>
|
||||
|
||||
################################################################################
|
||||
# Script Initialization
|
||||
|
@ -34,9 +35,12 @@ use Bugzilla;
|
|||
use Bugzilla::Constants;
|
||||
use Bugzilla::Error;
|
||||
use Bugzilla::Keyword;
|
||||
use Bugzilla::Product;
|
||||
use Bugzilla::Status;
|
||||
use Bugzilla::Field;
|
||||
|
||||
use Digest::MD5 qw(md5_base64);
|
||||
|
||||
my $user = Bugzilla->login(LOGIN_OPTIONAL);
|
||||
my $cgi = Bugzilla->cgi;
|
||||
|
||||
|
@ -77,6 +81,17 @@ if ($cgi->param('product')) {
|
|||
$vars->{'products'} = $user->get_selectable_products;
|
||||
}
|
||||
|
||||
Bugzilla::Product::preload($vars->{'products'});
|
||||
|
||||
# Allow consumers to specify whether or not they want flag data.
|
||||
if (defined $cgi->param('flags')) {
|
||||
$vars->{'show_flags'} = $cgi->param('flags');
|
||||
}
|
||||
else {
|
||||
# We default to sending flag data.
|
||||
$vars->{'show_flags'} = 1;
|
||||
}
|
||||
|
||||
# Create separate lists of open versus resolved statuses. This should really
|
||||
# be made part of the configuration.
|
||||
my @open_status;
|
||||
|
@ -91,7 +106,7 @@ $vars->{'closed_status'} = \@closed_status;
|
|||
# Generate a list of fields that can be queried.
|
||||
my @fields = @{Bugzilla::Field->match({obsolete => 0})};
|
||||
# Exclude fields the user cannot query.
|
||||
if (!Bugzilla->user->in_group(Bugzilla->params->{'timetrackinggroup'})) {
|
||||
if (!Bugzilla->user->is_timetracker) {
|
||||
@fields = grep { $_->name !~ /^(estimated_time|remaining_time|work_time|percentage_complete|deadline)$/ } @fields;
|
||||
}
|
||||
$vars->{'field'} = \@fields;
|
||||
|
@ -110,11 +125,41 @@ sub display_data {
|
|||
my $format = $template->get_format("config", scalar($cgi->param('format')),
|
||||
scalar($cgi->param('ctype')) || "js");
|
||||
|
||||
# Return HTTP headers.
|
||||
print "Content-Type: $format->{'ctype'}\n\n";
|
||||
|
||||
# Generate the configuration file and return it to the user.
|
||||
$template->process($format->{'template'}, $vars)
|
||||
# Generate the configuration data.
|
||||
my $output;
|
||||
$template->process($format->{'template'}, $vars, \$output)
|
||||
|| ThrowTemplateError($template->error());
|
||||
|
||||
# Wide characters cause md5_base64() to die.
|
||||
my $digest_data = $output;
|
||||
utf8::encode($digest_data) if utf8::is_utf8($digest_data);
|
||||
my $digest = md5_base64($digest_data);
|
||||
|
||||
# ETag support.
|
||||
my $if_none_match = $cgi->http('If-None-Match') || "";
|
||||
my $found304;
|
||||
my @if_none = split(/[\s,]+/, $if_none_match);
|
||||
foreach my $if_none (@if_none) {
|
||||
# remove quotes from begin and end of the string
|
||||
$if_none =~ s/^\"//g;
|
||||
$if_none =~ s/\"$//g;
|
||||
if ($if_none eq $digest or $if_none eq '*') {
|
||||
# leave the loop after the first match
|
||||
$found304 = $if_none;
|
||||
last;
|
||||
}
|
||||
}
|
||||
|
||||
if ($found304) {
|
||||
print $cgi->header(-type => 'text/html',
|
||||
-ETag => $found304,
|
||||
-status => '304 Not Modified');
|
||||
}
|
||||
else {
|
||||
# Return HTTP headers.
|
||||
print $cgi->header (-ETag => $digest,
|
||||
-type => $format->{'ctype'});
|
||||
print $output;
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
# nothing in this directory is retrievable unless overridden by an .htaccess
|
||||
# in a subdirectory
|
||||
deny from all
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue