Bug 40933 - НЕ ДО КОНЦА оттестированная объединённая версия Bugzilla 3.6 - НИКУДА НЕ РАЗВОРАЧИВАТЬ! :)

git-svn-id: svn://svn.office.custis.ru/3rdparty/bugzilla.org/trunk@738 6955db30-a419-402b-8a0d-67ecbb4d7f56
master
vfilippov 2010-05-14 20:02:34 +00:00
parent 0804786698
commit d2a48e6dc8
468 changed files with 43862 additions and 34068 deletions

View File

@ -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

View File

@ -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;

View File

@ -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;
@ -120,7 +123,7 @@ sub can_change_password {
my $verifier = $self->{_verifier}->{successful};
$verifier ||= $self->{_verifier};
my $getter = $self->{_info_getter}->{successful};
$getter = $self->{_info_getter}
$getter = $self->{_info_getter}
if (!$getter || $getter->isa('Bugzilla::Auth::Login::Cookie'));
return $verifier->can_change_password &&
$getter->user_can_create_account;
@ -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

View File

@ -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();

View File

@ -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()

View File

@ -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)) {

View File

@ -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;
}

View File

@ -41,37 +41,51 @@ sub check_credentials {
my $dbh = Bugzilla->dbh;
my $username = $login_data->{username};
my $user_id = login_to_id($username);
return { failure => AUTH_NO_SUCH_USER } unless $user_id;
my $user = new Bugzilla::User({ name => $username });
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;

View File

@ -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"}) {

View File

@ -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)) {

File diff suppressed because it is too large Load Diff

View File

@ -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;

View File

@ -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>

View File

@ -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();

298
Bugzilla/Comment.pm Normal file
View File

@ -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

View File

@ -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;

View File

@ -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'};

View File

@ -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;

View File

@ -0,0 +1,57 @@
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is 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;

View File

@ -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;

View File

@ -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',

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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";

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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',

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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',

View File

@ -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;

View File

@ -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;

View File

@ -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',

View File

@ -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
@ -103,7 +106,7 @@ use File::Basename;
EVT_OTHER EVT_ADDED_REMOVED EVT_COMMENT EVT_ATTACHMENT EVT_ATTACHMENT_DATA
EVT_PROJ_MANAGEMENT EVT_OPENED_CLOSED EVT_KEYWORD EVT_CC EVT_DEPEND_BLOCK
EVT_BUG_CREATED
NEG_EVENTS
EVT_UNCONFIRMED EVT_CHANGED_BY_ME
@ -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;

View File

@ -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
#####################################################################
@ -1001,7 +1026,7 @@ sub bz_start_transaction {
sub bz_commit_transaction {
my ($self) = @_;
if ($self->{private_bz_transaction_count} > 1) {
$self->{private_bz_transaction_count}--;
} elsif ($self->bz_in_transaction) {

View File

@ -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.
@ -158,15 +162,15 @@ sub sql_limit {
sub sql_string_concat {
my ($self, @params) = @_;
return 'CONCAT(' . join(', ', @params) . ')';
}
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
@ -200,7 +204,7 @@ sub sql_fulltext_search {
sub sql_istring {
my ($self, $string) = @_;
return $string;
}
@ -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 =
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

View File

@ -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;
@ -271,6 +277,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
@ -337,6 +347,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;

View File

@ -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.

View File

@ -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'}},
],
},
@ -666,7 +685,7 @@ use constant ABSTRACT_SCHEMA => {
DEFAULT => 'FALSE'},
buglist => {TYPE => 'BOOLEAN', NOTNULL => 1,
DEFAULT => 'FALSE'},
visibility_field_id => {TYPE => 'INT3',
visibility_field_id => {TYPE => 'INT3',
REFERENCES => {TABLE => 'fielddefs',
COLUMN => 'id'}},
# CustIS Bug 53617 - visibility_value_id is removed from here
@ -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);
}

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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)
{

222
Bugzilla/Extension.pm Normal file
View File

@ -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.

View File

@ -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},
@ -469,9 +470,9 @@ objects.
=cut
sub is_select {
return ($_[0]->type == FIELD_TYPE_SINGLE_SELECT
|| $_[0]->type == FIELD_TYPE_MULTI_SELECT) ? 1 : 0
sub is_select {
return ($_[0]->type == FIELD_TYPE_SINGLE_SELECT
|| $_[0]->type == FIELD_TYPE_MULTI_SELECT) ? 1 : 0
}
sub legal_values {
@ -511,7 +512,7 @@ Returns undef if there is no field that controls this field's visibility.
sub visibility_field {
my $self = shift;
if ($self->{visibility_field_id}) {
$self->{visibility_field} ||=
$self->{visibility_field} ||=
$self->new($self->{visibility_field_id});
}
return $self->{visibility_field};
@ -572,7 +573,7 @@ field controls the visibility of.
sub controls_visibility_of {
my $self = shift;
$self->{controls_visibility_of} ||=
$self->{controls_visibility_of} ||=
Bugzilla::Field->match({ visibility_field_id => $self->id });
return $self->{controls_visibility_of};
}
@ -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)

View File

@ -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
{

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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) = @_;
# 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({});
}
}
my %hooks;
my @hook_stack;
my %hook_hash;
# 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
"&amp;".)
=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>

View File

@ -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;
my $default_comp = DEFAULT_COMPONENT;
undef, $admin_group->id);
my $admin = Bugzilla::User->new($admin_id);
$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

View File

@ -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',
@ -555,12 +561,53 @@ 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));
@ -3104,7 +3188,7 @@ sub _change_text_types {
$dbh->bz_alter_column('namedqueries', 'query',
{ TYPE => 'LONGTEXT', NOTNULL => 1 });
}
}
sub _check_content_length {
my ($table_name, $field_name, $max_length, $id_field) = @_;
@ -3136,41 +3220,48 @@ sub _add_foreign_keys_to_multiselects {
my $dbh = Bugzilla->dbh;
my $names = $dbh->selectcol_arrayref(
'SELECT name
FROM fielddefs
'SELECT name
FROM fielddefs
WHERE type = ' . FIELD_TYPE_MULTI_SELECT);
foreach my $name (@$names) {
$dbh->bz_add_fk("bug_$name", "bug_id", {TABLE => 'bugs',
COLUMN => 'bug_id',
DELETE => 'CASCADE',});
$dbh->bz_add_fk("bug_$name", "value", {TABLE => $name,
COLUMN => 'value',
DELETE => 'RESTRICT',});
}
}
# 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__

View File

@ -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

View File

@ -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);

View File

@ -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

View File

@ -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) {
@ -122,20 +125,33 @@ sub install_string {
}
$string_template =~ s/\Q##$key##\E/$replacement/g;
}
return $string_template;
}
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;
}
# Used by template_include_path
sub _template_lang_directories {
my ($languages, $templatedir) = @_;
sub template_include_path {
my @usedlanguages = include_languages(@_);
# Now, we add template directories in the order they will be searched:
# 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);
}
}
}
# Then, we add normal template directories, sorted by language.
foreach my $lang (@usedlanguages) {
_add_language_set(\@include_path, $lang);
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;
}
}
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

View File

@ -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;

View File

@ -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

View File

@ -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") {

1171
Bugzilla/Migrate.pm Normal file

File diff suppressed because it is too large Load Diff

709
Bugzilla/Migrate/Gnats.pm Normal file
View File

@ -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;

View File

@ -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;
}
@ -231,11 +254,16 @@ sub _do_list_select {
$sql .= " WHERE $where ";
}
$sql .= " ORDER BY $order";
$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.

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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&amp;"
. $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}++;
}
}

View File

@ -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>

View File

@ -68,7 +68,8 @@ sub new {
elsif ($arg_count >= 6 && $arg_count <= 8) {
# 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.
# 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;

View File

@ -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

View File

@ -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 = '&amp;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}&amp;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 "&lt;missing bug number&gt;";
if (!$bug) {
return html_quote('<missing bug number>');
}
my $quote_bug_num = html_quote($bug_num);
detaint_natural($bug_num) || return "&lt;invalid bug number: $quote_bug_num&gt;";
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);
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>};
$bug = blessed($bug) ? $bug : new Bugzilla::Bug($bug);
return $link_text if $bug->{error};
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,
@ -530,7 +552,7 @@ sub create {
$var =~ s/</\\x3c/g;
return $var;
},
json => sub {
my ($var) = @_;
return encode_json($var);
@ -541,7 +563,7 @@ sub create {
my ($data) = @_;
return encode_base64($data);
},
# Converts data to quoted-printable
quoted_printable => sub {
my ($data) = @_;
@ -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/\@/\&#64;/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/\&gt;/>/g;
$var =~ s/\&quot;/\"/g;
$var =~ s/\&amp;/\&/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;
},
@ -799,18 +797,19 @@ sub create {
# Currently logged in user, if any
# 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 $dir_reader = new IO::Dir($templatedir) || die "$templatedir: $!";
my @language_dirs = grep { /^[a-z-]+$/i } $dir_reader->read;
$dir_reader->close;
my $paths = template_include_path({ use_languages => Bugzilla->languages });
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);
foreach my $dir (@$paths) {
my $template = Bugzilla::Template->create(include_path => [$dir]);
# 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__

View File

@ -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;

View File

@ -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;
# 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");
return \@hooks;
}
# 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>

View File

@ -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;

View File

@ -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();

File diff suppressed because it is too large Load Diff

View File

@ -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/\&/\&amp;/g;
$var =~ s/</\&lt;/g;
$var =~ s/>/\&gt;/g;
$var =~ s/\"/\&quot;/g;
my ($var) = Template::Filters::html_filter(@_);
# Obscure '@'.
$var =~ s/\@/\&#64;/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';
# 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;
my $sslbase = Bugzilla->params->{'sslbase'};
# 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

View File

@ -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

View File

@ -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

View File

@ -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,12 +352,9 @@ 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);
}
push (@history, \%bug_history);
}
@ -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;
@ -353,7 +482,7 @@ sub add_comment {
# Check parameters
defined $params->{id}
|| ThrowCodeError('param_required', { param => 'id' });
|| ThrowCodeError('param_required', { param => 'id' });
my $comment = $params->{comment};
(defined $comment && trim($comment) ne '')
|| ThrowCodeError('param_required', { param => 'comment' });
@ -362,9 +491,13 @@ 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) = @_;
my %field_values;
foreach my $field (keys %$params) {
my $field_name = FIELD_MAP->{$field} || $field;
$field_values{$field_name} = $params->{$field};
}
sub _attachment_to_hash {
my ($self, $attach, $filters) = @_;
unless (Bugzilla->user->is_timetracker) {
delete @field_values{qw(estimated_time remaining_time deadline)};
}
return \%field_values;
# Skipping attachment flags for now.
delete $attach->{flags};
my $attacher = new Bugzilla::User($attach->attacher->id);
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

View File

@ -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

View File

@ -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;

View File

@ -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.

View File

@ -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;

View File

@ -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>

View File

@ -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)) {
@ -233,7 +254,7 @@ sub BEGIN {
return as_nil();
}
else {
my $super_method = "SUPER::$method";
my $super_method = "SUPER::$method";
return $self->$super_method($value);
}
}
@ -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>

View File

@ -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

View File

@ -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) = @_;

136
Bugzilla/Whine/Query.pm Normal file
View File

@ -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

172
Bugzilla/Whine/Schedule.pm Normal file
View File

@ -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

View File

@ -60,6 +60,7 @@ sub _column_length
return $len;
}
undef &Text::TabularDisplay::_column_length;
*Text::TabularDisplay::_column_length = \&_column_length;
# -------------------------------------------------------------------

View File

@ -39,10 +39,11 @@ use strict;
use lib qw(. lib);
use Bugzilla;
use Bugzilla::BugMail;
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Flag;
use Bugzilla::FlagType;
use Bugzilla::Flag;
use Bugzilla::FlagType;
use Bugzilla::User;
use Bugzilla::Util;
use Bugzilla::Bug;
@ -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")
@ -106,34 +99,34 @@ elsif ($action eq "diff")
{
diff();
}
elsif ($action eq "viewall")
{
viewall();
elsif ($action eq "viewall")
{
viewall();
}
elsif ($action eq "enter")
{
elsif ($action eq "enter")
{
Bugzilla->login(LOGIN_REQUIRED);
enter();
enter();
}
elsif ($action eq "insert")
{
Bugzilla->login(LOGIN_REQUIRED);
insert();
}
elsif ($action eq "edit")
{
edit();
elsif ($action eq "edit")
{
edit();
}
elsif ($action eq "update")
{
elsif ($action eq "update")
{
Bugzilla->login(LOGIN_REQUIRED);
update();
}
elsif ($action eq "delete") {
delete_attachment();
}
else
{
else
{
ThrowCodeError("unknown_action", { action => $action });
}
@ -167,7 +160,7 @@ sub validateID {
ThrowTemplateError($template->error());
exit;
}
my $attach_id = $cgi->param($param);
# Validate the specified attachment id. detaint kills $attach_id if
@ -175,7 +168,7 @@ sub validateID {
# message here.
detaint_natural($attach_id)
|| ThrowUserError("invalid_attach_id", { attach_id => $cgi->param($param) });
# Make sure the attachment exists in the database.
my $attachment = new Bugzilla::Attachment($attach_id)
|| ThrowUserError("invalid_attach_id", { attach_id => $attach_id });
@ -190,8 +183,8 @@ sub check_can_access {
# Make sure the user is authorized to access this attachment's bug.
Bugzilla::Bug->check($attachment->bug_id);
if ($attachment->isprivate && $user->id != $attachment->attacher->id
&& !$user->is_insider)
if ($attachment->isprivate && $user->id != $attachment->attacher->id
&& !$user->is_insider)
{
ThrowUserError('auth_failure', {action => 'access',
object => 'attachment'});
@ -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;

View File

@ -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;
@ -385,7 +384,7 @@ $filename =~ s/\\/\\\\/g; # escape backslashes
$filename =~ s/"/\\"/g; # escape quotes
# Take appropriate action based on user's request.
if ($cmdtype eq "dorem") {
if ($cmdtype eq "dorem") {
if ($remaction eq "run") {
my $query_id;
($buffer, $query_id) = Bugzilla::Search::LookupNamedQuery(
@ -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.
@ -961,7 +953,7 @@ while (my @row = $buglist_sth->fetchrow_array()) {
s/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/$1-$2-$3 $4:$5:$6/;
$bug->{'changedtime'} = $bug->{'changeddate'}; # for iCalendar and Atom
$bug->{'changeddate'} = DiffDate($bug->{'changeddate'});
$bug->{'changeddate'} = DiffDate($bug->{'changeddate'});
}
if ($bug->{'opendate'}) {
@ -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);
}
}

View File

@ -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

110
chart.cgi
View File

@ -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 = '&amp;' . html_quote($url);
}
print $cgi->redirect(-location => correct_urlbase() . "query.cgi?format=create-series$url");
}
else {
ThrowCodeError("unknown_action");
}
@ -208,30 +248,31 @@ 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;
Bugzilla->user->in_group("editbugs") || ThrowUserError("illegal_series_creation");
my $user = Bugzilla->user;
$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 });
}
}
}
sub validateWidthAndHeight {
@ -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();

View File

@ -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

View File

@ -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

View File

@ -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) = @_;
@ -403,7 +323,7 @@ sub regenerate_stats {
# database was created, and the end date from the current day.
# If there were no bugs in the search, return early.
my $query = q{SELECT } .
$dbh->sql_to_days('creation_ts') . q{ AS start_day, } .
$dbh->sql_to_days('creation_ts') . q{ AS start_day, } .
$dbh->sql_to_days('current_date') . q{ AS end_day, } .
$dbh->sql_to_days("'1970-01-01'") .
qq{ FROM bugs $from_product

View File

@ -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;
}

3
contrib/.htaccess Normal file
View File

@ -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