2008-12-15 15:53:33 +03:00
|
|
|
# -*- Mode: perl; indent-tabs-mode: nil -*-
|
|
|
|
#
|
|
|
|
# The contents of this file are subject to the Mozilla Public
|
|
|
|
# License Version 1.1 (the "License"); you may not use this file
|
|
|
|
# except in compliance with the License. You may obtain a copy of
|
|
|
|
# the License at http://www.mozilla.org/MPL/
|
|
|
|
#
|
|
|
|
# Software distributed under the License is distributed on an "AS
|
|
|
|
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
|
|
|
|
# implied. See the License for the specific language governing
|
|
|
|
# rights and limitations under the License.
|
|
|
|
#
|
|
|
|
# The Original Code is the Bugzilla Bug Tracking System.
|
|
|
|
#
|
|
|
|
# Contributor(s): Dan Mosedale <dmose@mozilla.org>
|
|
|
|
# Frédéric Buclin <LpSolit@gmail.com>
|
|
|
|
# Myk Melez <myk@mozilla.org>
|
2010-05-15 00:02:34 +04:00
|
|
|
# Greg Hendricks <ghendricks@novell.com>
|
2008-12-15 15:53:33 +03:00
|
|
|
|
|
|
|
=head1 NAME
|
|
|
|
|
|
|
|
Bugzilla::Field - a particular piece of information about bugs
|
|
|
|
and useful routines for form field manipulation
|
|
|
|
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
|
|
|
|
use Bugzilla;
|
|
|
|
use Data::Dumper;
|
|
|
|
|
|
|
|
# Display information about all fields.
|
|
|
|
print Dumper(Bugzilla->get_fields());
|
|
|
|
|
|
|
|
# Display information about non-obsolete custom fields.
|
|
|
|
print Dumper(Bugzilla->active_custom_fields);
|
|
|
|
|
|
|
|
use Bugzilla::Field;
|
|
|
|
|
|
|
|
# Display information about non-obsolete custom fields.
|
|
|
|
# Bugzilla->get_fields() is a wrapper around Bugzilla::Field->match(),
|
|
|
|
# so both methods take the same arguments.
|
|
|
|
print Dumper(Bugzilla::Field->match({ obsolete => 0, custom => 1 }));
|
|
|
|
|
|
|
|
# Create or update a custom field or field definition.
|
|
|
|
my $field = Bugzilla::Field->create(
|
|
|
|
{name => 'cf_silly', description => 'Silly', custom => 1});
|
|
|
|
|
|
|
|
# Instantiate a Field object for an existing field.
|
|
|
|
my $field = new Bugzilla::Field({name => 'qacontact_accessible'});
|
|
|
|
if ($field->obsolete) {
|
|
|
|
print $field->description . " is obsolete\n";
|
|
|
|
}
|
|
|
|
|
|
|
|
# Validation Routines
|
|
|
|
check_field($name, $value, \@legal_values, $no_warn);
|
|
|
|
$fieldid = get_field_id($fieldname);
|
|
|
|
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
|
|
|
|
Field.pm defines field objects, which represent the particular pieces
|
|
|
|
of information that Bugzilla stores about bugs.
|
|
|
|
|
|
|
|
This package also provides functions for dealing with CGI form fields.
|
|
|
|
|
|
|
|
C<Bugzilla::Field> is an implementation of L<Bugzilla::Object>, and
|
|
|
|
so provides all of the methods available in L<Bugzilla::Object>,
|
|
|
|
in addition to what is documented here.
|
|
|
|
|
|
|
|
=cut
|
|
|
|
|
|
|
|
package Bugzilla::Field;
|
|
|
|
|
|
|
|
use strict;
|
|
|
|
|
|
|
|
use base qw(Exporter Bugzilla::Object);
|
2010-03-12 22:18:39 +03:00
|
|
|
@Bugzilla::Field::EXPORT = qw(check_field get_field_id get_legal_field_values update_visibility_values);
|
2008-12-15 15:53:33 +03:00
|
|
|
|
|
|
|
use Bugzilla::Constants;
|
|
|
|
use Bugzilla::Error;
|
2009-07-29 15:21:49 +04:00
|
|
|
use Bugzilla::Util;
|
|
|
|
|
|
|
|
use Scalar::Util qw(blessed);
|
2010-03-12 22:18:39 +03:00
|
|
|
use Encode;
|
|
|
|
use JSON;
|
2010-12-09 20:16:43 +03:00
|
|
|
use POSIX;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
|
|
|
###############################
|
|
|
|
#### Initialization ####
|
|
|
|
###############################
|
|
|
|
|
|
|
|
use constant DB_TABLE => 'fielddefs';
|
|
|
|
use constant LIST_ORDER => 'sortkey, name';
|
|
|
|
|
2009-07-29 15:21:49 +04:00
|
|
|
use constant DB_COLUMNS => qw(
|
|
|
|
id
|
|
|
|
name
|
|
|
|
description
|
|
|
|
type
|
|
|
|
custom
|
|
|
|
mailhead
|
|
|
|
sortkey
|
|
|
|
obsolete
|
|
|
|
enter_bug
|
2010-10-07 15:31:09 +04:00
|
|
|
clone_bug
|
2009-07-29 15:21:49 +04:00
|
|
|
buglist
|
|
|
|
visibility_field_id
|
|
|
|
value_field_id
|
2010-12-09 20:16:43 +03:00
|
|
|
delta_ts
|
2010-12-10 21:02:52 +03:00
|
|
|
has_activity
|
2010-12-13 20:08:25 +03:00
|
|
|
add_to_deps
|
2011-12-19 17:11:45 +04:00
|
|
|
url
|
2008-12-15 15:53:33 +03:00
|
|
|
);
|
|
|
|
|
|
|
|
use constant REQUIRED_CREATE_FIELDS => qw(name description);
|
|
|
|
|
|
|
|
use constant VALIDATORS => {
|
2010-10-07 15:31:09 +04:00
|
|
|
custom => \&Bugzilla::Object::check_boolean,
|
2010-05-15 00:02:34 +04:00
|
|
|
description => \&_check_description,
|
2010-10-07 15:31:09 +04:00
|
|
|
enter_bug => \&Bugzilla::Object::check_boolean,
|
|
|
|
clone_bug => \&Bugzilla::Object::check_boolean,
|
2010-05-15 00:02:34 +04:00
|
|
|
buglist => \&Bugzilla::Object::check_boolean,
|
2010-10-07 15:31:09 +04:00
|
|
|
mailhead => \&Bugzilla::Object::check_boolean,
|
|
|
|
obsolete => \&Bugzilla::Object::check_boolean,
|
2010-05-15 00:02:34 +04:00
|
|
|
sortkey => \&_check_sortkey,
|
|
|
|
type => \&_check_type,
|
2009-07-29 15:21:49 +04:00
|
|
|
visibility_field_id => \&_check_visibility_field_id,
|
2010-12-13 20:08:25 +03:00
|
|
|
add_to_deps => \&_check_add_to_deps,
|
2009-07-29 15:21:49 +04:00
|
|
|
};
|
|
|
|
|
|
|
|
use constant UPDATE_VALIDATORS => {
|
|
|
|
value_field_id => \&_check_value_field_id,
|
2008-12-15 15:53:33 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
use constant UPDATE_COLUMNS => qw(
|
|
|
|
description
|
|
|
|
mailhead
|
|
|
|
sortkey
|
|
|
|
obsolete
|
|
|
|
enter_bug
|
2010-10-07 15:31:09 +04:00
|
|
|
clone_bug
|
2009-07-29 15:21:49 +04:00
|
|
|
buglist
|
|
|
|
visibility_field_id
|
|
|
|
value_field_id
|
|
|
|
type
|
2010-12-09 20:16:43 +03:00
|
|
|
delta_ts
|
2010-12-10 21:02:52 +03:00
|
|
|
has_activity
|
2010-12-13 20:08:25 +03:00
|
|
|
add_to_deps
|
2011-12-19 17:11:45 +04:00
|
|
|
url
|
2008-12-15 15:53:33 +03:00
|
|
|
);
|
|
|
|
|
|
|
|
# How various field types translate into SQL data definitions.
|
|
|
|
use constant SQL_DEFINITIONS => {
|
|
|
|
# Using commas because these are constants and they shouldn't
|
|
|
|
# be auto-quoted by the "=>" operator.
|
|
|
|
FIELD_TYPE_FREETEXT, { TYPE => 'varchar(255)' },
|
2011-12-19 17:11:45 +04:00
|
|
|
FIELD_TYPE_EXTURL, { TYPE => 'varchar(255)' },
|
2010-09-29 20:44:33 +04:00
|
|
|
FIELD_TYPE_SINGLE_SELECT, { TYPE => 'varchar(255)', NOTNULL => 1,
|
2008-12-15 15:53:33 +03:00
|
|
|
DEFAULT => "'---'" },
|
|
|
|
FIELD_TYPE_TEXTAREA, { TYPE => 'MEDIUMTEXT' },
|
|
|
|
FIELD_TYPE_DATETIME, { TYPE => 'DATETIME' },
|
2009-07-29 15:21:49 +04:00
|
|
|
FIELD_TYPE_BUG_ID, { TYPE => 'INT3' },
|
2010-12-06 21:32:19 +03:00
|
|
|
FIELD_TYPE_NUMERIC, { TYPE => 'NUMERIC', NOTNULL => 1, DEFAULT => '0' },
|
2008-12-15 15:53:33 +03:00
|
|
|
};
|
|
|
|
|
2010-11-10 21:21:19 +03:00
|
|
|
# FIXME add default value_fields here
|
2008-12-15 15:53:33 +03:00
|
|
|
# Field definitions for the fields that ship with Bugzilla.
|
|
|
|
# These are used by populate_field_definitions to populate
|
|
|
|
# the fielddefs table.
|
|
|
|
use constant DEFAULT_FIELDS => (
|
2011-08-02 14:56:32 +04:00
|
|
|
{name => 'bug_id', desc => 'Bug ID', buglist => 1, in_new_bugmail => 1},
|
2011-07-27 18:35:30 +04:00
|
|
|
{name => 'short_desc', desc => 'Summary', buglist => 1, in_new_bugmail => 1},
|
|
|
|
{name => 'classification', desc => 'Classification', buglist => 1, in_new_bugmail => 1},
|
|
|
|
{name => 'product', desc => 'Product', buglist => 1, in_new_bugmail => 1, type => FIELD_TYPE_SINGLE_SELECT},
|
|
|
|
{name => 'version', desc => 'Version', buglist => 1, in_new_bugmail => 1},
|
|
|
|
{name => 'rep_platform', desc => 'Platform', buglist => 1, in_new_bugmail => 1, type => FIELD_TYPE_SINGLE_SELECT},
|
|
|
|
{name => 'bug_file_loc', desc => 'URL', buglist => 1, in_new_bugmail => 1},
|
|
|
|
{name => 'op_sys', desc => 'OS/Version', buglist => 1, in_new_bugmail => 1, type => FIELD_TYPE_SINGLE_SELECT},
|
|
|
|
{name => 'bug_status', desc => 'Status', buglist => 1, in_new_bugmail => 1, type => FIELD_TYPE_SINGLE_SELECT},
|
|
|
|
{name => 'status_whiteboard', desc => 'Status Whiteboard', buglist => 1, in_new_bugmail => 1},
|
|
|
|
{name => 'keywords', desc => 'Keywords', buglist => 1, in_new_bugmail => 1},
|
|
|
|
{name => 'resolution', desc => 'Resolution', buglist => 1, type => FIELD_TYPE_SINGLE_SELECT},
|
|
|
|
{name => 'bug_severity', desc => 'Severity', buglist => 1, in_new_bugmail => 1, type => FIELD_TYPE_SINGLE_SELECT},
|
|
|
|
{name => 'priority', desc => 'Priority', buglist => 1, in_new_bugmail => 1, type => FIELD_TYPE_SINGLE_SELECT},
|
|
|
|
{name => 'component', desc => 'Component', buglist => 1, in_new_bugmail => 1},
|
|
|
|
{name => 'assigned_to', desc => 'Assignee', buglist => 1, in_new_bugmail => 1},
|
|
|
|
{name => 'reporter', desc => 'Reporter', buglist => 1, in_new_bugmail => 1},
|
2009-07-29 15:21:49 +04:00
|
|
|
{name => 'votes', desc => 'Votes', buglist => 1},
|
2011-07-27 18:35:30 +04:00
|
|
|
{name => 'qa_contact', desc => 'QA Contact', buglist => 1, in_new_bugmail => 1},
|
|
|
|
{name => 'cc', desc => 'CC', buglist => 1, in_new_bugmail => 1},
|
|
|
|
{name => 'dependson', desc => 'Depends on', buglist => 1, in_new_bugmail => 1},
|
|
|
|
{name => 'blocked', desc => 'Blocks', buglist => 1, in_new_bugmail => 1},
|
2011-09-27 00:36:56 +04:00
|
|
|
{name => 'dup_id', desc => 'Duplicate of', buglist => 1, in_new_bugmail => 1, type => FIELD_TYPE_BUG_ID},
|
2008-12-15 15:53:33 +03:00
|
|
|
|
|
|
|
{name => 'attachments.description', desc => 'Attachment description'},
|
|
|
|
{name => 'attachments.filename', desc => 'Attachment filename'},
|
|
|
|
{name => 'attachments.mimetype', desc => 'Attachment mime type'},
|
|
|
|
{name => 'attachments.ispatch', desc => 'Attachment is patch'},
|
|
|
|
{name => 'attachments.isobsolete', desc => 'Attachment is obsolete'},
|
|
|
|
{name => 'attachments.isprivate', desc => 'Attachment is private'},
|
|
|
|
{name => 'attachments.submitter', desc => 'Attachment creator'},
|
|
|
|
|
2011-07-27 18:35:30 +04:00
|
|
|
{name => 'target_milestone', desc => 'Target Milestone', buglist => 1},
|
|
|
|
{name => 'creation_ts', desc => 'Creation time', buglist => 1, in_new_bugmail => 1},
|
|
|
|
{name => 'delta_ts', desc => 'Last changed time', buglist => 1, in_new_bugmail => 1},
|
2008-12-15 15:53:33 +03:00
|
|
|
{name => 'longdesc', desc => 'Comment'},
|
|
|
|
{name => 'longdescs.isprivate', desc => 'Comment is private'},
|
2011-07-27 18:35:30 +04:00
|
|
|
{name => 'alias', desc => 'Alias', buglist => 1},
|
2008-12-15 15:53:33 +03:00
|
|
|
{name => 'everconfirmed', desc => 'Ever Confirmed'},
|
|
|
|
{name => 'reporter_accessible', desc => 'Reporter Accessible'},
|
|
|
|
{name => 'cclist_accessible', desc => 'CC Accessible'},
|
2011-07-27 18:35:30 +04:00
|
|
|
{name => 'bug_group', desc => 'Group', in_new_bugmail => 1},
|
|
|
|
{name => 'estimated_time', desc => 'Estimated Hours', buglist => 1, in_new_bugmail => 1},
|
|
|
|
{name => 'remaining_time', desc => 'Remaining Hours', buglist => 1},
|
|
|
|
{name => 'deadline', desc => 'Deadline', buglist => 1, in_new_bugmail => 1},
|
2008-12-15 15:53:33 +03:00
|
|
|
{name => 'commenter', desc => 'Commenter'},
|
2011-07-27 18:35:30 +04:00
|
|
|
{name => 'flagtypes.name', desc => 'Flag Types', buglist => 1},
|
2008-12-15 15:53:33 +03:00
|
|
|
{name => 'requestees.login_name', desc => 'Flag Requestee'},
|
|
|
|
{name => 'setters.login_name', desc => 'Flag Setter'},
|
2011-07-27 18:35:30 +04:00
|
|
|
{name => 'work_time', desc => 'Hours Worked', buglist => 1},
|
|
|
|
{name => 'percentage_complete', desc => 'Percentage Complete', buglist => 1},
|
2008-12-15 15:53:33 +03:00
|
|
|
{name => 'content', desc => 'Content'},
|
|
|
|
{name => 'attach_data.thedata', desc => 'Attachment data'},
|
|
|
|
{name => 'attachments.isurl', desc => 'Attachment is a URL'},
|
2011-07-20 16:48:26 +04:00
|
|
|
{name => 'owner_idle_time', desc => 'Time Since Assignee Touched'},
|
2011-07-27 18:35:30 +04:00
|
|
|
{name => 'see_also', desc => 'See Also', buglist => 1, type => FIELD_TYPE_BUG_URLS},
|
2008-12-15 15:53:33 +03:00
|
|
|
);
|
|
|
|
|
2009-07-29 15:21:49 +04:00
|
|
|
################
|
|
|
|
# Constructors #
|
|
|
|
################
|
|
|
|
|
|
|
|
# Override match to add is_select.
|
|
|
|
sub match {
|
|
|
|
my $self = shift;
|
|
|
|
my ($params) = @_;
|
|
|
|
if (delete $params->{is_select}) {
|
|
|
|
$params->{type} = [FIELD_TYPE_SINGLE_SELECT, FIELD_TYPE_MULTI_SELECT];
|
|
|
|
}
|
|
|
|
return $self->SUPER::match(@_);
|
|
|
|
}
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
##############
|
|
|
|
# Validators #
|
|
|
|
##############
|
|
|
|
|
|
|
|
sub _check_description {
|
|
|
|
my ($invocant, $desc) = @_;
|
|
|
|
$desc = clean_text($desc);
|
|
|
|
$desc || ThrowUserError('field_missing_description');
|
|
|
|
return $desc;
|
|
|
|
}
|
|
|
|
|
|
|
|
sub _check_name {
|
|
|
|
my ($invocant, $name, $is_custom) = @_;
|
|
|
|
$name = lc(clean_text($name));
|
|
|
|
$name || ThrowUserError('field_missing_name');
|
|
|
|
|
|
|
|
# Don't want to allow a name that might mess up SQL.
|
|
|
|
my $name_regex = qr/^[\w\.]+$/;
|
|
|
|
# Custom fields have more restrictive name requirements than
|
|
|
|
# standard fields.
|
2009-11-09 22:23:43 +03:00
|
|
|
$name_regex = qr/^[a-zA-Z0-9_]+$/ if $is_custom;
|
2008-12-15 15:53:33 +03:00
|
|
|
# Custom fields can't be named just "cf_", and there is no normal
|
|
|
|
# field named just "cf_".
|
|
|
|
($name =~ $name_regex && $name ne "cf_")
|
|
|
|
|| ThrowUserError('field_invalid_name', { name => $name });
|
|
|
|
|
2010-09-30 15:27:44 +04:00
|
|
|
# If it's custom, prepend cf_ to the custom field name to distinguish
|
2008-12-15 15:53:33 +03:00
|
|
|
# it from standard fields.
|
|
|
|
if ($name !~ /^cf_/ && $is_custom) {
|
|
|
|
$name = 'cf_' . $name;
|
|
|
|
}
|
|
|
|
|
|
|
|
# Assure the name is unique. Names can't be changed, so we don't have
|
|
|
|
# to worry about what to do on updates.
|
2010-10-14 14:31:30 +04:00
|
|
|
my $field = Bugzilla->get_field($name);
|
2008-12-15 15:53:33 +03:00
|
|
|
ThrowUserError('field_already_exists', {'field' => $field }) if $field;
|
|
|
|
|
|
|
|
return $name;
|
|
|
|
}
|
|
|
|
|
|
|
|
sub _check_sortkey {
|
|
|
|
my ($invocant, $sortkey) = @_;
|
|
|
|
my $skey = $sortkey;
|
|
|
|
if (!defined $skey || $skey eq '') {
|
|
|
|
($sortkey) = Bugzilla->dbh->selectrow_array(
|
|
|
|
'SELECT MAX(sortkey) + 100 FROM fielddefs') || 100;
|
|
|
|
}
|
|
|
|
detaint_natural($sortkey)
|
|
|
|
|| ThrowUserError('field_invalid_sortkey', { sortkey => $skey });
|
|
|
|
return $sortkey;
|
|
|
|
}
|
|
|
|
|
|
|
|
sub _check_type {
|
|
|
|
my ($invocant, $type) = @_;
|
|
|
|
my $saved_type = $type;
|
|
|
|
# The constant here should be updated every time a new,
|
|
|
|
# higher field type is added.
|
2010-07-29 17:25:07 +04:00
|
|
|
(detaint_natural($type) && $type <= FIELD_TYPE__BOUNDARY)
|
2008-12-15 15:53:33 +03:00
|
|
|
|| ThrowCodeError('invalid_customfield_type', { type => $saved_type });
|
|
|
|
return $type;
|
|
|
|
}
|
|
|
|
|
2009-07-29 15:21:49 +04:00
|
|
|
sub _check_value_field_id {
|
|
|
|
my ($invocant, $field_id, $is_select) = @_;
|
|
|
|
$is_select = $invocant->is_select if !defined $is_select;
|
|
|
|
if ($field_id && !$is_select) {
|
|
|
|
ThrowUserError('field_value_control_select_only');
|
|
|
|
}
|
|
|
|
return $invocant->_check_visibility_field_id($field_id);
|
|
|
|
}
|
|
|
|
|
|
|
|
sub _check_visibility_field_id {
|
|
|
|
my ($invocant, $field_id) = @_;
|
|
|
|
$field_id = trim($field_id);
|
|
|
|
return undef if !$field_id;
|
2010-10-14 14:31:30 +04:00
|
|
|
my $field = Bugzilla->get_field($field_id);
|
2009-07-29 15:21:49 +04:00
|
|
|
if (blessed($invocant) && $field->id == $invocant->id) {
|
|
|
|
ThrowUserError('field_cant_control_self', { field => $field });
|
|
|
|
}
|
|
|
|
if (!$field->is_select) {
|
|
|
|
ThrowUserError('field_control_must_be_select',
|
|
|
|
{ field => $field });
|
|
|
|
}
|
|
|
|
return $field->id;
|
|
|
|
}
|
|
|
|
|
2010-12-13 20:08:25 +03:00
|
|
|
# This has effect only for fields of FIELD_TYPE_BUG_ID type
|
|
|
|
# When 1, add field value (bug id) to list of bugs blocked by current
|
|
|
|
# When 2, add field value (bug id) to list of bugs depending on current
|
|
|
|
sub _check_add_to_deps
|
|
|
|
{
|
|
|
|
my ($invocant, $value) = @_;
|
|
|
|
my %addto = ('' => 0, 1 => 1, 2 => 2, no => 0, blocked => 1, dependson => 2);
|
|
|
|
return $addto{$value || ''};
|
|
|
|
}
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
=pod
|
|
|
|
|
|
|
|
=head2 Instance Properties
|
|
|
|
|
|
|
|
=over
|
|
|
|
|
|
|
|
=item C<name>
|
|
|
|
|
|
|
|
the name of the field in the database; begins with "cf_" if field
|
|
|
|
is a custom field, but test the value of the boolean "custom" property
|
|
|
|
to determine if a given field is a custom field;
|
|
|
|
|
|
|
|
=item C<description>
|
|
|
|
|
|
|
|
a short string describing the field; displayed to Bugzilla users
|
|
|
|
in several places within Bugzilla's UI, f.e. as the form field label
|
|
|
|
on the "show bug" page;
|
|
|
|
|
|
|
|
=back
|
|
|
|
|
|
|
|
=cut
|
|
|
|
|
|
|
|
sub description { return $_[0]->{description} }
|
|
|
|
|
|
|
|
=over
|
|
|
|
|
|
|
|
=item C<type>
|
|
|
|
|
|
|
|
an integer specifying the kind of field this is; values correspond to
|
|
|
|
the FIELD_TYPE_* constants in Constants.pm
|
|
|
|
|
|
|
|
=back
|
|
|
|
|
|
|
|
=cut
|
|
|
|
|
|
|
|
sub type { return $_[0]->{type} }
|
|
|
|
|
|
|
|
=over
|
|
|
|
|
|
|
|
=item C<custom>
|
|
|
|
|
|
|
|
a boolean specifying whether or not the field is a custom field;
|
|
|
|
if true, field name should start "cf_", but use this property to determine
|
|
|
|
which fields are custom fields;
|
|
|
|
|
|
|
|
=back
|
|
|
|
|
|
|
|
=cut
|
|
|
|
|
|
|
|
sub custom { return $_[0]->{custom} }
|
|
|
|
|
|
|
|
=over
|
|
|
|
|
|
|
|
=item C<in_new_bugmail>
|
|
|
|
|
|
|
|
a boolean specifying whether or not the field is displayed in bugmail
|
|
|
|
for newly-created bugs;
|
|
|
|
|
|
|
|
=back
|
|
|
|
|
|
|
|
=cut
|
|
|
|
|
|
|
|
sub in_new_bugmail { return $_[0]->{mailhead} }
|
|
|
|
|
|
|
|
=over
|
|
|
|
|
|
|
|
=item C<sortkey>
|
|
|
|
|
|
|
|
an integer specifying the sortkey of the field.
|
|
|
|
|
|
|
|
=back
|
|
|
|
|
|
|
|
=cut
|
|
|
|
|
|
|
|
sub sortkey { return $_[0]->{sortkey} }
|
|
|
|
|
|
|
|
=over
|
|
|
|
|
|
|
|
=item C<obsolete>
|
|
|
|
|
|
|
|
a boolean specifying whether or not the field is obsolete;
|
|
|
|
|
|
|
|
=back
|
|
|
|
|
|
|
|
=cut
|
|
|
|
|
|
|
|
sub obsolete { return $_[0]->{obsolete} }
|
|
|
|
|
|
|
|
=over
|
|
|
|
|
|
|
|
=item C<enter_bug>
|
|
|
|
|
2010-09-30 15:27:44 +04:00
|
|
|
A boolean specifying whether or not this field should appear on
|
2008-12-15 15:53:33 +03:00
|
|
|
enter_bug.cgi
|
|
|
|
|
|
|
|
=back
|
|
|
|
|
|
|
|
=cut
|
|
|
|
|
|
|
|
sub enter_bug { return $_[0]->{enter_bug} }
|
|
|
|
|
|
|
|
=over
|
|
|
|
|
2010-10-07 15:31:09 +04:00
|
|
|
=item C<clone_bug>
|
|
|
|
|
|
|
|
A boolean specifying whether or not this field should be copied on bug clone
|
|
|
|
|
|
|
|
=back
|
|
|
|
|
|
|
|
=cut
|
|
|
|
|
|
|
|
sub clone_bug { return $_[0]->{clone_bug} }
|
|
|
|
|
|
|
|
=over
|
|
|
|
|
2009-07-29 15:21:49 +04:00
|
|
|
=item C<buglist>
|
|
|
|
|
|
|
|
A boolean specifying whether or not this field is selectable
|
|
|
|
as a display or order column in buglist.cgi
|
|
|
|
|
|
|
|
=back
|
|
|
|
|
|
|
|
=cut
|
|
|
|
|
|
|
|
sub buglist { return $_[0]->{buglist} }
|
|
|
|
|
|
|
|
=over
|
|
|
|
|
|
|
|
=item C<is_select>
|
|
|
|
|
|
|
|
True if this is a C<FIELD_TYPE_SINGLE_SELECT> or C<FIELD_TYPE_MULTI_SELECT>
|
|
|
|
field. It is only safe to call L</legal_values> if this is true.
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
=item C<legal_values>
|
|
|
|
|
2009-07-29 15:21:49 +04:00
|
|
|
Valid values for this field, as an array of L<Bugzilla::Field::Choice>
|
|
|
|
objects.
|
2008-12-15 15:53:33 +03:00
|
|
|
|
|
|
|
=back
|
|
|
|
|
|
|
|
=cut
|
|
|
|
|
2010-09-30 15:27:44 +04:00
|
|
|
sub is_select {
|
|
|
|
return ($_[0]->type == FIELD_TYPE_SINGLE_SELECT
|
|
|
|
|| $_[0]->type == FIELD_TYPE_MULTI_SELECT) ? 1 : 0
|
2009-07-29 15:21:49 +04:00
|
|
|
}
|
|
|
|
|
2010-12-10 21:02:52 +03:00
|
|
|
sub has_activity { $_[0]->{has_activity} }
|
|
|
|
|
2010-12-13 20:08:25 +03:00
|
|
|
sub add_to_deps { $_[0]->{add_to_deps} }
|
|
|
|
|
2011-12-19 17:11:45 +04:00
|
|
|
sub url { $_[0]->{url} }
|
|
|
|
|
2012-02-17 14:49:15 +04:00
|
|
|
# Includes disabled values is $include_disabled = true
|
|
|
|
# The full list with disabled values is not cached, as only used in administration
|
2010-10-27 19:53:21 +04:00
|
|
|
sub legal_values
|
|
|
|
{
|
2008-12-15 15:53:33 +03:00
|
|
|
my $self = shift;
|
2012-02-17 14:49:15 +04:00
|
|
|
my ($include_disabled) = @_;
|
2010-10-27 19:53:21 +04:00
|
|
|
return [] unless $self->is_select;
|
2012-02-17 14:49:15 +04:00
|
|
|
return [ Bugzilla::Field::Choice->type($self)->get_all('include_disabled') ] if $include_disabled;
|
2010-10-27 19:53:21 +04:00
|
|
|
if (!defined $self->{legal_values})
|
|
|
|
{
|
2009-07-29 15:21:49 +04:00
|
|
|
require Bugzilla::Field::Choice;
|
2010-10-27 19:53:21 +04:00
|
|
|
$self->{legal_values} = [ Bugzilla::Field::Choice->type($self)->get_all() ];
|
2008-12-15 15:53:33 +03:00
|
|
|
}
|
2010-10-27 19:53:21 +04:00
|
|
|
return $self->{legal_values};
|
2008-12-15 15:53:33 +03:00
|
|
|
}
|
|
|
|
|
2012-02-17 14:49:15 +04:00
|
|
|
# Always excludes disabled values
|
2010-12-10 02:53:33 +03:00
|
|
|
sub legal_value_names
|
|
|
|
{
|
|
|
|
my $self = shift;
|
|
|
|
return [] unless $self->is_select;
|
|
|
|
if (!$self->{legal_value_names})
|
|
|
|
{
|
|
|
|
require Bugzilla::Field::Choice;
|
|
|
|
my $type = Bugzilla::Field::Choice->type($self);
|
|
|
|
$self->{legal_value_names} = $type->get_all_names();
|
|
|
|
}
|
|
|
|
return $self->{legal_value_names};
|
|
|
|
}
|
|
|
|
|
2012-02-17 14:49:15 +04:00
|
|
|
# Always excludes disabled values
|
2009-09-07 21:19:11 +04:00
|
|
|
sub restricted_legal_values
|
|
|
|
{
|
|
|
|
my $self = shift;
|
2010-03-12 22:18:39 +03:00
|
|
|
my ($controller_value) = @_;
|
2010-11-24 20:22:45 +03:00
|
|
|
$controller_value = $controller_value->id if ref $controller_value;
|
2010-10-27 19:53:21 +04:00
|
|
|
return $self->legal_values unless $controller_value && $self->value_field_id;
|
2010-11-24 20:22:45 +03:00
|
|
|
if (!$self->{restricted_legal_values}->{$controller_value})
|
2010-10-27 19:53:21 +04:00
|
|
|
{
|
|
|
|
my $hash = Bugzilla->fieldvaluecontrol_hash->{$self->value_field_id}->{values}->{$self->id};
|
2010-11-24 20:22:45 +03:00
|
|
|
$self->{restricted_legal_values}->{$controller_value} = [
|
2011-10-24 16:16:25 +04:00
|
|
|
grep {
|
2011-10-24 16:17:30 +04:00
|
|
|
$_->is_static || !exists $hash->{$_->id} ||
|
2011-10-24 16:16:25 +04:00
|
|
|
!%{$hash->{$_->id}} || $hash->{$_->id}->{$controller_value}
|
|
|
|
} @{$self->legal_values}
|
2010-10-27 19:53:21 +04:00
|
|
|
];
|
|
|
|
}
|
2010-11-24 20:22:45 +03:00
|
|
|
return $self->{restricted_legal_values}->{$controller_value};
|
2009-09-07 21:19:11 +04:00
|
|
|
}
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
=pod
|
|
|
|
|
2009-07-29 15:21:49 +04:00
|
|
|
=over
|
|
|
|
|
|
|
|
=item C<visibility_field>
|
|
|
|
|
|
|
|
What field controls this field's visibility? Returns a C<Bugzilla::Field>
|
|
|
|
object representing the field that controls this field's visibility.
|
|
|
|
|
|
|
|
Returns undef if there is no field that controls this field's visibility.
|
|
|
|
|
|
|
|
=back
|
|
|
|
|
|
|
|
=cut
|
|
|
|
|
|
|
|
sub visibility_field {
|
|
|
|
my $self = shift;
|
|
|
|
if ($self->{visibility_field_id}) {
|
2010-09-30 15:27:44 +04:00
|
|
|
$self->{visibility_field} ||=
|
2009-07-29 15:21:49 +04:00
|
|
|
$self->new($self->{visibility_field_id});
|
|
|
|
}
|
|
|
|
return $self->{visibility_field};
|
|
|
|
}
|
|
|
|
|
2010-10-14 14:31:30 +04:00
|
|
|
sub visibility_field_id
|
|
|
|
{
|
|
|
|
my $self = shift;
|
|
|
|
return $self->{visibility_field_id};
|
|
|
|
}
|
|
|
|
|
2010-03-12 22:18:39 +03:00
|
|
|
sub visibility_values
|
|
|
|
{
|
|
|
|
my $self = shift;
|
2010-11-24 16:41:09 +03:00
|
|
|
return undef if !$self->visibility_field_id;
|
2010-03-12 22:18:39 +03:00
|
|
|
my $f;
|
|
|
|
if ($self->visibility_field && !($f = $self->{visibility_values}))
|
|
|
|
{
|
2010-11-24 16:41:09 +03:00
|
|
|
$f = [ keys %{Bugzilla->fieldvaluecontrol_hash
|
|
|
|
->{$self->visibility_field_id}
|
|
|
|
->{fields}
|
|
|
|
->{$self->id} || {} } ];
|
2010-03-12 22:18:39 +03:00
|
|
|
if (@$f)
|
|
|
|
{
|
|
|
|
my $type = Bugzilla::Field::Choice->type($self->visibility_field);
|
|
|
|
$f = $type->match({ id => $f });
|
|
|
|
}
|
|
|
|
$self->{visibility_values} = $f;
|
|
|
|
}
|
|
|
|
return $f;
|
|
|
|
}
|
2009-07-29 15:21:49 +04:00
|
|
|
|
2010-03-12 22:18:39 +03:00
|
|
|
sub has_visibility_value
|
|
|
|
{
|
|
|
|
my $self = shift;
|
2010-11-24 16:41:09 +03:00
|
|
|
return 1 if !$self->visibility_field_id;
|
2010-03-12 22:18:39 +03:00
|
|
|
my ($value) = @_;
|
2010-11-24 16:41:09 +03:00
|
|
|
$value = $value->id if ref $value;
|
|
|
|
my $hash = Bugzilla->fieldvaluecontrol_hash
|
|
|
|
->{$self->visibility_field_id}
|
|
|
|
->{fields}
|
|
|
|
->{$self->id};
|
|
|
|
return !$hash || !%$hash || $hash->{$value};
|
2010-03-12 22:18:39 +03:00
|
|
|
}
|
2009-07-29 15:21:49 +04:00
|
|
|
|
2010-03-12 22:18:39 +03:00
|
|
|
# Check visibility of field for a bug
|
|
|
|
sub check_visibility
|
|
|
|
{
|
2009-07-29 15:21:49 +04:00
|
|
|
my $self = shift;
|
2010-11-24 16:41:09 +03:00
|
|
|
my $bug = shift || return 1;
|
|
|
|
my $vf = $self->visibility_field || return 1;
|
|
|
|
my $m = $vf->name;
|
2011-05-24 19:59:11 +04:00
|
|
|
my $value = blessed $bug ? $bug->$m : $bug->{$m};
|
|
|
|
if (!blessed $value)
|
|
|
|
{
|
2011-10-20 17:46:54 +04:00
|
|
|
# FIXME: This does not allow selecting of fields
|
|
|
|
# non-uniquely identified by name, as a visibility
|
|
|
|
# controller field (for example, "component")
|
2011-05-24 19:59:11 +04:00
|
|
|
$value = Bugzilla::Field::Choice->type($vf)->new({ name => $value }) || return 1;
|
|
|
|
}
|
2010-11-24 16:41:09 +03:00
|
|
|
return $self->has_visibility_value($value);
|
2009-07-29 15:21:49 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
=pod
|
|
|
|
|
|
|
|
=over
|
|
|
|
|
|
|
|
=item C<controls_visibility_of>
|
|
|
|
|
|
|
|
An arrayref of C<Bugzilla::Field> objects, representing fields that this
|
|
|
|
field controls the visibility of.
|
|
|
|
|
|
|
|
=back
|
|
|
|
|
|
|
|
=cut
|
|
|
|
|
|
|
|
sub controls_visibility_of {
|
|
|
|
my $self = shift;
|
2010-10-14 14:31:30 +04:00
|
|
|
$self->{controls_visibility_of} ||= [ Bugzilla->get_fields({ visibility_field_id => $self->id }) ];
|
2009-07-29 15:21:49 +04:00
|
|
|
return $self->{controls_visibility_of};
|
|
|
|
}
|
|
|
|
|
|
|
|
=pod
|
|
|
|
|
|
|
|
=over
|
|
|
|
|
|
|
|
=item C<value_field>
|
|
|
|
|
|
|
|
The Bugzilla::Field that controls the list of values for this field.
|
|
|
|
|
|
|
|
Returns undef if there is no field that controls this field's visibility.
|
|
|
|
|
|
|
|
=back
|
|
|
|
|
|
|
|
=cut
|
|
|
|
|
|
|
|
sub value_field {
|
|
|
|
my $self = shift;
|
|
|
|
if ($self->{value_field_id}) {
|
|
|
|
$self->{value_field} ||= $self->new($self->{value_field_id});
|
|
|
|
}
|
|
|
|
return $self->{value_field};
|
|
|
|
}
|
|
|
|
|
2010-10-14 14:31:30 +04:00
|
|
|
sub value_field_id
|
|
|
|
{
|
|
|
|
my $self = shift;
|
|
|
|
return $self->{value_field_id};
|
|
|
|
}
|
|
|
|
|
2009-07-29 15:21:49 +04:00
|
|
|
=pod
|
|
|
|
|
|
|
|
=over
|
|
|
|
|
|
|
|
=item C<controls_values_of>
|
|
|
|
|
|
|
|
An arrayref of C<Bugzilla::Field> objects, representing fields that this
|
|
|
|
field controls the values of.
|
|
|
|
|
|
|
|
=back
|
|
|
|
|
|
|
|
=cut
|
|
|
|
|
|
|
|
sub controls_values_of {
|
|
|
|
my $self = shift;
|
2010-10-14 14:31:30 +04:00
|
|
|
$self->{controls_values_of} ||= [ Bugzilla->get_fields({ value_field_id => $self->id }) ];
|
2009-07-29 15:21:49 +04:00
|
|
|
return $self->{controls_values_of};
|
|
|
|
}
|
|
|
|
|
|
|
|
=pod
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
=head2 Instance Mutators
|
|
|
|
|
|
|
|
These set the particular field that they are named after.
|
|
|
|
|
|
|
|
They take a single value--the new value for that field.
|
|
|
|
|
|
|
|
They will throw an error if you try to set the values to something invalid.
|
|
|
|
|
|
|
|
=over
|
|
|
|
|
|
|
|
=item C<set_description>
|
|
|
|
|
|
|
|
=item C<set_enter_bug>
|
|
|
|
|
2010-10-07 15:31:09 +04:00
|
|
|
=item C<set_clone_bug>
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
=item C<set_obsolete>
|
|
|
|
|
|
|
|
=item C<set_sortkey>
|
|
|
|
|
|
|
|
=item C<set_in_new_bugmail>
|
|
|
|
|
2009-07-29 15:21:49 +04:00
|
|
|
=item C<set_buglist>
|
|
|
|
|
|
|
|
=item C<set_visibility_field>
|
|
|
|
|
|
|
|
=item C<set_value_field>
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
=back
|
|
|
|
|
|
|
|
=cut
|
|
|
|
|
|
|
|
sub set_description { $_[0]->set('description', $_[1]); }
|
|
|
|
sub set_enter_bug { $_[0]->set('enter_bug', $_[1]); }
|
2010-10-07 15:31:09 +04:00
|
|
|
sub set_clone_bug { $_[0]->set('clone_bug', $_[1]); }
|
2008-12-15 15:53:33 +03:00
|
|
|
sub set_obsolete { $_[0]->set('obsolete', $_[1]); }
|
|
|
|
sub set_sortkey { $_[0]->set('sortkey', $_[1]); }
|
|
|
|
sub set_in_new_bugmail { $_[0]->set('mailhead', $_[1]); }
|
2009-07-29 15:21:49 +04:00
|
|
|
sub set_buglist { $_[0]->set('buglist', $_[1]); }
|
2010-12-13 20:08:25 +03:00
|
|
|
sub set_add_to_deps { $_[0]->set('add_to_deps', $_[1]); }
|
2011-12-19 17:11:45 +04:00
|
|
|
sub set_url { $_[0]->set('url', $_[1]); }
|
2010-03-12 22:18:39 +03:00
|
|
|
|
|
|
|
sub set_visibility_field
|
|
|
|
{
|
2009-07-29 15:21:49 +04:00
|
|
|
my ($self, $value) = @_;
|
|
|
|
$self->set('visibility_field_id', $value);
|
|
|
|
delete $self->{visibility_field};
|
2010-03-12 22:18:39 +03:00
|
|
|
delete $self->{visibility_values};
|
2009-07-29 15:21:49 +04:00
|
|
|
}
|
2010-03-12 22:18:39 +03:00
|
|
|
|
|
|
|
sub set_visibility_values
|
|
|
|
{
|
|
|
|
my $self = shift;
|
|
|
|
my ($value_ids) = @_;
|
2010-11-10 21:21:19 +03:00
|
|
|
update_visibility_values($self, 0, $value_ids);
|
2010-03-12 22:18:39 +03:00
|
|
|
delete $self->{visibility_values};
|
2010-05-26 20:03:17 +04:00
|
|
|
return $value_ids && @$value_ids;
|
2009-07-29 15:21:49 +04:00
|
|
|
}
|
2010-03-12 22:18:39 +03:00
|
|
|
|
|
|
|
sub set_value_field
|
|
|
|
{
|
2009-07-29 15:21:49 +04:00
|
|
|
my ($self, $value) = @_;
|
|
|
|
$self->set('value_field_id', $value);
|
|
|
|
delete $self->{value_field};
|
|
|
|
}
|
|
|
|
|
|
|
|
# This is only used internally by upgrade code in Bugzilla::Field.
|
|
|
|
sub _set_type { $_[0]->set('type', $_[1]); }
|
2008-12-15 15:53:33 +03:00
|
|
|
|
|
|
|
=pod
|
|
|
|
|
|
|
|
=head2 Instance Method
|
|
|
|
|
|
|
|
=over
|
|
|
|
|
|
|
|
=item C<remove_from_db>
|
|
|
|
|
|
|
|
Attempts to remove the passed in field from the database.
|
|
|
|
Deleting a field is only successful if the field is obsolete and
|
|
|
|
there are no values specified (or EVER specified) for the field.
|
|
|
|
|
|
|
|
=back
|
|
|
|
|
|
|
|
=cut
|
|
|
|
|
|
|
|
sub remove_from_db {
|
|
|
|
my $self = shift;
|
|
|
|
my $dbh = Bugzilla->dbh;
|
|
|
|
|
|
|
|
my $name = $self->name;
|
|
|
|
|
|
|
|
if (!$self->custom) {
|
|
|
|
ThrowCodeError('field_not_custom', {'name' => $name });
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$self->obsolete) {
|
|
|
|
ThrowUserError('customfield_not_obsolete', {'name' => $self->name });
|
|
|
|
}
|
|
|
|
|
|
|
|
$dbh->bz_start_transaction();
|
|
|
|
|
|
|
|
# Check to see if bug activity table has records (should be fast with index)
|
|
|
|
my $has_activity = $dbh->selectrow_array("SELECT COUNT(*) FROM bugs_activity
|
|
|
|
WHERE fieldid = ?", undef, $self->id);
|
|
|
|
if ($has_activity) {
|
|
|
|
ThrowUserError('customfield_has_activity', {'name' => $name });
|
|
|
|
}
|
|
|
|
|
|
|
|
# Check to see if bugs table has records (slow)
|
|
|
|
my $bugs_query = "";
|
|
|
|
|
|
|
|
if ($self->type == FIELD_TYPE_MULTI_SELECT) {
|
|
|
|
$bugs_query = "SELECT COUNT(*) FROM bug_$name";
|
|
|
|
}
|
|
|
|
else {
|
2010-05-15 00:02:34 +04:00
|
|
|
$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 != ''";
|
|
|
|
}
|
2008-12-15 15:53:33 +03:00
|
|
|
# Ignore the default single select value
|
|
|
|
if ($self->type == FIELD_TYPE_SINGLE_SELECT) {
|
|
|
|
$bugs_query .= " AND $name != '---'";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
my $has_bugs = $dbh->selectrow_array($bugs_query);
|
|
|
|
if ($has_bugs) {
|
|
|
|
ThrowUserError('customfield_has_contents', {'name' => $name });
|
|
|
|
}
|
|
|
|
|
|
|
|
# Once we reach here, we should be OK to delete.
|
|
|
|
$dbh->do('DELETE FROM fielddefs WHERE id = ?', undef, $self->id);
|
|
|
|
|
|
|
|
my $type = $self->type;
|
|
|
|
|
|
|
|
# the values for multi-select are stored in a seperate table
|
|
|
|
if ($type != FIELD_TYPE_MULTI_SELECT) {
|
|
|
|
$dbh->bz_drop_column('bugs', $name);
|
|
|
|
}
|
|
|
|
|
2009-07-29 15:21:49 +04:00
|
|
|
if ($self->is_select) {
|
2008-12-15 15:53:33 +03:00
|
|
|
# Delete the table that holds the legal values for this field.
|
|
|
|
$dbh->bz_drop_field_tables($self);
|
|
|
|
}
|
|
|
|
|
2010-03-12 22:18:39 +03:00
|
|
|
$self->set_visibility_values(undef);
|
|
|
|
|
2010-12-10 22:14:39 +03:00
|
|
|
# Update some other field (refresh the cache)
|
|
|
|
Bugzilla->get_field('delta_ts')->touch;
|
2010-12-10 02:53:33 +03:00
|
|
|
Bugzilla->refresh_cache_fields;
|
2010-11-24 21:25:51 +03:00
|
|
|
|
2010-03-12 22:18:39 +03:00
|
|
|
$dbh->bz_commit_transaction();
|
2008-12-15 15:53:33 +03:00
|
|
|
}
|
|
|
|
|
2010-11-24 21:25:51 +03:00
|
|
|
# Overridden update() method - flushes field cache
|
|
|
|
sub update
|
|
|
|
{
|
|
|
|
my $self = shift;
|
2010-12-09 20:16:43 +03:00
|
|
|
$self->{delta_ts} = POSIX::strftime('%Y-%m-%d %H:%M:%S', localtime);
|
2010-11-24 21:25:51 +03:00
|
|
|
my ($changes, $old_self) = $self->SUPER::update(@_);
|
2010-12-10 02:53:33 +03:00
|
|
|
Bugzilla->refresh_cache_fields;
|
2010-11-24 21:25:51 +03:00
|
|
|
return wantarray ? ($changes, $old_self) : $changes;
|
|
|
|
}
|
|
|
|
|
2010-12-10 02:53:33 +03:00
|
|
|
# Update field change timestamp (needed for cache flushing)
|
|
|
|
sub touch
|
|
|
|
{
|
|
|
|
my $self = shift;
|
|
|
|
$self->{delta_ts} = POSIX::strftime('%Y-%m-%d %H:%M:%S', localtime);
|
2010-12-10 21:02:52 +03:00
|
|
|
$self->update;
|
2010-12-10 02:53:33 +03:00
|
|
|
}
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
=pod
|
|
|
|
|
|
|
|
=head2 Class Methods
|
|
|
|
|
|
|
|
=over
|
|
|
|
|
|
|
|
=item C<create>
|
|
|
|
|
|
|
|
Just like L<Bugzilla::Object/create>. Takes the following parameters:
|
|
|
|
|
|
|
|
=over
|
|
|
|
|
|
|
|
=item C<name> B<Required> - The name of the field.
|
|
|
|
|
|
|
|
=item C<description> B<Required> - The field label to display in the UI.
|
|
|
|
|
|
|
|
=item C<mailhead> - boolean - Whether this field appears at the
|
|
|
|
top of the bugmail for a newly-filed bug. Defaults to 0.
|
|
|
|
|
|
|
|
=item C<custom> - boolean - True if this is a Custom Field. The field
|
|
|
|
will be added to the C<bugs> table if it does not exist. Defaults to 0.
|
|
|
|
|
|
|
|
=item C<sortkey> - integer - The sortkey of the field. Defaults to 0.
|
|
|
|
|
|
|
|
=item C<enter_bug> - boolean - Whether this field is
|
|
|
|
editable on the bug creation form. Defaults to 0.
|
|
|
|
|
2009-07-29 15:21:49 +04:00
|
|
|
=item C<buglist> - boolean - Whether this field is
|
|
|
|
selectable as a display or order column in bug lists. Defaults to 0.
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
C<obsolete> - boolean - Whether this field is obsolete. Defaults to 0.
|
|
|
|
|
|
|
|
=back
|
|
|
|
|
|
|
|
=back
|
|
|
|
|
|
|
|
=cut
|
|
|
|
|
|
|
|
sub create {
|
|
|
|
my $class = shift;
|
2010-09-30 15:27:44 +04:00
|
|
|
my ($params) = @_;
|
|
|
|
|
|
|
|
# We must set up database schema BEFORE inserting a row into fielddefs!
|
2010-12-10 22:14:39 +03:00
|
|
|
$params->{delta_ts} = POSIX::strftime('%Y-%m-%d %H:%M:%S', localtime);
|
2010-09-30 15:27:44 +04:00
|
|
|
$class->check_required_create_fields($params);
|
|
|
|
my $field_values = $class->run_create_validators($params);
|
|
|
|
my $obj = bless $field_values, ref($class)||$class;
|
2008-12-15 15:53:33 +03:00
|
|
|
|
|
|
|
my $dbh = Bugzilla->dbh;
|
2010-09-30 15:27:44 +04:00
|
|
|
if ($obj->custom) {
|
|
|
|
my $name = $obj->name;
|
|
|
|
my $type = $obj->type;
|
2008-12-15 15:53:33 +03:00
|
|
|
if (SQL_DEFINITIONS->{$type}) {
|
|
|
|
# Create the database column that stores the data for this field.
|
|
|
|
$dbh->bz_add_column('bugs', $name, SQL_DEFINITIONS->{$type});
|
|
|
|
}
|
|
|
|
|
2010-09-30 15:27:44 +04:00
|
|
|
if ($obj->is_select) {
|
2008-12-15 15:53:33 +03:00
|
|
|
# Create the table that holds the legal values for this field.
|
2010-09-30 15:27:44 +04:00
|
|
|
$dbh->bz_add_field_tables($obj);
|
2008-12-15 15:53:33 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($type == FIELD_TYPE_SINGLE_SELECT) {
|
|
|
|
# Insert a default value of "---" into the legal values table.
|
|
|
|
$dbh->do("INSERT INTO $name (value) VALUES ('---')");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2010-09-30 15:27:44 +04:00
|
|
|
# Call real constructor
|
2010-11-24 21:25:51 +03:00
|
|
|
my $self = $class->SUPER::create($params);
|
|
|
|
|
2010-12-10 02:53:33 +03:00
|
|
|
# Refresh fields inside single request
|
|
|
|
Bugzilla->refresh_cache_fields;
|
2010-11-24 21:25:51 +03:00
|
|
|
|
|
|
|
return $self;
|
2008-12-15 15:53:33 +03:00
|
|
|
}
|
|
|
|
|
2010-12-09 20:16:43 +03:00
|
|
|
sub run_create_validators
|
|
|
|
{
|
2008-12-15 15:53:33 +03:00
|
|
|
my $class = shift;
|
|
|
|
my $dbh = Bugzilla->dbh;
|
|
|
|
my $params = $class->SUPER::run_create_validators(@_);
|
|
|
|
|
|
|
|
$params->{name} = $class->_check_name($params->{name}, $params->{custom});
|
|
|
|
if (!exists $params->{sortkey}) {
|
|
|
|
$params->{sortkey} = $dbh->selectrow_array(
|
|
|
|
"SELECT MAX(sortkey) + 100 FROM fielddefs") || 100;
|
|
|
|
}
|
|
|
|
|
2009-07-29 15:21:49 +04:00
|
|
|
my $type = $params->{type} || 0;
|
2010-09-30 15:27:44 +04:00
|
|
|
|
2010-05-15 00:02:34 +04:00
|
|
|
if ($params->{custom} && !$type) {
|
|
|
|
ThrowCodeError('field_type_not_specified');
|
|
|
|
}
|
2010-09-30 15:27:44 +04:00
|
|
|
|
|
|
|
$params->{value_field_id} =
|
2009-07-29 15:21:49 +04:00
|
|
|
$class->_check_value_field_id($params->{value_field_id},
|
2010-09-30 15:27:44 +04:00
|
|
|
($type == FIELD_TYPE_SINGLE_SELECT
|
2009-07-29 15:21:49 +04:00
|
|
|
|| $type == FIELD_TYPE_MULTI_SELECT) ? 1 : 0);
|
2008-12-15 15:53:33 +03:00
|
|
|
return $params;
|
|
|
|
}
|
|
|
|
|
|
|
|
=pod
|
|
|
|
|
|
|
|
=over
|
|
|
|
|
|
|
|
=item C<get_legal_field_values($field)>
|
|
|
|
|
|
|
|
Description: returns all the legal values for a field that has a
|
|
|
|
list of legal values, like rep_platform or resolution.
|
|
|
|
The table where these values are stored must at least have
|
|
|
|
the following columns: value, isactive, sortkey.
|
|
|
|
|
|
|
|
Params: C<$field> - Name of the table where valid values are.
|
|
|
|
|
|
|
|
Returns: a reference to a list of valid values.
|
|
|
|
|
|
|
|
=back
|
|
|
|
|
|
|
|
=cut
|
|
|
|
|
|
|
|
sub get_legal_field_values {
|
|
|
|
my ($field) = @_;
|
|
|
|
my $dbh = Bugzilla->dbh;
|
|
|
|
my $result_ref = $dbh->selectcol_arrayref(
|
|
|
|
"SELECT value FROM $field
|
|
|
|
WHERE isactive = ?
|
|
|
|
ORDER BY sortkey, value", undef, (1));
|
|
|
|
return $result_ref;
|
|
|
|
}
|
|
|
|
|
|
|
|
=over
|
|
|
|
|
|
|
|
=item C<populate_field_definitions()>
|
|
|
|
|
|
|
|
Description: Populates the fielddefs table during an installation
|
|
|
|
or upgrade.
|
|
|
|
|
|
|
|
Params: none
|
|
|
|
|
|
|
|
Returns: nothing
|
|
|
|
|
|
|
|
=back
|
|
|
|
|
|
|
|
=cut
|
|
|
|
|
|
|
|
sub populate_field_definitions {
|
|
|
|
my $dbh = Bugzilla->dbh;
|
|
|
|
|
|
|
|
# ADD and UPDATE field definitions
|
|
|
|
foreach my $def (DEFAULT_FIELDS) {
|
|
|
|
my $field = new Bugzilla::Field({ name => $def->{name} });
|
|
|
|
if ($field) {
|
|
|
|
$field->set_description($def->{desc});
|
|
|
|
$field->set_in_new_bugmail($def->{in_new_bugmail});
|
2009-07-29 15:21:49 +04:00
|
|
|
$field->set_buglist($def->{buglist});
|
|
|
|
$field->_set_type($def->{type}) if $def->{type};
|
2008-12-15 15:53:33 +03:00
|
|
|
$field->update();
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
if (exists $def->{in_new_bugmail}) {
|
|
|
|
$def->{mailhead} = $def->{in_new_bugmail};
|
|
|
|
delete $def->{in_new_bugmail};
|
|
|
|
}
|
2009-07-29 15:21:49 +04:00
|
|
|
$def->{description} = delete $def->{desc};
|
2008-12-15 15:53:33 +03:00
|
|
|
Bugzilla::Field->create($def);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
# DELETE fields which were added only accidentally, or which
|
|
|
|
# were never tracked in bugs_activity. Note that you can never
|
|
|
|
# delete fields which are used by bugs_activity.
|
|
|
|
|
|
|
|
# Oops. Bug 163299
|
|
|
|
$dbh->do("DELETE FROM fielddefs WHERE name='cc_accessible'");
|
|
|
|
# Oops. Bug 215319
|
|
|
|
$dbh->do("DELETE FROM fielddefs WHERE name='requesters.login_name'");
|
|
|
|
# This field was never tracked in bugs_activity, so it's safe to delete.
|
|
|
|
$dbh->do("DELETE FROM fielddefs WHERE name='attachments.thedata'");
|
|
|
|
|
|
|
|
# MODIFY old field definitions
|
|
|
|
|
|
|
|
# 2005-11-13 LpSolit@gmail.com - Bug 302599
|
|
|
|
# One of the field names was a fragment of SQL code, which is DB dependent.
|
|
|
|
# We have to rename it to a real name, which is DB independent.
|
|
|
|
my $new_field_name = 'days_elapsed';
|
|
|
|
my $field_description = 'Days since bug changed';
|
|
|
|
|
|
|
|
my ($old_field_id, $old_field_name) =
|
|
|
|
$dbh->selectrow_array('SELECT id, name FROM fielddefs
|
|
|
|
WHERE description = ?',
|
|
|
|
undef, $field_description);
|
|
|
|
|
|
|
|
if ($old_field_id && ($old_field_name ne $new_field_name)) {
|
|
|
|
print "SQL fragment found in the 'fielddefs' table...\n";
|
|
|
|
print "Old field name: " . $old_field_name . "\n";
|
|
|
|
# We have to fix saved searches first. Queries have been escaped
|
|
|
|
# before being saved. We have to do the same here to find them.
|
|
|
|
$old_field_name = url_quote($old_field_name);
|
|
|
|
my $broken_named_queries =
|
|
|
|
$dbh->selectall_arrayref('SELECT userid, name, query
|
|
|
|
FROM namedqueries WHERE ' .
|
|
|
|
$dbh->sql_istrcmp('query', '?', 'LIKE'),
|
|
|
|
undef, "%=$old_field_name%");
|
|
|
|
|
|
|
|
my $sth_UpdateQueries = $dbh->prepare('UPDATE namedqueries SET query = ?
|
|
|
|
WHERE userid = ? AND name = ?');
|
|
|
|
|
|
|
|
print "Fixing saved searches...\n" if scalar(@$broken_named_queries);
|
|
|
|
foreach my $named_query (@$broken_named_queries) {
|
|
|
|
my ($userid, $name, $query) = @$named_query;
|
|
|
|
$query =~ s/=\Q$old_field_name\E(&|$)/=$new_field_name$1/gi;
|
|
|
|
$sth_UpdateQueries->execute($query, $userid, $name);
|
|
|
|
}
|
|
|
|
|
|
|
|
# We now do the same with saved chart series.
|
|
|
|
my $broken_series =
|
|
|
|
$dbh->selectall_arrayref('SELECT series_id, query
|
|
|
|
FROM series WHERE ' .
|
|
|
|
$dbh->sql_istrcmp('query', '?', 'LIKE'),
|
|
|
|
undef, "%=$old_field_name%");
|
|
|
|
|
|
|
|
my $sth_UpdateSeries = $dbh->prepare('UPDATE series SET query = ?
|
|
|
|
WHERE series_id = ?');
|
|
|
|
|
|
|
|
print "Fixing saved chart series...\n" if scalar(@$broken_series);
|
|
|
|
foreach my $series (@$broken_series) {
|
|
|
|
my ($series_id, $query) = @$series;
|
|
|
|
$query =~ s/=\Q$old_field_name\E(&|$)/=$new_field_name$1/gi;
|
|
|
|
$sth_UpdateSeries->execute($query, $series_id);
|
|
|
|
}
|
|
|
|
# Now that saved searches have been fixed, we can fix the field name.
|
|
|
|
print "Fixing the 'fielddefs' table...\n";
|
|
|
|
print "New field name: " . $new_field_name . "\n";
|
|
|
|
$dbh->do('UPDATE fielddefs SET name = ? WHERE id = ?',
|
|
|
|
undef, ($new_field_name, $old_field_id));
|
|
|
|
}
|
|
|
|
|
|
|
|
# This field has to be created separately, or the above upgrade code
|
|
|
|
# might not run properly.
|
2010-09-30 15:27:44 +04:00
|
|
|
Bugzilla::Field->create({ name => $new_field_name,
|
2008-12-15 15:53:33 +03:00
|
|
|
description => $field_description })
|
|
|
|
unless new Bugzilla::Field({ name => $new_field_name });
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
=head2 Data Validation
|
|
|
|
|
|
|
|
=over
|
|
|
|
|
|
|
|
=item C<check_field($name, $value, \@legal_values, $no_warn)>
|
|
|
|
|
|
|
|
Description: Makes sure the field $name is defined and its $value
|
|
|
|
is non empty. If @legal_values is defined, this routine
|
|
|
|
checks whether its value is one of the legal values
|
|
|
|
associated with this field, else it checks against
|
|
|
|
the default valid values for this field obtained by
|
|
|
|
C<get_legal_field_values($name)>. If the test is successful,
|
|
|
|
the function returns 1. If the test fails, an error
|
|
|
|
is thrown (by default), unless $no_warn is true, in which
|
|
|
|
case the function returns 0.
|
|
|
|
|
|
|
|
Params: $name - the field name
|
|
|
|
$value - the field value
|
|
|
|
@legal_values - (optional) list of legal values
|
|
|
|
$no_warn - (optional) do not throw an error if true
|
2010-03-15 20:52:24 +03:00
|
|
|
\%args - (optional) additional template variables for error message
|
2008-12-15 15:53:33 +03:00
|
|
|
|
|
|
|
Returns: 1 on success; 0 on failure if $no_warn is true (else an
|
|
|
|
error is thrown).
|
|
|
|
|
|
|
|
=back
|
|
|
|
|
|
|
|
=cut
|
|
|
|
|
|
|
|
sub check_field {
|
2010-03-15 20:52:24 +03:00
|
|
|
my ($name, $value, $legalsRef, $no_warn, $args) = @_;
|
2008-12-15 15:53:33 +03:00
|
|
|
my $dbh = Bugzilla->dbh;
|
|
|
|
|
|
|
|
# If $legalsRef is undefined, we use the default valid values.
|
2010-09-30 15:27:44 +04:00
|
|
|
# Valid values for this check are all possible values.
|
2010-05-15 00:02:34 +04:00
|
|
|
# Using get_legal_values would only return active values, but since
|
2010-09-30 15:27:44 +04:00
|
|
|
# some bugs may have inactive values set, we want to check them too.
|
2008-12-15 15:53:33 +03:00
|
|
|
unless (defined $legalsRef) {
|
2010-10-14 14:31:30 +04:00
|
|
|
$legalsRef = Bugzilla->get_field($name)->legal_values;
|
2010-05-15 00:02:34 +04:00
|
|
|
my @values = map($_->name, @$legalsRef);
|
|
|
|
$legalsRef = \@values;
|
|
|
|
|
2008-12-15 15:53:33 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!defined($value)
|
|
|
|
|| trim($value) eq ""
|
2011-09-05 17:46:57 +04:00
|
|
|
|| !grep { $_ eq $value } @$legalsRef)
|
2008-12-15 15:53:33 +03:00
|
|
|
{
|
|
|
|
return 0 if $no_warn; # We don't want an error to be thrown; return.
|
|
|
|
trick_taint($name);
|
|
|
|
|
2010-10-14 14:31:30 +04:00
|
|
|
my $field = Bugzilla->get_field($name);
|
2008-12-15 15:53:33 +03:00
|
|
|
my $field_desc = $field ? $field->description : $name;
|
2010-03-15 20:52:24 +03:00
|
|
|
ThrowUserError('illegal_field', { field => $field_desc, value => $value, legals => $legalsRef, ($args ? %$args : ()) });
|
2008-12-15 15:53:33 +03:00
|
|
|
}
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
=pod
|
|
|
|
|
|
|
|
=over
|
|
|
|
|
|
|
|
=item C<get_field_id($fieldname)>
|
|
|
|
|
|
|
|
Description: Returns the ID of the specified field name and throws
|
|
|
|
an error if this field does not exist.
|
|
|
|
|
|
|
|
Params: $name - a field name
|
|
|
|
|
|
|
|
Returns: the corresponding field ID or an error if the field name
|
|
|
|
does not exist.
|
|
|
|
|
|
|
|
=back
|
|
|
|
|
|
|
|
=cut
|
|
|
|
|
2010-10-27 19:53:21 +04:00
|
|
|
sub get_field_id
|
|
|
|
{
|
2008-12-15 15:53:33 +03:00
|
|
|
my ($name) = @_;
|
|
|
|
trick_taint($name);
|
2010-10-27 19:53:21 +04:00
|
|
|
my $field = Bugzilla->get_field($name);
|
|
|
|
ThrowCodeError('invalid_field_name', {field => $name}) unless $field;
|
|
|
|
return $field->id;
|
2008-12-15 15:53:33 +03:00
|
|
|
}
|
|
|
|
|
2010-03-12 22:18:39 +03:00
|
|
|
# Shared between Bugzilla::Field and Bugzilla::Field::Choice
|
|
|
|
sub update_visibility_values
|
|
|
|
{
|
2010-11-10 21:21:19 +03:00
|
|
|
my ($controlled_field, $controlled_value_id, $visibility_value_ids) = @_;
|
2010-11-11 16:36:23 +03:00
|
|
|
$visibility_value_ids ||= [];
|
2010-11-10 21:21:19 +03:00
|
|
|
my $vis_field = $controlled_value_id
|
|
|
|
? $controlled_field->value_field
|
|
|
|
: $controlled_field->visibility_field;
|
|
|
|
if (!$vis_field)
|
|
|
|
{
|
|
|
|
return undef;
|
|
|
|
}
|
|
|
|
$controlled_field = Bugzilla->get_field($controlled_field) if !ref $controlled_field;
|
|
|
|
$controlled_value_id = int($controlled_value_id);
|
|
|
|
if (@$visibility_value_ids)
|
|
|
|
{
|
|
|
|
my $type = Bugzilla::Field::Choice->type($vis_field);
|
|
|
|
$visibility_value_ids = [ map { $_->id } @{ $type->new_from_list($visibility_value_ids) } ];
|
|
|
|
}
|
2010-03-12 22:18:39 +03:00
|
|
|
Bugzilla->dbh->do(
|
|
|
|
"DELETE FROM fieldvaluecontrol WHERE field_id=? AND value_id=?",
|
2010-11-10 21:21:19 +03:00
|
|
|
undef, $controlled_field->id, $controlled_value_id);
|
|
|
|
if (@$visibility_value_ids)
|
2010-03-12 22:18:39 +03:00
|
|
|
{
|
2010-11-10 21:21:19 +03:00
|
|
|
my $f = $controlled_field->id;
|
2010-03-12 22:18:39 +03:00
|
|
|
Bugzilla->dbh->do(
|
|
|
|
"INSERT INTO fieldvaluecontrol (field_id, value_id, visibility_value_id) VALUES ".
|
2010-11-10 21:21:19 +03:00
|
|
|
join(",", map { "($f, $controlled_value_id, $_)" } @$visibility_value_ids)
|
|
|
|
);
|
2010-03-12 22:18:39 +03:00
|
|
|
}
|
2010-12-09 20:16:43 +03:00
|
|
|
# Touch the field
|
2010-12-10 02:53:33 +03:00
|
|
|
$controlled_field->touch;
|
2010-11-10 21:21:19 +03:00
|
|
|
return 1;
|
2010-03-12 22:18:39 +03:00
|
|
|
}
|
|
|
|
|
2013-07-01 19:14:37 +04:00
|
|
|
sub update_controlled_values
|
2013-07-01 18:42:45 +04:00
|
|
|
{
|
2013-07-03 18:39:59 +04:00
|
|
|
my ($controlled_field, $controlled_value_ids, $visibility_value_id, $default_value_ids) = @_;
|
2013-07-01 18:42:45 +04:00
|
|
|
$controlled_value_ids ||= [];
|
|
|
|
my $vis_field = $controlled_value_ids
|
|
|
|
? $controlled_field->value_field
|
|
|
|
: $controlled_field->visibility_field;
|
|
|
|
if (!$vis_field)
|
|
|
|
{
|
|
|
|
return undef;
|
|
|
|
}
|
|
|
|
$controlled_field = Bugzilla->get_field($controlled_field) if !ref $controlled_field;
|
|
|
|
$visibility_value_id = int($visibility_value_id);
|
|
|
|
if ($visibility_value_id)
|
|
|
|
{
|
|
|
|
my $type = Bugzilla::Field::Choice->type($vis_field);
|
|
|
|
$visibility_value_id = $type->new($visibility_value_id)->{'id'};
|
|
|
|
}
|
|
|
|
Bugzilla->dbh->do(
|
|
|
|
"DELETE FROM fieldvaluecontrol WHERE field_id=? AND visibility_value_id=?",
|
|
|
|
undef, $controlled_field->id, $visibility_value_id);
|
|
|
|
if (@$controlled_value_ids)
|
|
|
|
{
|
|
|
|
my $type = Bugzilla::Field::Choice->type($controlled_field);
|
|
|
|
$controlled_value_ids = [ map { $_->id } @{ $type->new_from_list($controlled_value_ids) } ];
|
2013-07-03 18:39:59 +04:00
|
|
|
if ($default_value_ids)
|
|
|
|
{
|
|
|
|
my $type = Bugzilla::Field::Choice->type($controlled_field);
|
|
|
|
$default_value_ids = { map { $_->id => 1 } @{ $type->new_from_list($default_value_ids) } };
|
|
|
|
}
|
2013-07-01 18:42:45 +04:00
|
|
|
my $f = $controlled_field->id;
|
2013-07-03 18:39:59 +04:00
|
|
|
my $sql = "INSERT INTO fieldvaluecontrol (field_id, visibility_value_id, value_id, is_default) VALUES ".
|
|
|
|
join(",", map { "($f, $visibility_value_id, $_, " . ($default_value_ids->{$_} ? '1' : '0') . ')' } @$controlled_value_ids);
|
2013-07-01 18:42:45 +04:00
|
|
|
Bugzilla->dbh->do($sql);
|
|
|
|
}
|
|
|
|
# Touch the field
|
|
|
|
$controlled_field->touch;
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2010-03-12 22:18:39 +03:00
|
|
|
# Moved from bug/field-events.js.tmpl
|
2010-10-27 19:53:21 +04:00
|
|
|
# Now uses one pass over cached fieldvaluecontrol table
|
2010-03-12 22:18:39 +03:00
|
|
|
sub json_visibility
|
|
|
|
{
|
|
|
|
my $self = shift;
|
2010-11-10 21:21:19 +03:00
|
|
|
my $data = {
|
2010-10-27 19:53:21 +04:00
|
|
|
legal => [ map { [ $_->id, $_->name ] } @{$self->legal_values} ],
|
|
|
|
fields => {},
|
|
|
|
values => {},
|
|
|
|
};
|
|
|
|
my $hash = Bugzilla->fieldvaluecontrol_hash->{$self->id};
|
2010-11-10 21:21:19 +03:00
|
|
|
$data->{fields} = { map { Bugzilla->get_field($_)->name => $hash->{fields}->{$_} } keys %{$hash->{fields}} };
|
|
|
|
$data->{values} = { map { Bugzilla->get_field($_)->name => $hash->{values}->{$_} } keys %{$hash->{values}} };
|
|
|
|
return $data;
|
2010-03-12 22:18:39 +03:00
|
|
|
}
|
|
|
|
|
2010-11-10 21:21:19 +03:00
|
|
|
1;
|
|
|
|
__END__
|