508 lines
16 KiB
Perl
508 lines
16 KiB
Perl
# -*- Mode: perl; indent-tabs-mode: nil -*-
|
||
#
|
||
# The contents of this file are subject to the Mozilla Public
|
||
# License Version 1.1 (the "License"); you may not use this file
|
||
# except in compliance with the License. You may obtain a copy of
|
||
# the License at http://www.mozilla.org/MPL/
|
||
#
|
||
# Software distributed under the License is distributed on an "AS
|
||
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
|
||
# implied. See the License for the specific language governing
|
||
# rights and limitations under the License.
|
||
#
|
||
# The Initial Developer of the Original Code is NASA.
|
||
# Portions created by NASA are Copyright (C) 2006 San Jose State
|
||
# University Foundation. All Rights Reserved.
|
||
#
|
||
# The Original Code is the Bugzilla Bug Tracking System.
|
||
#
|
||
# Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org>
|
||
# Vitaliy Filippov <vitalif@mail.ru>
|
||
|
||
use strict;
|
||
|
||
##############################################
|
||
# Class representing single value of a field #
|
||
##############################################
|
||
|
||
package Bugzilla::Field::Choice;
|
||
|
||
use base qw(Bugzilla::Object);
|
||
|
||
use Bugzilla::Config qw(SetParam write_params);
|
||
use Bugzilla::Constants;
|
||
use Bugzilla::Error;
|
||
use Bugzilla::Field;
|
||
use Bugzilla::Util qw(trim detaint_natural trick_taint diff_arrays);
|
||
|
||
use Scalar::Util qw(blessed);
|
||
|
||
##################
|
||
# Initialization #
|
||
##################
|
||
|
||
use constant DB_COLUMNS => qw(
|
||
id
|
||
value
|
||
sortkey
|
||
);
|
||
|
||
use constant UPDATE_COLUMNS => qw(
|
||
value
|
||
sortkey
|
||
);
|
||
|
||
use constant NAME_FIELD => 'value';
|
||
use constant LIST_ORDER => 'sortkey, value';
|
||
|
||
use constant REQUIRED_CREATE_FIELDS => qw(value);
|
||
|
||
use constant VALIDATORS => {
|
||
value => \&_check_value,
|
||
sortkey => \&_check_sortkey,
|
||
};
|
||
|
||
use constant CLASS_MAP => {
|
||
bug_status => 'Bugzilla::Status',
|
||
product => 'Bugzilla::Product',
|
||
};
|
||
|
||
use constant DEFAULT_MAP => {
|
||
op_sys => 'defaultopsys',
|
||
rep_platform => 'defaultplatform',
|
||
priority => 'defaultpriority',
|
||
bug_severity => 'defaultseverity',
|
||
};
|
||
|
||
#################
|
||
# Class Factory #
|
||
#################
|
||
|
||
# Bugzilla::Field::Choice is actually an abstract base class. Every field
|
||
# type has its own dynamically-generated class for its values. This allows
|
||
# certain fields to have special types, like how bug_status's values
|
||
# are Bugzilla::Status objects.
|
||
|
||
sub type {
|
||
my ($class, $field) = @_;
|
||
my $field_obj = blessed $field ? $field : Bugzilla::Field->check($field);
|
||
my $field_name = $field_obj->name;
|
||
|
||
if ($class->CLASS_MAP->{$field_name}) {
|
||
return $class->CLASS_MAP->{$field_name};
|
||
}
|
||
|
||
# For generic classes, we use a lowercase class name, so as
|
||
# not to interfere with any real subclasses we might make some day.
|
||
my $package = "Bugzilla::Field::Choice::$field_name";
|
||
Bugzilla->request_cache->{"field_$package"} = $field_obj;
|
||
|
||
# This package only needs to be created once. We check if the DB_TABLE
|
||
# glob for this package already exists, which tells us whether or not
|
||
# we need to create the package (this works even under mod_perl, where
|
||
# this package definition will persist across requests)).
|
||
if (!defined *{"${package}::DB_TABLE"}) {
|
||
eval <<EOC;
|
||
package $package;
|
||
use base qw(Bugzilla::Field::Choice);
|
||
use constant DB_TABLE => '$field_name';
|
||
EOC
|
||
}
|
||
|
||
return $package;
|
||
}
|
||
|
||
################
|
||
# Constructors #
|
||
################
|
||
|
||
# We just make new() enforce this, which should give developers
|
||
# the understanding that you can't use Bugzilla::Field::Choice
|
||
# without calling type().
|
||
sub new {
|
||
my $class = shift;
|
||
if ($class eq 'Bugzilla::Field::Choice') {
|
||
ThrowCodeError('field_choice_must_use_type');
|
||
}
|
||
$class->SUPER::new(@_);
|
||
}
|
||
|
||
#########################
|
||
# Database Manipulation #
|
||
#########################
|
||
|
||
# Our subclasses can take more arguments than we normally accept.
|
||
# So, we override create() to remove arguments that aren't valid
|
||
# columns. (Normally Bugzilla::Object dies if you pass arguments
|
||
# that aren't valid columns.)
|
||
sub create {
|
||
my $class = shift;
|
||
my ($params) = @_;
|
||
foreach my $key (keys %$params) {
|
||
if (!grep {$_ eq $key} $class->DB_COLUMNS) {
|
||
delete $params->{$key};
|
||
}
|
||
}
|
||
return $class->SUPER::create(@_);
|
||
}
|
||
|
||
sub update {
|
||
my $self = shift;
|
||
my $dbh = Bugzilla->dbh;
|
||
my $fname = $self->field->name;
|
||
|
||
$dbh->bz_start_transaction();
|
||
|
||
my ($changes, $old_self) = $self->SUPER::update(@_);
|
||
if (exists $changes->{value}) {
|
||
my ($old, $new) = @{ $changes->{value} };
|
||
if ($self->field->type == FIELD_TYPE_MULTI_SELECT) {
|
||
$dbh->do("UPDATE bug_$fname SET value = ? WHERE value = ?",
|
||
undef, $new, $old);
|
||
}
|
||
else {
|
||
$dbh->do("UPDATE bugs SET $fname = ? WHERE $fname = ?",
|
||
undef, $new, $old);
|
||
}
|
||
|
||
if ($old_self->is_default) {
|
||
my $param = $self->DEFAULT_MAP->{$self->field->name};
|
||
SetParam($param, $self->name);
|
||
write_params();
|
||
}
|
||
}
|
||
|
||
$dbh->bz_commit_transaction();
|
||
return wantarray ? ($changes, $old_self) : $changes;
|
||
}
|
||
|
||
sub remove_from_db {
|
||
my $self = shift;
|
||
if ($self->is_default) {
|
||
ThrowUserError('fieldvalue_is_default',
|
||
{ field => $self->field, value => $self,
|
||
param_name => $self->DEFAULT_MAP->{$self->field->name},
|
||
});
|
||
}
|
||
if ($self->is_static) {
|
||
ThrowUserError('fieldvalue_not_deletable',
|
||
{ field => $self->field, value => $self });
|
||
}
|
||
if ($self->bug_count) {
|
||
ThrowUserError("fieldvalue_still_has_bugs",
|
||
{ field => $self->field, value => $self });
|
||
}
|
||
$self->_check_if_controller();
|
||
$self->set_visibility_values([]);
|
||
$self->SUPER::remove_from_db();
|
||
}
|
||
|
||
# Factored out to make life easier for subclasses.
|
||
sub _check_if_controller {
|
||
my $self = shift;
|
||
my $vis_fields = $self->controls_visibility_of_fields;
|
||
my $values = $self->controlled_values;
|
||
if (@$vis_fields || scalar(keys %$values)) {
|
||
ThrowUserError('fieldvalue_is_controller',
|
||
{ value => $self, fields => [map($_->name, @$vis_fields)],
|
||
vals => $values });
|
||
}
|
||
}
|
||
|
||
|
||
#############
|
||
# Accessors #
|
||
#############
|
||
|
||
sub sortkey { return $_[0]->{'sortkey'}; }
|
||
|
||
sub bug_count {
|
||
my $self = shift;
|
||
return $self->{bug_count} if defined $self->{bug_count};
|
||
my $dbh = Bugzilla->dbh;
|
||
my $fname = $self->field->name;
|
||
my $count;
|
||
if ($self->field->type == FIELD_TYPE_MULTI_SELECT) {
|
||
$count = $dbh->selectrow_array("SELECT COUNT(*) FROM bug_$fname
|
||
WHERE value = ?", undef, $self->name);
|
||
}
|
||
else {
|
||
$count = $dbh->selectrow_array("SELECT COUNT(*) FROM bugs
|
||
WHERE $fname = ?",
|
||
undef, $self->name);
|
||
}
|
||
$self->{bug_count} = $count;
|
||
return $count;
|
||
}
|
||
|
||
sub field {
|
||
my $invocant = shift;
|
||
my $class = ref $invocant || $invocant;
|
||
my $cache = Bugzilla->request_cache;
|
||
# This is just to make life easier for subclasses. Our auto-generated
|
||
# subclasses from type() already have this set.
|
||
$cache->{"field_$class"} ||=
|
||
new Bugzilla::Field({ name => $class->DB_TABLE });
|
||
return $cache->{"field_$class"};
|
||
}
|
||
|
||
sub is_default {
|
||
my $self = shift;
|
||
my $name = $self->DEFAULT_MAP->{$self->field->name};
|
||
# If it doesn't exist in DEFAULT_MAP, then there is no parameter
|
||
# related to this field.
|
||
return 0 unless $name;
|
||
return ($self->name eq Bugzilla->params->{$name}) ? 1 : 0;
|
||
}
|
||
|
||
sub is_static {
|
||
my $self = shift;
|
||
# If we need to special-case Resolution for *anything* else, it should
|
||
# get its own subclass.
|
||
if ($self->field->name eq 'resolution') {
|
||
return grep($_ eq $self->name, ('', 'FIXED', 'MOVED', 'DUPLICATE'))
|
||
? 1 : 0;
|
||
}
|
||
elsif ($self->field->custom) {
|
||
return $self->name eq '---' ? 1 : 0;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
sub controls_visibility_of_fields {
|
||
my $self = shift;
|
||
$self->{controls_visibility_of_fields} ||= Bugzilla::Field->match(
|
||
{ visibility_field_id => $self->field->id,
|
||
visibility_value_id => $self->id });
|
||
return $self->{controls_visibility_of_fields};
|
||
}
|
||
|
||
sub controlled_values
|
||
{
|
||
my $self = shift;
|
||
return $self->{controlled_values} if defined $self->{controlled_values};
|
||
my $fields = $self->field->controls_values_of;
|
||
my %controlled_values;
|
||
# TODO move this into Bugzilla::Field::Choice::match() with MATCH_JOIN
|
||
# but no MATCH_JOIN by now is available
|
||
foreach my $field (@$fields) {
|
||
my $type = Bugzilla::Field::Choice->type($field);
|
||
my $sql =
|
||
"SELECT f.".join(", f.", $type->DB_COLUMNS).
|
||
" FROM fieldvaluecontrol c, ".$type->DB_TABLE." f".
|
||
" WHERE c.field_id=? AND c.visibility_value_id=? AND c.value_id=f.id".
|
||
" ORDER BY f.sortkey";
|
||
# Обходим самозарождение греха при конкатенации DB_COLUMNS и DB_TABLE
|
||
trick_taint($sql);
|
||
my $f = Bugzilla->dbh->selectall_arrayref($sql, {Slice=>{}}, $field->id, $self->id) || [];
|
||
$f = [ map { bless $_, $type } @$f ];
|
||
$controlled_values{$field->name} = $f;
|
||
}
|
||
$self->{controlled_values} = \%controlled_values;
|
||
return $self->{controlled_values};
|
||
}
|
||
|
||
sub controlled_plus_generic
|
||
{
|
||
my $self = shift;
|
||
return $self->{controlled_plus_generic} if defined $self->{controlled_plus_generic};
|
||
my $fields = $self->field->controls_values_of;
|
||
my %controlled_values;
|
||
# TODO move this into Bugzilla::Field::Choice::match() with MATCH_JOIN
|
||
# but no MATCH_JOIN by now is available
|
||
foreach my $field (@$fields) {
|
||
my $type = Bugzilla::Field::Choice->type($field);
|
||
my $sql =
|
||
"(SELECT f.".join(", f.", $type->DB_COLUMNS).
|
||
" FROM fieldvaluecontrol c, ".$type->DB_TABLE." f".
|
||
" WHERE c.field_id=? AND c.visibility_value_id=? AND c.value_id=f.id)".
|
||
" UNION ALL (SELECT f.".join(", f.", $type->DB_COLUMNS).
|
||
" FROM ".$type->DB_TABLE." f LEFT JOIN fieldvaluecontrol c ON c.value_id=f.id AND c.field_id=?".
|
||
" WHERE c.field_id IS NULL)".
|
||
" ORDER BY sortkey";
|
||
# Обходим самозарождение греха при конкатенации DB_COLUMNS и DB_TABLE
|
||
trick_taint($sql);
|
||
my $f = Bugzilla->dbh->selectall_arrayref($sql, {Slice=>{}}, $field->id, $self->id, $field->id) || [];
|
||
$f = [ map { bless $_, $type } @$f ];
|
||
$controlled_values{$field->name} = $f;
|
||
}
|
||
$self->{controlled_plus_generic} = \%controlled_values;
|
||
return $self->{controlled_plus_generic};
|
||
}
|
||
|
||
sub visibility_values
|
||
{
|
||
my $self = shift;
|
||
my $f;
|
||
unless ($f = $self->{visibility_values})
|
||
{
|
||
# TODO move this into Bugzilla::Field::Choice::match() with MATCH_JOIN
|
||
# but no MATCH_JOIN by now is available
|
||
my $type = Bugzilla::Field::Choice->type($self->field->value_field);
|
||
my $sql =
|
||
"SELECT f.".join(", f.", $type->DB_COLUMNS).
|
||
" FROM fieldvaluecontrol c, ".$type->DB_TABLE." f".
|
||
" WHERE c.field_id=? AND c.visibility_value_id=f.id AND c.value_id=?";
|
||
# Обходим самозарождение греха при конкатенации DB_COLUMNS и DB_TABLE
|
||
trick_taint($sql);
|
||
$f = Bugzilla->dbh->selectall_arrayref($sql, {Slice=>{}}, $self->field->id, $self->id) || [];
|
||
$f = [ map { bless $_, $type } @$f ];
|
||
$self->{visibility_values} = $f;
|
||
}
|
||
return $f;
|
||
}
|
||
|
||
############
|
||
# Mutators #
|
||
############
|
||
|
||
sub set_name { $_[0]->set('value', $_[1]); }
|
||
sub set_sortkey { $_[0]->set('sortkey', $_[1]); }
|
||
|
||
sub set_visibility_values
|
||
{
|
||
my $self = shift;
|
||
my ($value_ids) = @_;
|
||
return undef if !ref $value_ids || $value_ids !~ 'ARRAY';
|
||
my $type = Bugzilla::Field::Choice->type($self->field->value_field);
|
||
@$value_ids = map { $_ ? $type->check({ id => $_ }) : () } @$value_ids;
|
||
my ($a, $r) = diff_arrays([map { $_->id } @$value_ids], [map { $_->id } @{$self->visibility_values}]);
|
||
return undef unless @$a || @$r;
|
||
Bugzilla->dbh->do(
|
||
"DELETE FROM fieldvaluecontrol WHERE field_id=? AND value_id=?",
|
||
undef, $self->field->id, $self->id);
|
||
if (@$value_ids)
|
||
{
|
||
Bugzilla->dbh->do(
|
||
"INSERT INTO fieldvaluecontrol (field_id, value_id, visibility_value_id) VALUES ".
|
||
join(",", ("(?,?,?)") x @$value_ids),
|
||
undef, map { ($self->field->id, $self->id, $_->id) } @$value_ids);
|
||
}
|
||
delete $self->{visibility_values};
|
||
return @$value_ids;
|
||
}
|
||
|
||
##############
|
||
# Validators #
|
||
##############
|
||
|
||
sub _check_value {
|
||
my ($invocant, $value) = @_;
|
||
|
||
my $field = $invocant->field;
|
||
|
||
$value = trim($value);
|
||
|
||
# Make sure people don't rename static values
|
||
if (blessed($invocant) && $value ne $invocant->name
|
||
&& $invocant->is_static)
|
||
{
|
||
ThrowUserError('fieldvalue_not_editable',
|
||
{ field => $field, old_value => $invocant });
|
||
}
|
||
|
||
ThrowUserError('fieldvalue_undefined') if !defined $value || $value eq "";
|
||
ThrowUserError('fieldvalue_name_too_long', { value => $value })
|
||
if length($value) > MAX_FIELD_VALUE_SIZE;
|
||
|
||
my $exists = $invocant->type($field)->new({ name => $value });
|
||
if ($exists && (!blessed($invocant) || $invocant->id != $exists->id)) {
|
||
ThrowUserError('fieldvalue_already_exists',
|
||
{ field => $field, value => $exists });
|
||
}
|
||
|
||
return $value;
|
||
}
|
||
|
||
sub _check_sortkey {
|
||
my ($invocant, $value) = @_;
|
||
$value = trim($value);
|
||
return 0 if !$value;
|
||
# Store for the error message in case detaint_natural clears it.
|
||
my $orig_value = $value;
|
||
detaint_natural($value)
|
||
|| ThrowUserError('fieldvalue_sortkey_invalid',
|
||
{ sortkey => $orig_value,
|
||
field => $invocant->field });
|
||
return $value;
|
||
}
|
||
|
||
1;
|
||
|
||
__END__
|
||
|
||
=head1 NAME
|
||
|
||
Bugzilla::Field::Choice - A legal value for a <select>-type field.
|
||
|
||
=head1 SYNOPSIS
|
||
|
||
my $field = new Bugzilla::Field({name => 'bug_status'});
|
||
|
||
my $choice = new Bugzilla::Field::Choice->type($field)->new(1);
|
||
|
||
my $choices = Bugzilla::Field::Choice->type($field)->new_from_list([1,2,3]);
|
||
my $choices = Bugzilla::Field::Choice->type($field)->get_all();
|
||
my $choices = Bugzilla::Field::Choice->type($field->match({ sortkey => 10 });
|
||
|
||
=head1 DESCRIPTION
|
||
|
||
This is an implementation of L<Bugzilla::Object>, but with a twist.
|
||
You can't call any class methods (such as C<new>, C<create>, etc.)
|
||
directly on C<Bugzilla::Field::Choice> itself. Instead, you have to
|
||
call C<Bugzilla::Field::Choice-E<gt>type($field)> to get the class
|
||
you're going to instantiate, and then you call the methods on that.
|
||
|
||
We do that because each field has its own database table for its values, so
|
||
each value type needs its own class.
|
||
|
||
See the L</SYNOPSIS> for examples of how this works.
|
||
|
||
=head1 METHODS
|
||
|
||
=head2 Class Factory
|
||
|
||
In object-oriented design, a "class factory" is a method that picks
|
||
and returns the right class for you, based on an argument that you pass.
|
||
|
||
=over
|
||
|
||
=item C<type>
|
||
|
||
Takes a single argument, which is either the name of a field from the
|
||
C<fielddefs> table, or a L<Bugzilla::Field> object representing a field.
|
||
|
||
Returns an appropriate subclass of C<Bugzilla::Field::Choice> that you
|
||
can now call class methods on (like C<new>, C<create>, C<match>, etc.)
|
||
|
||
B<NOTE>: YOU CANNOT CALL CLASS METHODS ON C<Bugzilla::Field::Choice>. You
|
||
must call C<type> to get a class you can call methods on.
|
||
|
||
=back
|
||
|
||
=head2 Accessors
|
||
|
||
These are in addition to the standard L<Bugzilla::Object> accessors.
|
||
|
||
=over
|
||
|
||
=item C<sortkey>
|
||
|
||
The key that determines the sort order of this item.
|
||
|
||
=item C<field>
|
||
|
||
The L<Bugzilla::Field> object that this field value belongs to.
|
||
|
||
=item C<controlled_values>
|
||
|
||
Tells you which values in B<other> fields appear (become visible) when this
|
||
value is set in its field.
|
||
|
||
Returns a hashref of arrayrefs. The hash keys are the names of fields,
|
||
and the values are arrays of C<Bugzilla::Field::Choice> objects,
|
||
representing values that this value controls the visibility of, for
|
||
that field.
|
||
|
||
=back
|