Compare commits
1 Commits
tags/BUGZI
...
src
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
258dc9fead |
@@ -1,454 +0,0 @@
|
||||
# -*- 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): Bradley Baetz <bbaetz@student.usyd.edu.au>
|
||||
# Erik Stambaugh <erik@dasbistro.com>
|
||||
# A. Karl Kornel <karl@kornel.name>
|
||||
|
||||
package Bugzilla;
|
||||
|
||||
use strict;
|
||||
|
||||
use Bugzilla::Auth;
|
||||
use Bugzilla::Auth::Login::WWW;
|
||||
use Bugzilla::CGI;
|
||||
use Bugzilla::Config;
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::DB;
|
||||
use Bugzilla::Template;
|
||||
use Bugzilla::User;
|
||||
use Bugzilla::Error;
|
||||
use Bugzilla::Util;
|
||||
|
||||
use File::Basename;
|
||||
|
||||
#####################################################################
|
||||
# Constants
|
||||
#####################################################################
|
||||
|
||||
# Scripts that are not stopped by shutdownhtml being in effect.
|
||||
use constant SHUTDOWNHTML_EXEMPT => [
|
||||
'editparams.cgi',
|
||||
'checksetup.pl',
|
||||
];
|
||||
|
||||
# Non-cgi scripts that should silently exit.
|
||||
use constant SHUTDOWNHTML_EXIT_SILENTLY => [
|
||||
'whine.pl'
|
||||
];
|
||||
|
||||
#####################################################################
|
||||
# Global Code
|
||||
#####################################################################
|
||||
|
||||
# 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
|
||||
&& Param("shutdownhtml")
|
||||
&& lsearch(SHUTDOWNHTML_EXEMPT, basename($0)) == -1)
|
||||
{
|
||||
# Allow non-cgi scripts to exit silently (without displaying any
|
||||
# message), if desired. At this point, no DBI call has been made
|
||||
# yet, and no error will be returned if the DB is inaccessible.
|
||||
if (lsearch(SHUTDOWNHTML_EXIT_SILENTLY, basename($0)) > -1
|
||||
&& !i_am_cgi())
|
||||
{
|
||||
exit;
|
||||
}
|
||||
|
||||
# For security reasons, log out users when Bugzilla is down.
|
||||
# Bugzilla->login() is required to catch the logincookie, if any.
|
||||
my $user = Bugzilla->login(LOGIN_OPTIONAL);
|
||||
my $userid = $user->id;
|
||||
Bugzilla->logout();
|
||||
|
||||
my $template = Bugzilla->template;
|
||||
my $vars = {};
|
||||
$vars->{'message'} = 'shutdown';
|
||||
$vars->{'userid'} = $userid;
|
||||
# Generate and return a message about the downtime, appropriately
|
||||
# for if we're a command-line script or a CGI script.
|
||||
my $extension;
|
||||
if (i_am_cgi() && (!Bugzilla->cgi->param('ctype')
|
||||
|| Bugzilla->cgi->param('ctype') eq 'html')) {
|
||||
$extension = 'html';
|
||||
}
|
||||
else {
|
||||
$extension = 'txt';
|
||||
}
|
||||
print Bugzilla->cgi->header() if i_am_cgi();
|
||||
my $t_output;
|
||||
$template->process("global/message.$extension.tmpl", $vars, \$t_output)
|
||||
|| ThrowTemplateError($template->error);
|
||||
print $t_output . "\n";
|
||||
exit;
|
||||
}
|
||||
|
||||
#####################################################################
|
||||
# Subroutines and Methods
|
||||
#####################################################################
|
||||
|
||||
my $_template;
|
||||
sub template {
|
||||
my $class = shift;
|
||||
$_template ||= Bugzilla::Template->create();
|
||||
return $_template;
|
||||
}
|
||||
|
||||
my $_cgi;
|
||||
sub cgi {
|
||||
my $class = shift;
|
||||
$_cgi ||= new Bugzilla::CGI();
|
||||
return $_cgi;
|
||||
}
|
||||
|
||||
my $_user;
|
||||
sub user {
|
||||
my $class = shift;
|
||||
|
||||
if (not defined $_user) {
|
||||
$_user = new Bugzilla::User;
|
||||
}
|
||||
|
||||
return $_user;
|
||||
}
|
||||
|
||||
my $_sudoer;
|
||||
sub sudoer {
|
||||
my $class = shift;
|
||||
return $_sudoer;
|
||||
}
|
||||
|
||||
sub sudo_request {
|
||||
my $class = shift;
|
||||
my $new_user = shift;
|
||||
my $new_sudoer = shift;
|
||||
|
||||
$_user = $new_user;
|
||||
$_sudoer = $new_sudoer;
|
||||
|
||||
# NOTE: If you want to log the start of an sudo session, do it here.
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
sub login {
|
||||
my ($class, $type) = @_;
|
||||
my $authenticated_user = Bugzilla::Auth::Login::WWW->login($type);
|
||||
|
||||
# At this point, we now know if a real person is logged in.
|
||||
# We must now check to see if an sudo session is in progress.
|
||||
# For a session to be in progress, the following must be true:
|
||||
# 1: There must be a logged in user
|
||||
# 2: That user must be in the 'bz_sudoer' group
|
||||
# 3: There must be a valid value in the 'sudo' cookie
|
||||
# 4: A Bugzilla::User object must exist for the given cookie value
|
||||
# 5: That user must NOT be in the 'bz_sudo_protect' group
|
||||
my $sudo_cookie = $class->cgi->cookie('sudo');
|
||||
detaint_natural($sudo_cookie) if defined($sudo_cookie);
|
||||
my $sudo_target;
|
||||
$sudo_target = new Bugzilla::User($sudo_cookie) if defined($sudo_cookie);
|
||||
if (defined($authenticated_user) &&
|
||||
$authenticated_user->in_group('bz_sudoers') &&
|
||||
defined($sudo_cookie) &&
|
||||
defined($sudo_target) &&
|
||||
!($sudo_target->in_group('bz_sudo_protect'))
|
||||
)
|
||||
{
|
||||
$_user = $sudo_target;
|
||||
$_sudoer = $authenticated_user;
|
||||
|
||||
# NOTE: If you want to do any special logging, do it here.
|
||||
}
|
||||
else {
|
||||
$_user = $authenticated_user;
|
||||
}
|
||||
|
||||
return $_user;
|
||||
}
|
||||
|
||||
sub logout {
|
||||
my ($class, $option) = @_;
|
||||
|
||||
# If we're not logged in, go away
|
||||
return unless user->id;
|
||||
|
||||
$option = LOGOUT_CURRENT unless defined $option;
|
||||
Bugzilla::Auth::Login::WWW->logout($_user, $option);
|
||||
}
|
||||
|
||||
sub logout_user {
|
||||
my ($class, $user) = @_;
|
||||
# When we're logging out another user we leave cookies alone, and
|
||||
# therefore avoid calling Bugzilla->logout() directly.
|
||||
Bugzilla::Auth::Login::WWW->logout($user, LOGOUT_ALL);
|
||||
}
|
||||
|
||||
# just a compatibility front-end to logout_user that gets a user by id
|
||||
sub logout_user_by_id {
|
||||
my ($class, $id) = @_;
|
||||
my $user = new Bugzilla::User($id);
|
||||
$class->logout_user($user);
|
||||
}
|
||||
|
||||
# hack that invalidates credentials for a single request
|
||||
sub logout_request {
|
||||
undef $_user;
|
||||
undef $_sudoer;
|
||||
# We can't delete from $cgi->cookie, so logincookie data will remain
|
||||
# there. Don't rely on it: use Bugzilla->user->login instead!
|
||||
}
|
||||
|
||||
my $_dbh;
|
||||
my $_dbh_main;
|
||||
my $_dbh_shadow;
|
||||
sub dbh {
|
||||
my $class = shift;
|
||||
|
||||
# If we're not connected, then we must want the main db
|
||||
if (!$_dbh) {
|
||||
$_dbh = $_dbh_main = Bugzilla::DB::connect_main();
|
||||
}
|
||||
|
||||
return $_dbh;
|
||||
}
|
||||
|
||||
my $_batch;
|
||||
sub batch {
|
||||
my $class = shift;
|
||||
my $newval = shift;
|
||||
if (defined $newval) {
|
||||
$_batch = $newval;
|
||||
}
|
||||
return $_batch || 0;
|
||||
}
|
||||
|
||||
sub switch_to_shadow_db {
|
||||
my $class = shift;
|
||||
|
||||
if (!$_dbh_shadow) {
|
||||
if (Param('shadowdb')) {
|
||||
$_dbh_shadow = Bugzilla::DB::connect_shadow();
|
||||
} else {
|
||||
$_dbh_shadow = $_dbh_main;
|
||||
}
|
||||
}
|
||||
|
||||
$_dbh = $_dbh_shadow;
|
||||
# we have to return $class->dbh instead of $_dbh as
|
||||
# $_dbh_shadow may be undefined if no shadow DB is used
|
||||
# and no connection to the main DB has been established yet.
|
||||
return $class->dbh;
|
||||
}
|
||||
|
||||
sub switch_to_main_db {
|
||||
my $class = shift;
|
||||
|
||||
$_dbh = $_dbh_main;
|
||||
# We have to return $class->dbh instead of $_dbh as
|
||||
# $_dbh_main may be undefined if no connection to the main DB
|
||||
# has been established yet.
|
||||
return $class->dbh;
|
||||
}
|
||||
|
||||
# Private methods
|
||||
|
||||
# Per process cleanup
|
||||
sub _cleanup {
|
||||
undef $_cgi;
|
||||
undef $_user;
|
||||
|
||||
# See bug 192531. If we don't clear the possibly active statement handles,
|
||||
# then when this is called from the END block, it happens _before_ the
|
||||
# destructors in Bugzilla::DB have happened.
|
||||
# See http://rt.perl.org/rt2/Ticket/Display.html?id=17450#38810
|
||||
# Without disconnecting explicitly here, noone notices, because DBI::END
|
||||
# ends up calling DBD::mysql's $drh->disconnect_all, which is a noop.
|
||||
# This code is evil, but it needs to be done, at least until SendSQL and
|
||||
# friends can be removed
|
||||
@Bugzilla::DB::SQLStateStack = ();
|
||||
undef $Bugzilla::DB::_current_sth;
|
||||
|
||||
# When we support transactions, need to ->rollback here
|
||||
$_dbh_main->disconnect if $_dbh_main;
|
||||
$_dbh_shadow->disconnect if $_dbh_shadow and Param("shadowdb");
|
||||
undef $_dbh_main;
|
||||
undef $_dbh_shadow;
|
||||
undef $_dbh;
|
||||
}
|
||||
|
||||
sub END {
|
||||
_cleanup();
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla - Semi-persistent collection of various objects used by scripts
|
||||
and modules
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
use Bugzilla;
|
||||
|
||||
sub someModulesSub {
|
||||
Bugzilla->dbh->prepare(...);
|
||||
Bugzilla->template->process(...);
|
||||
}
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
Several Bugzilla 'things' are used by a variety of modules and scripts. This
|
||||
includes database handles, template objects, and so on.
|
||||
|
||||
This module is a singleton intended as a central place to store these objects.
|
||||
This approach has several advantages:
|
||||
|
||||
=over 4
|
||||
|
||||
=item *
|
||||
|
||||
They're not global variables, so we don't have issues with them staying around
|
||||
with mod_perl
|
||||
|
||||
=item *
|
||||
|
||||
Everything is in one central place, so its easy to access, modify, and maintain
|
||||
|
||||
=item *
|
||||
|
||||
Code in modules can get access to these objects without having to have them
|
||||
all passed from the caller, and the caller's caller, and....
|
||||
|
||||
=item *
|
||||
|
||||
We can reuse objects across requests using mod_perl where appropriate (eg
|
||||
templates), whilst destroying those which are only valid for a single request
|
||||
(such as the current user)
|
||||
|
||||
=back
|
||||
|
||||
Note that items accessible via this object are demand-loaded when requested.
|
||||
|
||||
For something to be added to this object, it should either be able to benefit
|
||||
from persistence when run under mod_perl (such as the a C<template> object),
|
||||
or should be something which is globally required by a large ammount of code
|
||||
(such as the current C<user> object).
|
||||
|
||||
=head1 METHODS
|
||||
|
||||
Note that all C<Bugzilla> functionality is method based; use C<Bugzilla-E<gt>dbh>
|
||||
rather than C<Bugzilla::dbh>. Nothing cares about this now, but don't rely on
|
||||
that.
|
||||
|
||||
=over 4
|
||||
|
||||
=item C<template>
|
||||
|
||||
The current C<Template> object, to be used for output
|
||||
|
||||
=item C<cgi>
|
||||
|
||||
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<user>
|
||||
|
||||
C<undef> if there is no currently logged in user or if the login code has not
|
||||
yet been run. If an sudo session is in progress, the C<Bugzilla::User>
|
||||
corresponding to the person who is being impersonated. If no session is in
|
||||
progress, the current C<Bugzilla::User>.
|
||||
|
||||
=item C<sudoer>
|
||||
|
||||
C<undef> if there is no currently logged in user, the currently logged in user
|
||||
is not in the I<sudoer> group, or there is no session in progress. If an sudo
|
||||
session is in progress, returns the C<Bugzilla::User> object corresponding to
|
||||
the person who logged in and initiated the session. If no session is in
|
||||
progress, returns the C<Bugzilla::User> object corresponding to the currently
|
||||
logged in user.
|
||||
|
||||
=item C<sudo_request>
|
||||
This begins an sudo session for the current request. It is meant to be
|
||||
used when a session has just started. For normal use, sudo access should
|
||||
normally be set at login time.
|
||||
|
||||
=item C<login>
|
||||
|
||||
Logs in a user, returning a C<Bugzilla::User> object, or C<undef> if there is
|
||||
no logged in user. See L<Bugzilla::Auth|Bugzilla::Auth>, and
|
||||
L<Bugzilla::User|Bugzilla::User>.
|
||||
|
||||
=item C<logout($option)>
|
||||
|
||||
Logs out the current user, which involves invalidating user sessions and
|
||||
cookies. Three options are available from
|
||||
L<Bugzilla::Constants|Bugzilla::Constants>: LOGOUT_CURRENT (the
|
||||
default), LOGOUT_ALL or LOGOUT_KEEP_CURRENT.
|
||||
|
||||
=item C<logout_user($user)>
|
||||
|
||||
Logs out the specified user (invalidating all his sessions), taking a
|
||||
Bugzilla::User instance.
|
||||
|
||||
=item C<logout_by_id($id)>
|
||||
|
||||
Logs out the user with the id specified. This is a compatibility
|
||||
function to be used in callsites where there is only a userid and no
|
||||
Bugzilla::User instance.
|
||||
|
||||
=item C<logout_request>
|
||||
|
||||
Essentially, causes calls to C<Bugzilla-E<gt>user> to return C<undef>. This has the
|
||||
effect of logging out a user for the current request only; cookies and
|
||||
database sessions are left intact.
|
||||
|
||||
=item C<batch>
|
||||
|
||||
Set to true, by calling Bugzilla->batch(1), to indicate that Bugzilla is
|
||||
being called in a non-interactive manner and errors should be passed to
|
||||
die() rather than being sent to a browser and finished with an exit().
|
||||
Bugzilla->batch will return the current state of this flag.
|
||||
|
||||
=item C<dbh>
|
||||
|
||||
The current database handle. See L<DBI>.
|
||||
|
||||
=item C<switch_to_shadow_db>
|
||||
|
||||
Switch from using the main database to using the shadow database.
|
||||
|
||||
=item C<switch_to_main_db>
|
||||
|
||||
Change the database object to refer to the main database.
|
||||
|
||||
=back
|
||||
@@ -1,433 +0,0 @@
|
||||
# -*- 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>
|
||||
# Myk Melez <myk@mozilla.org>
|
||||
|
||||
use strict;
|
||||
|
||||
package Bugzilla::Attachment;
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::Attachment - a file related to a bug that a user has uploaded
|
||||
to the Bugzilla server
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
use Bugzilla::Attachment;
|
||||
|
||||
# Get the attachment with the given ID.
|
||||
my $attachment = Bugzilla::Attachment->get($attach_id);
|
||||
|
||||
# Get the attachments with the given IDs.
|
||||
my $attachments = Bugzilla::Attachment->get_list($attach_ids);
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
This module defines attachment objects, which represent files related to bugs
|
||||
that users upload to the Bugzilla server.
|
||||
|
||||
=cut
|
||||
|
||||
# This module requires that its caller have said "require globals.pl"
|
||||
# to import relevant functions from that script.
|
||||
|
||||
use Bugzilla::Flag;
|
||||
use Bugzilla::Config qw(:locations);
|
||||
use Bugzilla::User;
|
||||
|
||||
sub get {
|
||||
my $invocant = shift;
|
||||
my $id = shift;
|
||||
|
||||
my $attachments = _retrieve([$id]);
|
||||
my $self = $attachments->[0];
|
||||
bless($self, ref($invocant) || $invocant) if $self;
|
||||
|
||||
return $self;
|
||||
}
|
||||
|
||||
sub get_list {
|
||||
my $invocant = shift;
|
||||
my $ids = shift;
|
||||
|
||||
my $attachments = _retrieve($ids);
|
||||
foreach my $attachment (@$attachments) {
|
||||
bless($attachment, ref($invocant) || $invocant);
|
||||
}
|
||||
|
||||
return $attachments;
|
||||
}
|
||||
|
||||
sub _retrieve {
|
||||
my ($ids) = @_;
|
||||
|
||||
return [] if scalar(@$ids) == 0;
|
||||
|
||||
my @columns = (
|
||||
'attachments.attach_id AS id',
|
||||
'attachments.bug_id AS bug_id',
|
||||
'attachments.description AS description',
|
||||
'attachments.mimetype AS contenttype',
|
||||
'attachments.submitter_id AS _attacher_id',
|
||||
Bugzilla->dbh->sql_date_format('attachments.creation_ts',
|
||||
'%Y.%m.%d %H:%i') . " AS attached",
|
||||
'attachments.filename AS filename',
|
||||
'attachments.ispatch AS ispatch',
|
||||
'attachments.isurl AS isurl',
|
||||
'attachments.isobsolete AS isobsolete',
|
||||
'attachments.isprivate AS isprivate'
|
||||
);
|
||||
my $columns = join(", ", @columns);
|
||||
|
||||
my $records = Bugzilla->dbh->selectall_arrayref("SELECT $columns
|
||||
FROM attachments
|
||||
WHERE attach_id IN (" .
|
||||
join(",", @$ids) . ")
|
||||
ORDER BY attach_id",
|
||||
{ Slice => {} });
|
||||
return $records;
|
||||
}
|
||||
|
||||
=pod
|
||||
|
||||
=head2 Instance Properties
|
||||
|
||||
=over
|
||||
|
||||
=item C<id>
|
||||
|
||||
the unique identifier for the attachment
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
sub id {
|
||||
my $self = shift;
|
||||
return $self->{id};
|
||||
}
|
||||
|
||||
=over
|
||||
|
||||
=item C<bug_id>
|
||||
|
||||
the ID of the bug to which the attachment is attached
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
# XXX Once Bug.pm slims down sufficiently this should become a reference
|
||||
# to a bug object.
|
||||
sub bug_id {
|
||||
my $self = shift;
|
||||
return $self->{bug_id};
|
||||
}
|
||||
|
||||
=over
|
||||
|
||||
=item C<description>
|
||||
|
||||
user-provided text describing the attachment
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
sub description {
|
||||
my $self = shift;
|
||||
return $self->{description};
|
||||
}
|
||||
|
||||
=over
|
||||
|
||||
=item C<contenttype>
|
||||
|
||||
the attachment's MIME media type
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
sub contenttype {
|
||||
my $self = shift;
|
||||
return $self->{contenttype};
|
||||
}
|
||||
|
||||
=over
|
||||
|
||||
=item C<attacher>
|
||||
|
||||
the user who attached the attachment
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
sub attacher {
|
||||
my $self = shift;
|
||||
return $self->{attacher} if exists $self->{attacher};
|
||||
$self->{attacher} = new Bugzilla::User($self->{_attacher_id});
|
||||
return $self->{attacher};
|
||||
}
|
||||
|
||||
=over
|
||||
|
||||
=item C<attached>
|
||||
|
||||
the date and time on which the attacher attached the attachment
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
sub attached {
|
||||
my $self = shift;
|
||||
return $self->{attached};
|
||||
}
|
||||
|
||||
=over
|
||||
|
||||
=item C<filename>
|
||||
|
||||
the name of the file the attacher attached
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
sub filename {
|
||||
my $self = shift;
|
||||
return $self->{filename};
|
||||
}
|
||||
|
||||
=over
|
||||
|
||||
=item C<ispatch>
|
||||
|
||||
whether or not the attachment is a patch
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
sub ispatch {
|
||||
my $self = shift;
|
||||
return $self->{ispatch};
|
||||
}
|
||||
|
||||
=over
|
||||
|
||||
=item C<isurl>
|
||||
|
||||
whether or not the attachment is a URL
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
sub isurl {
|
||||
my $self = shift;
|
||||
return $self->{isurl};
|
||||
}
|
||||
|
||||
=over
|
||||
|
||||
=item C<isobsolete>
|
||||
|
||||
whether or not the attachment is obsolete
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
sub isobsolete {
|
||||
my $self = shift;
|
||||
return $self->{isobsolete};
|
||||
}
|
||||
|
||||
=over
|
||||
|
||||
=item C<isprivate>
|
||||
|
||||
whether or not the attachment is private
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
sub isprivate {
|
||||
my $self = shift;
|
||||
return $self->{isprivate};
|
||||
}
|
||||
|
||||
=over
|
||||
|
||||
=item C<data>
|
||||
|
||||
the content of the attachment
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
sub data {
|
||||
my $self = shift;
|
||||
return $self->{data} if exists $self->{data};
|
||||
|
||||
# First try to get the attachment data from the database.
|
||||
($self->{data}) = Bugzilla->dbh->selectrow_array("SELECT thedata
|
||||
FROM attach_data
|
||||
WHERE id = ?",
|
||||
undef,
|
||||
$self->{id});
|
||||
|
||||
# If there's no attachment data in the database, the attachment is stored
|
||||
# in a local file, so retrieve it from there.
|
||||
if (length($self->{data}) == 0) {
|
||||
if (open(AH, $self->_get_local_filename())) {
|
||||
local $/;
|
||||
binmode AH;
|
||||
$self->{data} = <AH>;
|
||||
close(AH);
|
||||
}
|
||||
}
|
||||
|
||||
return $self->{data};
|
||||
}
|
||||
|
||||
=over
|
||||
|
||||
=item C<datasize>
|
||||
|
||||
the length (in characters) of the attachment content
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
# datasize is a property of the data itself, and it's unclear whether we should
|
||||
# expose it at all, since you can easily derive it from the data itself: in TT,
|
||||
# attachment.data.size; in Perl, length($attachment->{data}). But perhaps
|
||||
# it makes sense for performance reasons, since accessing the data forces it
|
||||
# to get retrieved from the database/filesystem and loaded into memory,
|
||||
# while datasize avoids loading the attachment into memory, calling SQL's
|
||||
# LENGTH() function or stat()ing the file instead. I've left it in for now.
|
||||
|
||||
sub datasize {
|
||||
my $self = shift;
|
||||
return $self->{datasize} if exists $self->{datasize};
|
||||
|
||||
# If we have already retrieved the data, return its size.
|
||||
return length($self->{data}) if exists $self->{data};
|
||||
|
||||
($self->{datasize}) =
|
||||
Bugzilla->dbh->selectrow_array("SELECT LENGTH(thedata)
|
||||
FROM attach_data
|
||||
WHERE id = ?",
|
||||
undef,
|
||||
$self->{id});
|
||||
|
||||
# If there's no attachment data in the database, the attachment
|
||||
# is stored in a local file, so retrieve its size from the file.
|
||||
if ($self->{datasize} == 0) {
|
||||
if (open(AH, $self->_get_local_filename())) {
|
||||
binmode AH;
|
||||
$self->{datasize} = (stat(AH))[7];
|
||||
close(AH);
|
||||
}
|
||||
}
|
||||
|
||||
return $self->{datasize};
|
||||
}
|
||||
|
||||
=over
|
||||
|
||||
=item C<flags>
|
||||
|
||||
flags that have been set on the attachment
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
sub flags {
|
||||
my $self = shift;
|
||||
return $self->{flags} if exists $self->{flags};
|
||||
|
||||
$self->{flags} = Bugzilla::Flag::match({ attach_id => $self->id,
|
||||
is_active => 1 });
|
||||
return $self->{flags};
|
||||
}
|
||||
|
||||
# Instance methods; no POD documentation here yet because the only one so far
|
||||
# is private.
|
||||
|
||||
sub _get_local_filename {
|
||||
my $self = shift;
|
||||
my $hash = ($self->id % 100) + 100;
|
||||
$hash =~ s/.*(\d\d)$/group.$1/;
|
||||
return "$attachdir/$hash/attachment." . $self->id;
|
||||
}
|
||||
|
||||
=pod
|
||||
|
||||
=head2 Class Methods
|
||||
|
||||
=over
|
||||
|
||||
=item C<get_attachments_by_bug($bug_id)>
|
||||
|
||||
Description: retrieves and returns the attachments the currently logged in
|
||||
user can view for the given bug.
|
||||
|
||||
Params: C<$bug_id> - integer - the ID of the bug for which
|
||||
to retrieve and return attachments.
|
||||
|
||||
Returns: a reference to an array of attachment objects.
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
sub get_attachments_by_bug {
|
||||
my ($class, $bug_id) = @_;
|
||||
my $user = Bugzilla->user;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
# By default, private attachments are not accessible, unless the user
|
||||
# is in the insider group or submitted the attachment.
|
||||
my $and_restriction = '';
|
||||
my @values = ($bug_id);
|
||||
|
||||
unless ($user->is_insider) {
|
||||
$and_restriction = 'AND (isprivate = 0 OR submitter_id = ?)';
|
||||
push(@values, $user->id);
|
||||
}
|
||||
|
||||
my $attach_ids = $dbh->selectcol_arrayref("SELECT attach_id FROM attachments
|
||||
WHERE bug_id = ? $and_restriction",
|
||||
undef, @values);
|
||||
my $attachments = Bugzilla::Attachment->get_list($attach_ids);
|
||||
return $attachments;
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -1,325 +0,0 @@
|
||||
# -*- 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): Bradley Baetz <bbaetz@acm.org>
|
||||
# Erik Stambaugh <erik@dasbistro.com>
|
||||
|
||||
package Bugzilla::Auth;
|
||||
|
||||
use strict;
|
||||
|
||||
use Bugzilla::Config;
|
||||
use Bugzilla::Constants;
|
||||
|
||||
# The verification method that was successfully used upon login, if any
|
||||
my $current_verify_class = undef;
|
||||
|
||||
# 'inherit' from the main verify method
|
||||
BEGIN {
|
||||
for my $verifyclass (split /,\s*/, Param("user_verify_class")) {
|
||||
if ($verifyclass =~ /^([A-Za-z0-9_\.\-]+)$/) {
|
||||
$verifyclass = $1;
|
||||
} else {
|
||||
die "Badly-named user_verify_class '$verifyclass'";
|
||||
}
|
||||
require "Bugzilla/Auth/Verify/" . $verifyclass . ".pm";
|
||||
}
|
||||
}
|
||||
|
||||
# PRIVATE
|
||||
|
||||
# A number of features, like password change requests, require the DB
|
||||
# verification method to be on the list.
|
||||
sub has_db {
|
||||
for (split (/[\s,]+/, Param("user_verify_class"))) {
|
||||
if (/^DB$/) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
# Returns the network address for a given IP
|
||||
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;
|
||||
}
|
||||
|
||||
my $addr = unpack("N", pack("CCCC", split(/\./, $ipaddr)));
|
||||
|
||||
my $maskbits = Param('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)));
|
||||
}
|
||||
|
||||
# This is a replacement for the inherited authenticate function
|
||||
# go through each of the available methods for each function
|
||||
sub authenticate {
|
||||
my $class = shift;
|
||||
my @args = @_;
|
||||
my @firstresult = ();
|
||||
my @result = ();
|
||||
my $current_verify_method;
|
||||
for my $method (split /,\s*/, Param("user_verify_class")) {
|
||||
$current_verify_method = $method;
|
||||
$method = "Bugzilla::Auth::Verify::" . $method;
|
||||
@result = $method->authenticate(@args);
|
||||
@firstresult = @result unless @firstresult;
|
||||
|
||||
if (($result[0] != AUTH_NODATA)&&($result[0] != AUTH_LOGINFAILED)) {
|
||||
unshift @result, ($current_verify_method);
|
||||
return @result;
|
||||
}
|
||||
}
|
||||
@result = @firstresult;
|
||||
# no auth match
|
||||
|
||||
# see if we can set $current to the first verify method that
|
||||
# will allow a new login
|
||||
|
||||
my $chosen_verify_method;
|
||||
for my $method (split /,\s*/, Param("user_verify_class")) {
|
||||
$current_verify_method = $method;
|
||||
$method = "Bugzilla::Auth::Verify::" . $method;
|
||||
if ($method->can_edit('new')) {
|
||||
$chosen_verify_method = $method;
|
||||
}
|
||||
}
|
||||
|
||||
unshift @result, $chosen_verify_method;
|
||||
return @result;
|
||||
}
|
||||
|
||||
sub can_edit {
|
||||
my ($class, $type) = @_;
|
||||
if ($current_verify_class) {
|
||||
return $current_verify_class->can_edit($type);
|
||||
}
|
||||
# $current_verify_class will not be set if the user isn't logged in. That
|
||||
# happens when the user is trying to create a new account, which (for now)
|
||||
# is hard-coded to work with DB.
|
||||
elsif (has_db) {
|
||||
return Bugzilla::Auth::Verify::DB->can_edit($type);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::Auth - Authentication handling for Bugzilla users
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
Handles authentication for Bugzilla users.
|
||||
|
||||
Authentication from Bugzilla involves two sets of modules. One set is
|
||||
used to obtain the data (from CGI, email, etc), and the other set uses
|
||||
this data to authenticate against the datasource (the Bugzilla DB, LDAP,
|
||||
cookies, etc).
|
||||
|
||||
Modules for obtaining the data are located under L<Bugzilla::Auth::Login>, and
|
||||
modules for authenticating are located in L<Bugzilla::Auth::Verify>.
|
||||
|
||||
=head1 METHODS
|
||||
|
||||
C<Bugzilla::Auth> contains several helper methods to be used by
|
||||
authentication or login modules.
|
||||
|
||||
=over 4
|
||||
|
||||
=item C<Bugzilla::Auth::get_netaddr($ipaddr)>
|
||||
|
||||
Given an ip address, this returns the associated network address, using
|
||||
C<Param('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.
|
||||
|
||||
=back
|
||||
|
||||
=head1 AUTHENTICATION
|
||||
|
||||
Authentication modules check a user's credentials (username, password,
|
||||
etc) to verify who the user is. The methods that C<Bugzilla::Auth> uses for
|
||||
authentication are wrappers that check all configured modules (via the
|
||||
C<Param('user_info_class')> and C<Param('user_verify_class')>) in sequence.
|
||||
|
||||
=head2 METHODS
|
||||
|
||||
=over 4
|
||||
|
||||
=item C<authenticate($username, $pass)>
|
||||
|
||||
This method is passed a username and a password, and returns a list
|
||||
containing up to four return values, depending on the results of the
|
||||
authentication.
|
||||
|
||||
The first return value is the name of the class that generated the results
|
||||
constined in the remaining return values. The second return value is one of
|
||||
the status codes defined in L<Bugzilla::Constants|Bugzilla::Constants> and
|
||||
described below. The rest of the return values are status code-specific
|
||||
and are explained in the status code descriptions.
|
||||
|
||||
=item C<AUTH_OK>
|
||||
|
||||
Authentication succeeded. The third variable is the userid of the new
|
||||
user.
|
||||
|
||||
=item C<AUTH_NODATA>
|
||||
|
||||
Insufficient login data was provided by the user. This may happen in several
|
||||
cases, such as cookie authentication when the cookie is not present.
|
||||
|
||||
=item C<AUTH_ERROR>
|
||||
|
||||
An error occurred when trying to use the login mechanism. The third return
|
||||
value may contain the Bugzilla userid, but will probably be C<undef>,
|
||||
signifiying that the userid is unknown. The fourth value is a tag describing
|
||||
the error used by the authentication error templates to print a description
|
||||
to the user. The optional fifth argument is a hashref of values used as part
|
||||
of the tag's error descriptions.
|
||||
|
||||
This error template must have a name/location of
|
||||
I<account/auth/C<lc(authentication-type)>-error.html.tmpl>.
|
||||
|
||||
=item C<AUTH_LOGINFAILED>
|
||||
|
||||
An incorrect username or password was given. Note that for security reasons,
|
||||
both cases return the same error code. However, in the case of a valid
|
||||
username, the third argument may be the userid. The authentication
|
||||
mechanism may not always be able to discover the userid if the password is
|
||||
not known, so whether or not this argument is present is implementation
|
||||
specific. For security reasons, the presence or lack of a userid value should
|
||||
not be communicated to the user.
|
||||
|
||||
The fourth argument is an optional tag from the authentication server
|
||||
describing the error. The tag can be used by a template to inform the user
|
||||
about the error. Similar to C<AUTH_ERROR>, an optional hashref may be
|
||||
present as a fifth argument, to be used by the tag to give more detailed
|
||||
information.
|
||||
|
||||
=item C<AUTH_DISABLED>
|
||||
|
||||
The user successfully logged in, but their account has been disabled.
|
||||
The third argument in the returned array is the userid, and the fourth
|
||||
is some text explaining why the account was disabled. This text would
|
||||
typically come from the C<disabledtext> field in the C<profiles> table.
|
||||
Note that this argument is a string, not a tag.
|
||||
|
||||
=item C<current_verify_class>
|
||||
|
||||
This scalar gets populated with the full name (eg.,
|
||||
C<Bugzilla::Auth::Verify::DB>) of the verification method being used by the
|
||||
current user. If no user is logged in, it will contain the name of the first
|
||||
method that allows new users, if any. Otherwise, it carries an undefined
|
||||
value.
|
||||
|
||||
=item C<can_edit>
|
||||
|
||||
This determines if the user's account details can be modified. It returns a
|
||||
reference to a hash with the keys C<userid>, C<login_name>, and C<realname>,
|
||||
which determine whether their respective profile values may be altered, and
|
||||
C<new>, which determines if new accounts may be created.
|
||||
|
||||
Each user verification method (chosen with C<Param('user_verify_class')> has
|
||||
its own set of can_edit values. Calls to can_edit return the appropriate
|
||||
values for the current user's login method.
|
||||
|
||||
If a user is not logged in, C<can_edit> will contain the values of the first
|
||||
verify method that allows new users to be created, if available. Otherwise it
|
||||
returns an empty hash.
|
||||
|
||||
=back
|
||||
|
||||
=head1 LOGINS
|
||||
|
||||
A login module can be used to try to log in a Bugzilla user in a
|
||||
particular way. For example,
|
||||
L<Bugzilla::Auth::Login::WWW::CGI|Bugzilla::Auth::Login::WWW::CGI>
|
||||
logs in users from CGI scripts, first by using form variables, and then
|
||||
by trying cookies as a fallback.
|
||||
|
||||
The login interface consists of the following methods:
|
||||
|
||||
=over 4
|
||||
|
||||
=item C<login>, which takes a C<$type> argument, using constants found in
|
||||
C<Bugzilla::Constants>.
|
||||
|
||||
The login method may use various authentication modules (described
|
||||
above) to try to authenticate a user, and should return the userid on
|
||||
success, or C<undef> on failure.
|
||||
|
||||
When a login is required, but data is not present, it is the job of the
|
||||
login method to prompt the user for this data.
|
||||
|
||||
The constants accepted by C<login> include the following:
|
||||
|
||||
=item C<LOGIN_OPTIONAL>
|
||||
|
||||
A login is never required to access this data. Attempting to login is
|
||||
still useful, because this allows the page to be personalised. Note that
|
||||
an incorrect login will still trigger an error, even though the lack of
|
||||
a login will be OK.
|
||||
|
||||
=item C<LOGIN_NORMAL>
|
||||
|
||||
A login may or may not be required, depending on the setting of the
|
||||
I<requirelogin> parameter.
|
||||
|
||||
=item C<LOGIN_REQUIRED>
|
||||
|
||||
A login is always required to access this data.
|
||||
|
||||
=item C<logout>, which takes a C<Bugzilla::User> argument for the user
|
||||
being logged out, and an C<$option> argument. Possible values for
|
||||
C<$option> include:
|
||||
|
||||
=item C<LOGOUT_CURRENT>
|
||||
|
||||
Log out the user and invalidate his currently registered session.
|
||||
|
||||
=item C<LOGOUT_ALL>
|
||||
|
||||
Log out the user, and invalidate all sessions the user has registered in
|
||||
Bugzilla.
|
||||
|
||||
=item C<LOGOUT_KEEP_CURRENT>
|
||||
|
||||
Invalidate all sessions the user has registered excluding his current
|
||||
session; this option should leave the user logged in. This is useful for
|
||||
user-performed password changes.
|
||||
|
||||
=back
|
||||
|
||||
=head1 SEE ALSO
|
||||
|
||||
L<Bugzilla::Auth::Login::WWW::CGI>, L<Bugzilla::Auth::Login::WWW::CGI::Cookie>, L<Bugzilla::Auth::Verify::DB>
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
# -*- 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): Erik Stambaugh <erik@dasbistro.com>
|
||||
|
||||
package Bugzilla::Auth::Login::WWW;
|
||||
|
||||
use strict;
|
||||
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::Config;
|
||||
|
||||
# $current_login_class stores the name of the login style that succeeded.
|
||||
my $current_login_class = undef;
|
||||
sub login_class {
|
||||
my ($class, $type) = @_;
|
||||
if ($type) {
|
||||
$current_login_class = $type;
|
||||
}
|
||||
return $current_login_class;
|
||||
}
|
||||
|
||||
# can_logout determines if a user may log out
|
||||
sub can_logout {
|
||||
return 1 if (login_class && login_class->can_logout);
|
||||
return 0;
|
||||
}
|
||||
|
||||
sub login {
|
||||
my ($class, $type) = @_;
|
||||
|
||||
my $user = Bugzilla->user;
|
||||
|
||||
# Avoid double-logins, which may confuse the auth code
|
||||
# (double cookies, odd compat code settings, etc)
|
||||
return $user if $user->id;
|
||||
|
||||
$type = LOGIN_REQUIRED if Bugzilla->cgi->param('GoAheadAndLogIn');
|
||||
$type = LOGIN_NORMAL unless defined $type;
|
||||
|
||||
# Log in using whatever methods are defined in user_info_class.
|
||||
# Please note the particularly strange way require() and the function
|
||||
# calls are being done, because we're calling a module that's named in
|
||||
# a string. I assure you it works, and it avoids the need for an eval().
|
||||
my $userid;
|
||||
for my $login_class (split(/,\s*/, Param('user_info_class'))) {
|
||||
require "Bugzilla/Auth/Login/WWW/" . $login_class . ".pm";
|
||||
$userid = "Bugzilla::Auth::Login::WWW::$login_class"->login($type);
|
||||
if ($userid) {
|
||||
$class->login_class("Bugzilla::Auth::Login::WWW::$login_class");
|
||||
last;
|
||||
}
|
||||
}
|
||||
|
||||
if ($userid) {
|
||||
$user = new Bugzilla::User($userid);
|
||||
|
||||
# Redirect to SSL if required
|
||||
if (Param('sslbase') ne '' and Param('ssl') ne 'never') {
|
||||
Bugzilla->cgi->require_https(Param('sslbase'));
|
||||
}
|
||||
$user->set_flags('can_logout' => $class->can_logout);
|
||||
} else {
|
||||
Bugzilla->logout_request();
|
||||
}
|
||||
return $user;
|
||||
}
|
||||
|
||||
sub logout {
|
||||
my ($class, $user, $option) = @_;
|
||||
if (can_logout) {
|
||||
$class->login_class->logout($user, $option);
|
||||
}
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::Auth::Login::WWW - WWW login information gathering module
|
||||
|
||||
=head1 METHODS
|
||||
|
||||
=over
|
||||
|
||||
=item C<login>
|
||||
|
||||
Passes C<login> calls to each class defined in the param C<user_info_class>
|
||||
and returns a C<Bugzilla::User> object from the first one that successfully
|
||||
gathers user login information.
|
||||
|
||||
=back
|
||||
@@ -1,275 +0,0 @@
|
||||
# -*- 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>
|
||||
# Dan Mosedale <dmose@mozilla.org>
|
||||
# Joe Robins <jmrobins@tgix.com>
|
||||
# Dave Miller <justdave@syndicomm.com>
|
||||
# Christopher Aillon <christopher@aillon.com>
|
||||
# Gervase Markham <gerv@gerv.net>
|
||||
# Christian Reis <kiko@async.com.br>
|
||||
# Bradley Baetz <bbaetz@acm.org>
|
||||
# Erik Stambaugh <erik@dasbistro.com>
|
||||
|
||||
package Bugzilla::Auth::Login::WWW::CGI;
|
||||
|
||||
use strict;
|
||||
|
||||
use Bugzilla::Config;
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::Error;
|
||||
use Bugzilla::Util;
|
||||
use Bugzilla::Token;
|
||||
|
||||
sub login {
|
||||
my ($class, $type) = @_;
|
||||
|
||||
# 'NORMAL' logins depend on the 'requirelogin' param
|
||||
if ($type == LOGIN_NORMAL) {
|
||||
$type = Param('requirelogin') ? LOGIN_REQUIRED : LOGIN_OPTIONAL;
|
||||
}
|
||||
|
||||
my $cgi = Bugzilla->cgi;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
# First, try the actual login method against form variables
|
||||
my $username = $cgi->param("Bugzilla_login");
|
||||
my $passwd = $cgi->param("Bugzilla_password");
|
||||
|
||||
$cgi->delete('Bugzilla_login', 'Bugzilla_password');
|
||||
|
||||
# Perform the actual authentication, get the method name from the class name
|
||||
my ($authmethod, $authres, $userid, $extra, $info) =
|
||||
Bugzilla::Auth->authenticate($username, $passwd);
|
||||
|
||||
if ($authres == AUTH_OK) {
|
||||
# Login via username/password was correct and valid, so create
|
||||
# and send out the login cookies
|
||||
my $ipaddr = $cgi->remote_addr;
|
||||
unless ($cgi->param('Bugzilla_restrictlogin') ||
|
||||
Param('loginnetmask') == 32) {
|
||||
$ipaddr = Bugzilla::Auth::get_netaddr($ipaddr);
|
||||
}
|
||||
|
||||
# The IP address is valid, at least for comparing with itself in a
|
||||
# subsequent login
|
||||
trick_taint($ipaddr);
|
||||
|
||||
my $logincookie = Bugzilla::Token::GenerateUniqueToken('logincookies', 'cookie');
|
||||
|
||||
$dbh->do("INSERT INTO logincookies (cookie, userid, ipaddr, lastused)
|
||||
VALUES (?, ?, ?, NOW())",
|
||||
undef,
|
||||
$logincookie, $userid, $ipaddr);
|
||||
|
||||
# Remember cookie only if admin has told so
|
||||
# or admin didn't forbid it and user told to remember.
|
||||
if ((Param('rememberlogin') eq 'on') ||
|
||||
((Param('rememberlogin') ne 'off') &&
|
||||
$cgi->param('Bugzilla_remember') &&
|
||||
($cgi->param('Bugzilla_remember') eq 'on'))) {
|
||||
$cgi->send_cookie(-name => 'Bugzilla_login',
|
||||
-value => $userid,
|
||||
-expires => 'Fri, 01-Jan-2038 00:00:00 GMT');
|
||||
$cgi->send_cookie(-name => 'Bugzilla_logincookie',
|
||||
-value => $logincookie,
|
||||
-expires => 'Fri, 01-Jan-2038 00:00:00 GMT');
|
||||
|
||||
}
|
||||
else {
|
||||
$cgi->send_cookie(-name => 'Bugzilla_login',
|
||||
-value => $userid);
|
||||
$cgi->send_cookie(-name => 'Bugzilla_logincookie',
|
||||
-value => $logincookie);
|
||||
}
|
||||
}
|
||||
elsif ($authres == AUTH_NODATA) {
|
||||
# No data from the form, so try to login via cookies
|
||||
$username = $cgi->cookie("Bugzilla_login");
|
||||
$passwd = $cgi->cookie("Bugzilla_logincookie");
|
||||
|
||||
require Bugzilla::Auth::Login::WWW::CGI::Cookie;
|
||||
my $authmethod = "Cookie";
|
||||
|
||||
($authres, $userid, $extra) =
|
||||
Bugzilla::Auth::Login::WWW::CGI::Cookie->authenticate($username, $passwd);
|
||||
|
||||
# If the data for the cookie was incorrect, then treat that as
|
||||
# NODATA. This could occur if the user's IP changed, for example.
|
||||
# Give them un-loggedin access if allowed (checked below)
|
||||
$authres = AUTH_NODATA if $authres == AUTH_LOGINFAILED;
|
||||
}
|
||||
|
||||
# Now check the result
|
||||
|
||||
# An error may have occurred with the login mechanism
|
||||
if ($authres == AUTH_ERROR) {
|
||||
ThrowCodeError("auth_err",
|
||||
{ authmethod => lc($authmethod),
|
||||
userid => $userid,
|
||||
auth_err_tag => $extra,
|
||||
info => $info
|
||||
});
|
||||
}
|
||||
|
||||
# We can load the page if the login was ok, or there was no data
|
||||
# but a login wasn't required
|
||||
if ($authres == AUTH_OK ||
|
||||
($authres == AUTH_NODATA && $type == LOGIN_OPTIONAL)) {
|
||||
|
||||
# login succeded, so we're done
|
||||
return $userid;
|
||||
}
|
||||
|
||||
# No login details were given, but we require a login if the
|
||||
# page does
|
||||
if ($authres == AUTH_NODATA && $type == LOGIN_REQUIRED) {
|
||||
|
||||
# Redirect to SSL if required
|
||||
if (Param('sslbase') ne '' and Param('ssl') ne 'never') {
|
||||
$cgi->require_https(Param('sslbase'));
|
||||
}
|
||||
|
||||
# Throw up the login page
|
||||
|
||||
print Bugzilla->cgi->header();
|
||||
|
||||
my $template = Bugzilla->template;
|
||||
$template->process("account/auth/login.html.tmpl",
|
||||
{ 'target' => $cgi->url(-relative=>1),
|
||||
'caneditaccount' => Bugzilla::Auth->can_edit('new'),
|
||||
'has_db' => Bugzilla::Auth->has_db,
|
||||
}
|
||||
)
|
||||
|| ThrowTemplateError($template->error());
|
||||
|
||||
# This seems like as good as time as any to get rid of old
|
||||
# crufty junk in the logincookies table. Get rid of any entry
|
||||
# that hasn't been used in a month.
|
||||
$dbh->do("DELETE FROM logincookies WHERE " .
|
||||
$dbh->sql_to_days('NOW()') . " - " .
|
||||
$dbh->sql_to_days('lastused') . " > 30");
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
# The username/password may be wrong
|
||||
# Don't let the user know whether the username exists or whether
|
||||
# the password was just wrong. (This makes it harder for a cracker
|
||||
# to find account names by brute force)
|
||||
if ($authres == AUTH_LOGINFAILED) {
|
||||
ThrowUserError("invalid_username_or_password");
|
||||
}
|
||||
|
||||
# The account may be disabled
|
||||
if ($authres == AUTH_DISABLED) {
|
||||
clear_browser_cookies();
|
||||
# and throw a user error
|
||||
ThrowUserError("account_disabled",
|
||||
{'disabled_reason' => $extra});
|
||||
}
|
||||
|
||||
# If we get here, then we've run out of options, which shouldn't happen
|
||||
ThrowCodeError("authres_unhandled", { authres => $authres,
|
||||
type => $type });
|
||||
}
|
||||
|
||||
# This auth style allows the user to log out.
|
||||
sub can_logout { return 1; }
|
||||
|
||||
# Logs user out, according to the option provided; this consists of
|
||||
# removing entries from logincookies for the specified $user.
|
||||
sub logout {
|
||||
my ($class, $user, $option) = @_;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $cgi = Bugzilla->cgi;
|
||||
$option = LOGOUT_ALL unless defined $option;
|
||||
|
||||
if ($option == LOGOUT_ALL) {
|
||||
$dbh->do("DELETE FROM logincookies WHERE userid = ?",
|
||||
undef, $user->id);
|
||||
return;
|
||||
}
|
||||
|
||||
# The LOGOUT_*_CURRENT options require the current login cookie.
|
||||
# If a new cookie has been issued during this run, that's the current one.
|
||||
# If not, it's the one we've received.
|
||||
my $cookie;
|
||||
foreach (@{$cgi->{'Bugzilla_cookie_list'}}) {
|
||||
if ($_->name() eq 'Bugzilla_logincookie') {
|
||||
$cookie = $_->value();
|
||||
last;
|
||||
}
|
||||
}
|
||||
$cookie ||= $cgi->cookie("Bugzilla_logincookie");
|
||||
trick_taint($cookie);
|
||||
|
||||
# These queries use both the cookie ID and the user ID as keys. Even
|
||||
# though we know the userid must match, we still check it in the SQL
|
||||
# as a sanity check, since there is no locking here, and if the user
|
||||
# logged out from two machines simultaneously, while someone else
|
||||
# logged in and got the same cookie, we could be logging the other
|
||||
# user out here. Yes, this is very very very unlikely, but why take
|
||||
# chances? - bbaetz
|
||||
if ($option == LOGOUT_KEEP_CURRENT) {
|
||||
$dbh->do("DELETE FROM logincookies WHERE cookie != ? AND userid = ?",
|
||||
undef, $cookie, $user->id);
|
||||
} elsif ($option == LOGOUT_CURRENT) {
|
||||
$dbh->do("DELETE FROM logincookies WHERE cookie = ? AND userid = ?",
|
||||
undef, $cookie, $user->id);
|
||||
} else {
|
||||
die("Invalid option $option supplied to logout()");
|
||||
}
|
||||
|
||||
if ($option != LOGOUT_KEEP_CURRENT) {
|
||||
clear_browser_cookies();
|
||||
Bugzilla->logout_request();
|
||||
}
|
||||
}
|
||||
|
||||
sub clear_browser_cookies {
|
||||
my $cgi = Bugzilla->cgi;
|
||||
$cgi->remove_cookie('Bugzilla_login');
|
||||
$cgi->remove_cookie('Bugzilla_logincookie');
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::Auth::Login::WWW::CGI - CGI-based logins for Bugzilla
|
||||
|
||||
=head1 SUMMARY
|
||||
|
||||
This is a L<login module|Bugzilla::Auth/"LOGIN"> for Bugzilla. Users connecting
|
||||
from a CGI script use this module to authenticate. Logouts are also handled here.
|
||||
|
||||
=head1 BEHAVIOUR
|
||||
|
||||
Users are first authenticated against the default authentication handler,
|
||||
using the CGI parameters I<Bugzilla_login> and I<Bugzilla_password>.
|
||||
|
||||
If no data is present for that, then cookies are tried, using
|
||||
L<Bugzilla::Auth::Login::WWW::CGI::Cookie>.
|
||||
|
||||
=head1 SEE ALSO
|
||||
|
||||
L<Bugzilla::Auth>
|
||||
@@ -1,113 +0,0 @@
|
||||
# -*- 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>
|
||||
# Dan Mosedale <dmose@mozilla.org>
|
||||
# Joe Robins <jmrobins@tgix.com>
|
||||
# Dave Miller <justdave@syndicomm.com>
|
||||
# Christopher Aillon <christopher@aillon.com>
|
||||
# Gervase Markham <gerv@gerv.net>
|
||||
# Christian Reis <kiko@async.com.br>
|
||||
# Bradley Baetz <bbaetz@acm.org>
|
||||
|
||||
package Bugzilla::Auth::Login::WWW::CGI::Cookie;
|
||||
|
||||
use strict;
|
||||
|
||||
use Bugzilla::Auth;
|
||||
use Bugzilla::Config;
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::Util;
|
||||
|
||||
sub authenticate {
|
||||
my ($class, $login, $login_cookie) = @_;
|
||||
|
||||
return (AUTH_NODATA) unless defined $login && defined $login_cookie;
|
||||
|
||||
my $cgi = Bugzilla->cgi;
|
||||
|
||||
my $ipaddr = $cgi->remote_addr();
|
||||
my $netaddr = Bugzilla::Auth::get_netaddr($ipaddr);
|
||||
|
||||
# Anything goes for these params - they're just strings which
|
||||
# we're going to verify against the db
|
||||
trick_taint($login);
|
||||
trick_taint($login_cookie);
|
||||
trick_taint($ipaddr);
|
||||
|
||||
my $query = "SELECT profiles.userid, profiles.disabledtext " .
|
||||
"FROM logincookies, profiles " .
|
||||
"WHERE logincookies.cookie=? AND " .
|
||||
" logincookies.userid=profiles.userid AND " .
|
||||
" logincookies.userid=? AND " .
|
||||
" (logincookies.ipaddr=?";
|
||||
my @params = ($login_cookie, $login, $ipaddr);
|
||||
if (defined $netaddr) {
|
||||
trick_taint($netaddr);
|
||||
$query .= " OR logincookies.ipaddr=?";
|
||||
push(@params, $netaddr);
|
||||
}
|
||||
$query .= ")";
|
||||
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my ($userid, $disabledtext) = $dbh->selectrow_array($query, undef, @params);
|
||||
|
||||
return (AUTH_DISABLED, $userid, $disabledtext)
|
||||
if ($disabledtext);
|
||||
|
||||
if ($userid) {
|
||||
# If we logged in successfully, then update the lastused time on the
|
||||
# login cookie
|
||||
$dbh->do("UPDATE logincookies SET lastused=NOW() WHERE cookie=?",
|
||||
undef,
|
||||
$login_cookie);
|
||||
|
||||
return (AUTH_OK, $userid);
|
||||
}
|
||||
|
||||
# If we get here, then the login failed.
|
||||
return (AUTH_LOGINFAILED);
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::Auth::Login::WWW::CGI::Cookie - cookie authentication for Bugzilla
|
||||
|
||||
=head1 SUMMARY
|
||||
|
||||
This is an L<authentication module|Bugzilla::Auth/"AUTHENTICATION"> for
|
||||
Bugzilla, which logs the user in using a persistent cookie stored in the
|
||||
C<logincookies> table.
|
||||
|
||||
The actual password is not stored in the cookie; only the userid and a
|
||||
I<logincookie> (which is used to reverify the login without requiring the
|
||||
password to be sent over the network) are. These I<logincookies> are
|
||||
restricted to certain IP addresses as a security meaure. The exact
|
||||
restriction can be specified by the admin via the C<loginnetmask> parameter.
|
||||
|
||||
This module does not ever send a cookie (It has no way of knowing when a user
|
||||
is successfully logged in). Instead L<Bugzilla::Auth::Login::WWW::CGI> handles this.
|
||||
|
||||
=head1 SEE ALSO
|
||||
|
||||
L<Bugzilla::Auth>, L<Bugzilla::Auth::Login::WWW::CGI>
|
||||
@@ -1,192 +0,0 @@
|
||||
# -*- 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): Erik Stambaugh <erik@dasbistro.com>
|
||||
|
||||
package Bugzilla::Auth::Login::WWW::Env;
|
||||
|
||||
use strict;
|
||||
|
||||
use Bugzilla::Config;
|
||||
use Bugzilla::Error;
|
||||
use Bugzilla::Util;
|
||||
use Bugzilla::User;
|
||||
|
||||
sub login {
|
||||
my ($class, $type) = @_;
|
||||
|
||||
# XXX This does not currently work correctly with Param('requirelogin').
|
||||
# Bug 253636 will hopefully see that param's needs taken care of in a
|
||||
# parent module, but for the time being, this module does not honor
|
||||
# the param in the way that CGI.pm does.
|
||||
|
||||
my $matched_userid = '';
|
||||
my $matched_extern_id = '';
|
||||
my $disabledtext = '';
|
||||
my $new_login_name = 0;
|
||||
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $sth;
|
||||
|
||||
# Gather the environment variables
|
||||
my $env_id = $ENV{Param("auth_env_id")};
|
||||
my $env_email = $ENV{Param("auth_env_email")};
|
||||
my $env_realname = $ENV{Param("auth_env_realname")};
|
||||
|
||||
# allow undefined values to work with trick_taint
|
||||
for ($env_id, $env_email, $env_realname) { $_ ||= '' };
|
||||
# make sure the email field contains only a valid email address
|
||||
my $emailregexp = Param("emailregexp");
|
||||
if ($env_email =~ /($emailregexp)/) {
|
||||
$env_email = $1;
|
||||
}
|
||||
else {
|
||||
return undef;
|
||||
}
|
||||
# untaint the remaining values
|
||||
trick_taint($env_id);
|
||||
trick_taint($env_realname);
|
||||
|
||||
if ($env_id || $env_email) {
|
||||
# Look in the DB for the extern_id
|
||||
if ($env_id) {
|
||||
|
||||
# Not having the email address defined but having an ID isn't
|
||||
# allowed.
|
||||
return undef unless $env_email;
|
||||
|
||||
$sth = $dbh->prepare("SELECT userid, disabledtext " .
|
||||
"FROM profiles WHERE extern_id=?");
|
||||
$sth->execute($env_id);
|
||||
my $fetched = $sth->fetch;
|
||||
if ($fetched) {
|
||||
$matched_userid = $fetched->[0];
|
||||
$disabledtext = $fetched->[1];
|
||||
}
|
||||
}
|
||||
|
||||
unless ($matched_userid) {
|
||||
# There was either no match for the external ID given, or one was
|
||||
# not present.
|
||||
#
|
||||
# Check to see if the email address is in there and has no
|
||||
# external id assigned. We test for both the login name (which we
|
||||
# also sent), and the id, so that we have a way of telling that we
|
||||
# got something instead of a bunch of NULLs
|
||||
$sth = $dbh->prepare("SELECT extern_id, userid, disabledtext " .
|
||||
"FROM profiles WHERE " .
|
||||
$dbh->sql_istrcmp('login_name', '?'));
|
||||
$sth->execute($env_email);
|
||||
|
||||
$sth->execute();
|
||||
my $fetched = $sth->fetch();
|
||||
if ($fetched) {
|
||||
($matched_extern_id, $matched_userid, $disabledtext) = @{$fetched};
|
||||
}
|
||||
if ($matched_userid) {
|
||||
if ($matched_extern_id) {
|
||||
# someone with a different external ID has that address!
|
||||
ThrowUserError("extern_id_conflict");
|
||||
}
|
||||
else
|
||||
{
|
||||
# someone with no external ID used that address, time to
|
||||
# add the ID!
|
||||
$sth = $dbh->prepare("UPDATE profiles " .
|
||||
"SET extern_id=? WHERE userid=?");
|
||||
$sth->execute($env_id, $matched_userid);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
# Need to create a new user with that email address. Note
|
||||
# that cryptpassword has been filled in with '*', since the
|
||||
# user has no DB password.
|
||||
insert_new_user($env_email, $env_realname, '*');
|
||||
my $new_user = Bugzilla::User->new_from_login($env_email);
|
||||
$matched_userid = $new_user->id;
|
||||
$new_login_name = $matched_userid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# now that we hopefully have a username, we need to see if the data
|
||||
# has to be updated
|
||||
if ($matched_userid) {
|
||||
$sth = $dbh->prepare("SELECT login_name, realname " .
|
||||
"FROM profiles " .
|
||||
"WHERE userid=?");
|
||||
$sth->execute($matched_userid);
|
||||
my $fetched = $sth->fetch;
|
||||
my $username = $fetched->[0];
|
||||
my $this_realname = $fetched->[1];
|
||||
if ( ($username ne $env_email) ||
|
||||
($this_realname ne $env_realname) ) {
|
||||
|
||||
$sth = $dbh->prepare("UPDATE profiles " .
|
||||
"SET login_name=?, " .
|
||||
"realname=? " .
|
||||
"WHERE userid=?");
|
||||
$sth->execute($env_email,
|
||||
($env_realname || $this_realname),
|
||||
$matched_userid);
|
||||
$sth->execute;
|
||||
$new_login_name = $matched_userid;
|
||||
}
|
||||
}
|
||||
|
||||
# If the login name may be new, make sure the regexp groups are current
|
||||
if ($new_login_name) {
|
||||
my $userprofile = new Bugzilla::User($matched_userid);
|
||||
$userprofile->derive_regexp_groups;
|
||||
}
|
||||
|
||||
# Now we throw an error if the user has been disabled
|
||||
if ($disabledtext) {
|
||||
ThrowUserError("account_disabled",
|
||||
{'disabled_reason' => $disabledtext});
|
||||
}
|
||||
|
||||
return $matched_userid;
|
||||
|
||||
}
|
||||
|
||||
# This auth style does not allow the user to log out.
|
||||
sub can_logout { return 0; }
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::Auth::Env - Environment Variable Authentication
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
Many external user authentication systems supply login information to CGI
|
||||
programs via environment variables. This module checks to see if those
|
||||
variables are populated and, if so, assumes authentication was successful and
|
||||
returns the user's ID, having automatically created a new profile if
|
||||
necessary.
|
||||
|
||||
=head1 SEE ALSO
|
||||
|
||||
L<Bugzilla::Auth>
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
How Auth Works
|
||||
==============
|
||||
Christian Reis <kiko@async.com.br>
|
||||
|
||||
Overview
|
||||
--------
|
||||
|
||||
Authentication in Bugzilla is handled by a collection of modules that live in
|
||||
the Bugzilla::Auth package. These modules are organized hierarchically based
|
||||
upon their responsibility.
|
||||
|
||||
The authentication scheme is divided in two tasks: Login and Verify. Login
|
||||
involves gathering credentials from a user, while Verify validates them
|
||||
against an authentication service.
|
||||
|
||||
The Bugzilla parameters user_info_class and user_verify_class contain a
|
||||
list of Login and Verify modules, respectively.
|
||||
|
||||
Task: Login
|
||||
-----------
|
||||
|
||||
This task obtains user credentials based on a request. Examples of requests
|
||||
include CGI access from the Bugzilla web interface, email submissions and
|
||||
credentials supplied by standalone scripts.
|
||||
|
||||
Each type of Bugzilla front-end should have its own package. For instance,
|
||||
access via the Bugzilla web pages should go through Bugzilla::Auth::WWW.
|
||||
These packages would contain modules of their own to perform whatever extra
|
||||
functions are needed, like the CGI and Cookie modules in the case of WWW.
|
||||
|
||||
Task: Verify
|
||||
------------
|
||||
|
||||
This task validates user credentials against a user authentication service.
|
||||
|
||||
The default service in Bugzilla has been the database, which stores the
|
||||
login_name and cryptpasswd fields in the profiles table. An alternative means
|
||||
of validation, LDAP, is already supported, and other contributions would be
|
||||
appreciated.
|
||||
|
||||
The module layout is similar to the Login package, but there is no need for a
|
||||
sub-level as there is with Login request types.
|
||||
|
||||
Params
|
||||
------
|
||||
|
||||
There are two params that define behaviour for each authentication task. Each
|
||||
of them defines a comma-separated list of modules to be tried in order.
|
||||
|
||||
- user_info_class determines the module(s) used to obtain user
|
||||
credentials. This param is specific to the requests from Bugzilla web
|
||||
pages, so all of the listed modules live under
|
||||
Bugzilla::Auth::Login::WWW
|
||||
|
||||
- user_verify_class determines the module(s) used to verify credentials.
|
||||
This param is general and concerns the whole Bugzilla instance, since
|
||||
the same back end should be used regardless of what front end is used.
|
||||
|
||||
Responsibilities
|
||||
----------------
|
||||
|
||||
Bugzilla::Auth
|
||||
|
||||
This module is responsible for abstracting away as much as possible the
|
||||
login and logout tasks in Bugzilla.
|
||||
|
||||
It offers login() and logout() methods that are proxied to the selected
|
||||
login and verify packages.
|
||||
|
||||
Bugzilla::Auth::Login
|
||||
|
||||
This is a container to hold the various modules for each request type.
|
||||
|
||||
Bugzilla::Auth::Login::WWW
|
||||
|
||||
This module is responsible for abstracting away details of which web-based
|
||||
login modules exist and are in use. It offers login() and logout() methods
|
||||
that proxy through to whatever specific modules
|
||||
|
||||
Bugzilla::Auth::Verify
|
||||
|
||||
This module is responsible for abstracting away details of which
|
||||
credential verification modules exist, and should proxy calls through to
|
||||
them. There is a method that is particularly important, and which should
|
||||
be proxied through to the specific:
|
||||
|
||||
can_edit($type)
|
||||
|
||||
This method takes an argument that specifies what sort of change
|
||||
is being requested; the specific module should return 1 or 0 based
|
||||
on the fact that it implements or not the required change.
|
||||
|
||||
Current values for $type are "new" for new accounts, and "userid",
|
||||
"login_name", "realname" for their respective fields.
|
||||
|
||||
Specific Login Modules
|
||||
----------------------
|
||||
|
||||
WWW
|
||||
|
||||
The main authentication frontend; regular pages (CGIs) should use only
|
||||
this module. It offers a convenient frontend to the main functionality
|
||||
that CGIs need, using form parameters and cookies.
|
||||
|
||||
- Cookie
|
||||
|
||||
Implements part of the backend code that deals with browser
|
||||
cookies. It's actually tied in to DB.pm, so Cookie logins that use
|
||||
LDAP won't work at all.
|
||||
|
||||
LDAP
|
||||
|
||||
The other authentication module is LDAP-based; it is *only* used for
|
||||
password authentication and not for any other login-related task (it
|
||||
actually relies on the database to handle the profile information).
|
||||
|
||||
Legacy
|
||||
------
|
||||
|
||||
Bugzilla.pm
|
||||
|
||||
There is glue code that currently lives in the top-level module
|
||||
Bugzilla.pm; this module handles backwards-compatibility data that is used
|
||||
in a number of CGIs. This data has been slowly removed from the Bugzilla
|
||||
pages and eventually should go away completely, at which point Bugzilla.pm
|
||||
will be just a wrapper to conveniently offer template, cgi, dbh and user
|
||||
variables.
|
||||
|
||||
This module is meant to be used only by Bugzilla pages, and in the case of
|
||||
a reorganization which moves CGI-specific code to a subdirectory,
|
||||
Bugzilla.pm should go with it.
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
# -*- 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>
|
||||
# Dan Mosedale <dmose@mozilla.org>
|
||||
# Joe Robins <jmrobins@tgix.com>
|
||||
# Dave Miller <justdave@syndicomm.com>
|
||||
# Christopher Aillon <christopher@aillon.com>
|
||||
# Gervase Markham <gerv@gerv.net>
|
||||
# Christian Reis <kiko@async.com.br>
|
||||
# Bradley Baetz <bbaetz@acm.org>
|
||||
# Erik Stambaugh <erik@dasbistro.com>
|
||||
|
||||
package Bugzilla::Auth::Verify::DB;
|
||||
|
||||
use strict;
|
||||
|
||||
use Bugzilla::Config;
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::Util;
|
||||
use Bugzilla::User;
|
||||
|
||||
my $edit_options = {
|
||||
'new' => 1,
|
||||
'userid' => 0,
|
||||
'login_name' => 1,
|
||||
'realname' => 1,
|
||||
};
|
||||
|
||||
sub can_edit {
|
||||
my ($class, $type) = @_;
|
||||
return $edit_options->{$type};
|
||||
}
|
||||
|
||||
sub authenticate {
|
||||
my ($class, $username, $passwd) = @_;
|
||||
|
||||
return (AUTH_NODATA) unless defined $username && defined $passwd;
|
||||
|
||||
my $userid = Bugzilla::User::login_to_id($username);
|
||||
return (AUTH_LOGINFAILED) unless $userid;
|
||||
|
||||
return (AUTH_LOGINFAILED, $userid)
|
||||
unless $class->check_password($userid, $passwd);
|
||||
|
||||
# The user's credentials are okay, so delete any outstanding
|
||||
# password tokens they may have generated.
|
||||
require Bugzilla::Token;
|
||||
Bugzilla::Token::DeletePasswordTokens($userid, "user_logged_in");
|
||||
|
||||
# Account may have been disabled
|
||||
my $disabledtext = $class->get_disabled($userid);
|
||||
return (AUTH_DISABLED, $userid, $disabledtext)
|
||||
if $disabledtext ne '';
|
||||
|
||||
return (AUTH_OK, $userid);
|
||||
}
|
||||
|
||||
sub get_disabled {
|
||||
my ($class, $userid) = @_;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $sth = $dbh->prepare_cached("SELECT disabledtext FROM profiles " .
|
||||
"WHERE userid=?");
|
||||
my ($text) = $dbh->selectrow_array($sth, undef, $userid);
|
||||
return $text;
|
||||
}
|
||||
|
||||
sub check_password {
|
||||
my ($class, $userid, $passwd) = @_;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $sth = $dbh->prepare_cached("SELECT cryptpassword FROM profiles " .
|
||||
"WHERE userid=?");
|
||||
my ($realcryptpwd) = $dbh->selectrow_array($sth, undef, $userid);
|
||||
|
||||
# Get the salt from the user's crypted password.
|
||||
my $salt = $realcryptpwd;
|
||||
|
||||
# Using the salt, crypt the password the user entered.
|
||||
my $enteredCryptedPassword = crypt($passwd, $salt);
|
||||
|
||||
return $enteredCryptedPassword eq $realcryptpwd;
|
||||
}
|
||||
|
||||
sub change_password {
|
||||
my ($class, $userid, $password) = @_;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $cryptpassword = bz_crypt($password);
|
||||
$dbh->do("UPDATE profiles SET cryptpassword = ? WHERE userid = ?",
|
||||
undef, $cryptpassword, $userid);
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::Auth::Verify::DB - database authentication for Bugzilla
|
||||
|
||||
=head1 SUMMARY
|
||||
|
||||
This is an L<authentication module|Bugzilla::Auth/"AUTHENTICATION"> for
|
||||
Bugzilla, which logs the user in using the password stored in the C<profiles>
|
||||
table. This is the most commonly used authentication module.
|
||||
|
||||
=head1 SEE ALSO
|
||||
|
||||
L<Bugzilla::Auth>
|
||||
@@ -1,198 +0,0 @@
|
||||
# -*- 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>
|
||||
# Dan Mosedale <dmose@mozilla.org>
|
||||
# Joe Robins <jmrobins@tgix.com>
|
||||
# Dave Miller <justdave@syndicomm.com>
|
||||
# Christopher Aillon <christopher@aillon.com>
|
||||
# Gervase Markham <gerv@gerv.net>
|
||||
# Christian Reis <kiko@async.com.br>
|
||||
# Bradley Baetz <bbaetz@acm.org>
|
||||
# Erik Stambaugh <erik@dasbistro.com>
|
||||
|
||||
package Bugzilla::Auth::Verify::LDAP;
|
||||
|
||||
use strict;
|
||||
|
||||
use Bugzilla::Config;
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::User;
|
||||
|
||||
use Net::LDAP;
|
||||
|
||||
my $edit_options = {
|
||||
'new' => 0,
|
||||
'userid' => 0,
|
||||
'login_name' => 0,
|
||||
'realname' => 0,
|
||||
};
|
||||
|
||||
sub can_edit {
|
||||
my ($class, $type) = @_;
|
||||
return $edit_options->{$type};
|
||||
}
|
||||
|
||||
sub authenticate {
|
||||
my ($class, $username, $passwd) = @_;
|
||||
|
||||
# If no password was provided, then fail the authentication.
|
||||
# While it may be valid to not have an LDAP password, when you
|
||||
# bind without a password (regardless of the binddn value), you
|
||||
# will get an anonymous bind. I do not know of a way to determine
|
||||
# whether a bind is anonymous or not without making changes to the
|
||||
# LDAP access control settings
|
||||
return (AUTH_NODATA) unless $username && $passwd;
|
||||
|
||||
# We need to bind anonymously to the LDAP server. This is
|
||||
# because we need to get the Distinguished Name of the user trying
|
||||
# to log in. Some servers (such as iPlanet) allow you to have unique
|
||||
# uids spread out over a subtree of an area (such as "People"), so
|
||||
# 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.
|
||||
my $LDAPserver = Param("LDAPserver");
|
||||
if ($LDAPserver eq "") {
|
||||
return (AUTH_ERROR, undef, "server_not_defined");
|
||||
}
|
||||
|
||||
my $LDAPport = "389"; # default LDAP port
|
||||
if($LDAPserver =~ /:/) {
|
||||
($LDAPserver, $LDAPport) = split(":",$LDAPserver);
|
||||
}
|
||||
my $LDAPconn = Net::LDAP->new($LDAPserver, port => $LDAPport, version => 3);
|
||||
if(!$LDAPconn) {
|
||||
return (AUTH_ERROR, undef, "connect_failed");
|
||||
}
|
||||
|
||||
my $mesg;
|
||||
if (Param("LDAPbinddn")) {
|
||||
my ($LDAPbinddn,$LDAPbindpass) = split(":",Param("LDAPbinddn"));
|
||||
$mesg = $LDAPconn->bind($LDAPbinddn, password => $LDAPbindpass);
|
||||
}
|
||||
else {
|
||||
$mesg = $LDAPconn->bind();
|
||||
}
|
||||
if($mesg->code) {
|
||||
return (AUTH_ERROR, undef,
|
||||
"connect_failed",
|
||||
{ errstr => $mesg->error });
|
||||
}
|
||||
|
||||
# We've got our anonymous bind; let's look up this user.
|
||||
$mesg = $LDAPconn->search( base => Param("LDAPBaseDN"),
|
||||
scope => "sub",
|
||||
filter => '(&(' . Param("LDAPuidattribute") . "=$username)" . Param("LDAPfilter") . ')',
|
||||
attrs => ['dn'],
|
||||
);
|
||||
return (AUTH_LOGINFAILED, undef, "lookup_failure")
|
||||
unless $mesg->count;
|
||||
|
||||
# Now we get the DN from this search.
|
||||
my $userDN = $mesg->shift_entry->dn;
|
||||
|
||||
# Now we attempt to bind as the specified user.
|
||||
$mesg = $LDAPconn->bind( $userDN, password => $passwd);
|
||||
|
||||
return (AUTH_LOGINFAILED) if $mesg->code;
|
||||
|
||||
# And now we're going to repeat the search, so that we can get the
|
||||
# mail attribute for this user.
|
||||
$mesg = $LDAPconn->search( base => Param("LDAPBaseDN"),
|
||||
scope => "sub",
|
||||
filter => '(&(' . Param("LDAPuidattribute") . "=$username)" . Param("LDAPfilter") . ')',
|
||||
);
|
||||
my $user_entry = $mesg->shift_entry if !$mesg->code && $mesg->count;
|
||||
if(!$user_entry || !$user_entry->exists(Param("LDAPmailattribute"))) {
|
||||
return (AUTH_ERROR, undef,
|
||||
"cannot_retreive_attr",
|
||||
{ attr => Param("LDAPmailattribute") });
|
||||
}
|
||||
|
||||
# get the mail attribute
|
||||
$username = $user_entry->get_value(Param("LDAPmailattribute"));
|
||||
# OK, so now we know that the user is valid. Lets try finding them in the
|
||||
# Bugzilla database
|
||||
|
||||
# XXX - should this part be made more generic, and placed in
|
||||
# Bugzilla::Auth? Lots of login mechanisms may have to do this, although
|
||||
# until we actually get some more, its hard to know - BB
|
||||
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $sth = $dbh->prepare_cached("SELECT userid, disabledtext " .
|
||||
"FROM profiles " .
|
||||
"WHERE " .
|
||||
$dbh->sql_istrcmp('login_name', '?'));
|
||||
my ($userid, $disabledtext) =
|
||||
$dbh->selectrow_array($sth,
|
||||
undef,
|
||||
$username);
|
||||
|
||||
# If the user doesn't exist, then they need to be added
|
||||
unless ($userid) {
|
||||
# We'll want the user's name for this.
|
||||
my $userRealName = $user_entry->get_value("displayName");
|
||||
if($userRealName eq "") {
|
||||
$userRealName = $user_entry->get_value("cn");
|
||||
}
|
||||
insert_new_user($username, $userRealName);
|
||||
|
||||
($userid, $disabledtext) = $dbh->selectrow_array($sth,
|
||||
undef,
|
||||
$username);
|
||||
return (AUTH_ERROR, $userid, "no_userid")
|
||||
unless $userid;
|
||||
}
|
||||
|
||||
# we're done, so disconnect
|
||||
$LDAPconn->unbind;
|
||||
|
||||
# Test for disabled account
|
||||
return (AUTH_DISABLED, $userid, $disabledtext)
|
||||
if $disabledtext ne '';
|
||||
|
||||
# If we get to here, then the user is allowed to login, so we're done!
|
||||
return (AUTH_OK, $userid);
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::Auth::Verify::LDAP - LDAP based authentication for Bugzilla
|
||||
|
||||
This is an L<authentication module|Bugzilla::Auth/"AUTHENTICATION"> for
|
||||
Bugzilla, which logs the user in using an LDAP directory.
|
||||
|
||||
=head1 DISCLAIMER
|
||||
|
||||
B<This module is experimental>. It is poorly documented, and not very flexible.
|
||||
Search L<http:E<sol>E<sol>bugzilla.mozilla.orgE<sol>> for a list of known LDAP bugs.
|
||||
|
||||
None of the core Bugzilla developers, nor any of the large installations, use
|
||||
this module, and so it has received less testing. (In fact, this iteration
|
||||
hasn't been tested at all)
|
||||
|
||||
Patches are accepted.
|
||||
|
||||
=head1 SEE ALSO
|
||||
|
||||
L<Bugzilla::Auth>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,855 +0,0 @@
|
||||
# -*- 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>,
|
||||
# Bryce Nesbitt <bryce-mozilla@nextbus.com>
|
||||
# Dan Mosedale <dmose@mozilla.org>
|
||||
# Alan Raetz <al_raetz@yahoo.com>
|
||||
# Jacob Steenhagen <jake@actex.net>
|
||||
# Matthew Tuck <matty@chariot.net.au>
|
||||
# Bradley Baetz <bbaetz@student.usyd.edu.au>
|
||||
# J. Paul Reed <preed@sigkill.com>
|
||||
# Gervase Markham <gerv@gerv.net>
|
||||
# Byron Jones <bugzilla@glob.com.au>
|
||||
|
||||
use strict;
|
||||
|
||||
package Bugzilla::BugMail;
|
||||
|
||||
use Bugzilla::User;
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::Config qw(:DEFAULT $datadir);
|
||||
use Bugzilla::Util;
|
||||
|
||||
use Date::Parse;
|
||||
use Date::Format;
|
||||
use Mail::Mailer;
|
||||
use Mail::Header;
|
||||
use MIME::Base64;
|
||||
use MIME::QuotedPrint;
|
||||
use MIME::Parser;
|
||||
use Mail::Address;
|
||||
|
||||
# We need these strings for the X-Bugzilla-Reasons header
|
||||
# Note: this hash uses "," rather than "=>" to avoid auto-quoting of the LHS.
|
||||
my %rel_names = (REL_ASSIGNEE , "AssignedTo",
|
||||
REL_REPORTER , "Reporter",
|
||||
REL_QA , "QAcontact",
|
||||
REL_CC , "CC",
|
||||
REL_VOTER , "Voter");
|
||||
|
||||
# This code is really ugly. It was a commandline interface, then it was moved.
|
||||
# This really needs to be cleaned at some point.
|
||||
|
||||
my %nomail;
|
||||
|
||||
my $sitespec = '@'.Param('urlbase');
|
||||
$sitespec =~ s/:\/\//\./; # Make the protocol look like part of the domain
|
||||
$sitespec =~ s/^([^:\/]+):(\d+)/$1/; # Remove a port number, to relocate
|
||||
if ($2) {
|
||||
$sitespec = "-$2$sitespec"; # Put the port number back in, before the '@'
|
||||
}
|
||||
|
||||
# This is run when we load the package
|
||||
if (open(NOMAIL, '<', "$datadir/nomail")) {
|
||||
while (<NOMAIL>) {
|
||||
$nomail{trim($_)} = 1;
|
||||
}
|
||||
close(NOMAIL);
|
||||
}
|
||||
|
||||
sub FormatTriple {
|
||||
my ($a, $b, $c) = (@_);
|
||||
$^A = "";
|
||||
my $temp = formline << 'END', $a, $b, $c;
|
||||
^>>>>>>>>>>>>>>>>>>|^<<<<<<<<<<<<<<<<<<<<<<<<<<<|^<<<<<<<<<<<<<<<<<<<<<<<<<<<~~
|
||||
END
|
||||
; # This semicolon appeases my emacs editor macros. :-)
|
||||
return $^A;
|
||||
}
|
||||
|
||||
sub FormatDouble {
|
||||
my ($a, $b) = (@_);
|
||||
$a .= ":";
|
||||
$^A = "";
|
||||
my $temp = formline << 'END', $a, $b;
|
||||
^>>>>>>>>>>>>>>>>>> ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<~~
|
||||
END
|
||||
; # This semicolon appeases my emacs editor macros. :-)
|
||||
return $^A;
|
||||
}
|
||||
|
||||
# This is a bit of a hack, basically keeping the old system()
|
||||
# cmd line interface. Should clean this up at some point.
|
||||
#
|
||||
# args: bug_id, and an optional hash ref which may have keys for:
|
||||
# changer, owner, qa, reporter, cc
|
||||
# Optional hash contains values of people which will be forced to those
|
||||
# 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) = (@_);
|
||||
|
||||
# This only works in a sub. Probably something to do with the
|
||||
# require abuse we do.
|
||||
&::GetVersionTable();
|
||||
|
||||
return ProcessOneBug($id, $forced);
|
||||
}
|
||||
|
||||
sub ProcessOneBug {
|
||||
my ($id, $forced) = (@_);
|
||||
|
||||
my @headerlist;
|
||||
my %defmailhead;
|
||||
my %fielddescription;
|
||||
|
||||
my $msg = "";
|
||||
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
my $fields = $dbh->selectall_arrayref('SELECT name, description, mailhead
|
||||
FROM fielddefs ORDER BY sortkey');
|
||||
|
||||
foreach my $fielddef (@$fields) {
|
||||
my ($field, $description, $mailhead) = @$fielddef;
|
||||
push(@headerlist, $field);
|
||||
$defmailhead{$field} = $mailhead;
|
||||
$fielddescription{$field} = $description;
|
||||
}
|
||||
|
||||
my %values = %{$dbh->selectrow_hashref(
|
||||
'SELECT ' . join(',', @::log_columns) . ',
|
||||
lastdiffed AS start, LOCALTIMESTAMP(0) AS end
|
||||
FROM bugs WHERE bug_id = ?',
|
||||
undef, $id)};
|
||||
|
||||
$values{product} = &::get_product_name($values{product_id});
|
||||
$values{component} = &::get_component_name($values{component_id});
|
||||
|
||||
my ($start, $end) = ($values{start}, $values{end});
|
||||
|
||||
# User IDs of people in various roles. More than one person can 'have' a
|
||||
# role, if the person in that role has changed, or people are watching.
|
||||
my $reporter = $values{'reporter'};
|
||||
my @assignees = ($values{'assigned_to'});
|
||||
my @qa_contacts = ($values{'qa_contact'});
|
||||
|
||||
my $cc_users = $dbh->selectall_arrayref(
|
||||
"SELECT cc.who, profiles.login_name
|
||||
FROM cc
|
||||
INNER JOIN profiles
|
||||
ON cc.who = profiles.userid
|
||||
WHERE bug_id = ?",
|
||||
undef, $id);
|
||||
|
||||
my (@ccs, @cc_login_names);
|
||||
foreach my $cc_user (@$cc_users) {
|
||||
my ($user_id, $user_login) = @$cc_user;
|
||||
push (@ccs, $user_id);
|
||||
push (@cc_login_names, $user_login);
|
||||
}
|
||||
|
||||
# Include the people passed in as being in particular roles.
|
||||
# This can include people who used to hold those roles.
|
||||
# At this point, we don't care if there are duplicates in these arrays.
|
||||
my $changer = $forced->{'changer'};
|
||||
if ($forced->{'owner'}) {
|
||||
push (@assignees, &::DBNameToIdAndCheck($forced->{'owner'}));
|
||||
}
|
||||
|
||||
if ($forced->{'qacontact'}) {
|
||||
push (@qa_contacts, &::DBNameToIdAndCheck($forced->{'qacontact'}));
|
||||
}
|
||||
|
||||
if ($forced->{'cc'}) {
|
||||
foreach my $cc (@{$forced->{'cc'}}) {
|
||||
push(@ccs, &::DBNameToIdAndCheck($cc));
|
||||
}
|
||||
}
|
||||
|
||||
# Convert to names, for later display
|
||||
$values{'assigned_to'} = &::DBID_to_name($values{'assigned_to'});
|
||||
$values{'reporter'} = &::DBID_to_name($values{'reporter'});
|
||||
if ($values{'qa_contact'}) {
|
||||
$values{'qa_contact'} = &::DBID_to_name($values{'qa_contact'});
|
||||
}
|
||||
$values{'cc'} = join(', ', @cc_login_names);
|
||||
$values{'estimated_time'} = format_time_decimal($values{'estimated_time'});
|
||||
|
||||
if ($values{'deadline'}) {
|
||||
$values{'deadline'} = time2str("%Y-%m-%d", str2time($values{'deadline'}));
|
||||
}
|
||||
|
||||
my $dependslist = $dbh->selectcol_arrayref(
|
||||
'SELECT dependson FROM dependencies
|
||||
WHERE blocked = ? ORDER BY dependson',
|
||||
undef, ($id));
|
||||
|
||||
$values{'dependson'} = join(",", @$dependslist);
|
||||
|
||||
my $blockedlist = $dbh->selectcol_arrayref(
|
||||
'SELECT blocked FROM dependencies
|
||||
WHERE dependson = ? ORDER BY blocked',
|
||||
undef, ($id));
|
||||
|
||||
$values{'blocked'} = join(",", @$blockedlist);
|
||||
|
||||
my @args = ($id);
|
||||
|
||||
# If lastdiffed is NULL, then we don't limit the search on time.
|
||||
my $when_restriction = '';
|
||||
if ($start) {
|
||||
$when_restriction = ' AND bug_when > ? AND bug_when <= ?';
|
||||
push @args, ($start, $end);
|
||||
}
|
||||
|
||||
my $diffs = $dbh->selectall_arrayref(
|
||||
"SELECT profiles.login_name, fielddefs.description,
|
||||
bugs_activity.bug_when, bugs_activity.removed,
|
||||
bugs_activity.added, bugs_activity.attach_id, fielddefs.name
|
||||
FROM bugs_activity
|
||||
INNER JOIN fielddefs
|
||||
ON fielddefs.fieldid = bugs_activity.fieldid
|
||||
INNER JOIN profiles
|
||||
ON profiles.userid = bugs_activity.who
|
||||
WHERE bugs_activity.bug_id = ?
|
||||
$when_restriction
|
||||
ORDER BY bugs_activity.bug_when", undef, @args);
|
||||
|
||||
my $difftext = "";
|
||||
my $diffheader = "";
|
||||
my @diffparts;
|
||||
my $lastwho = "";
|
||||
foreach my $ref (@$diffs) {
|
||||
my ($who, $what, $when, $old, $new, $attachid, $fieldname) = (@$ref);
|
||||
my $diffpart = {};
|
||||
if ($who ne $lastwho) {
|
||||
$lastwho = $who;
|
||||
$diffheader = "\n$who" . Param('emailsuffix') . " changed:\n\n";
|
||||
$diffheader .= FormatTriple("What ", "Removed", "Added");
|
||||
$diffheader .= ('-' x 76) . "\n";
|
||||
}
|
||||
$what =~ s/^(Attachment )?/Attachment #$attachid / if $attachid;
|
||||
if( $fieldname eq 'estimated_time' ||
|
||||
$fieldname eq 'remaining_time' ) {
|
||||
$old = format_time_decimal($old);
|
||||
$new = format_time_decimal($new);
|
||||
}
|
||||
if ($attachid) {
|
||||
($diffpart->{'isprivate'}) = $dbh->selectrow_array(
|
||||
'SELECT isprivate FROM attachments WHERE attach_id = ?',
|
||||
undef, ($attachid));
|
||||
}
|
||||
$difftext = FormatTriple($what, $old, $new);
|
||||
$diffpart->{'header'} = $diffheader;
|
||||
$diffpart->{'fieldname'} = $fieldname;
|
||||
$diffpart->{'text'} = $difftext;
|
||||
push(@diffparts, $diffpart);
|
||||
}
|
||||
|
||||
my $deptext = "";
|
||||
|
||||
my $dependency_diffs = $dbh->selectall_arrayref(
|
||||
"SELECT bugs_activity.bug_id, bugs.short_desc, fielddefs.name,
|
||||
bugs_activity.removed, bugs_activity.added
|
||||
FROM bugs_activity
|
||||
INNER JOIN bugs
|
||||
ON bugs.bug_id = bugs_activity.bug_id
|
||||
INNER JOIN dependencies
|
||||
ON bugs_activity.bug_id = dependencies.dependson
|
||||
INNER JOIN fielddefs
|
||||
ON fielddefs.fieldid = bugs_activity.fieldid
|
||||
WHERE dependencies.blocked = ?
|
||||
AND (fielddefs.name = 'bug_status'
|
||||
OR fielddefs.name = 'resolution')
|
||||
$when_restriction
|
||||
ORDER BY bugs_activity.bug_when, bugs.bug_id", undef, @args);
|
||||
|
||||
my $thisdiff = "";
|
||||
my $lastbug = "";
|
||||
my $interestingchange = 0;
|
||||
my @depbugs;
|
||||
foreach my $dependency_diff (@$dependency_diffs) {
|
||||
my ($depbug, $summary, $what, $old, $new) = @$dependency_diff;
|
||||
|
||||
if ($depbug ne $lastbug) {
|
||||
if ($interestingchange) {
|
||||
$deptext .= $thisdiff;
|
||||
}
|
||||
$lastbug = $depbug;
|
||||
my $urlbase = Param("urlbase");
|
||||
$thisdiff =
|
||||
"\nBug $id depends on bug $depbug, which changed state.\n\n" .
|
||||
"Bug $depbug Summary: $summary\n" .
|
||||
"${urlbase}show_bug.cgi?id=$depbug\n\n";
|
||||
$thisdiff .= FormatTriple("What ", "Old Value", "New Value");
|
||||
$thisdiff .= ('-' x 76) . "\n";
|
||||
$interestingchange = 0;
|
||||
}
|
||||
$thisdiff .= FormatTriple($fielddescription{$what}, $old, $new);
|
||||
if ($what eq 'bug_status' && &::IsOpenedState($old) ne &::IsOpenedState($new)) {
|
||||
$interestingchange = 1;
|
||||
}
|
||||
|
||||
push(@depbugs, $depbug);
|
||||
}
|
||||
|
||||
if ($interestingchange) {
|
||||
$deptext .= $thisdiff;
|
||||
}
|
||||
|
||||
$deptext = trim($deptext);
|
||||
|
||||
if ($deptext) {
|
||||
my $diffpart = {};
|
||||
$diffpart->{'text'} = "\n" . trim("\n\n" . $deptext);
|
||||
push(@diffparts, $diffpart);
|
||||
}
|
||||
|
||||
|
||||
my ($newcomments, $anyprivate) = get_comments_by_bug($id, $start, $end);
|
||||
|
||||
###########################################################################
|
||||
# Start of email filtering code
|
||||
###########################################################################
|
||||
|
||||
# A user_id => roles hash to keep track of people.
|
||||
my %recipients;
|
||||
|
||||
# Now we work out all the people involved with this bug, and note all of
|
||||
# the relationships in a hash. The keys are userids, the values are an
|
||||
# array of role constants.
|
||||
|
||||
# Voters
|
||||
my $voters = $dbh->selectcol_arrayref(
|
||||
"SELECT who FROM votes WHERE bug_id = ?", undef, ($id));
|
||||
|
||||
push(@{$recipients{$_}}, REL_VOTER) foreach (@$voters);
|
||||
|
||||
# CCs
|
||||
push(@{$recipients{$_}}, REL_CC) foreach (@ccs);
|
||||
|
||||
# Reporter (there's only ever one)
|
||||
push(@{$recipients{$reporter}}, REL_REPORTER);
|
||||
|
||||
# QA Contact
|
||||
if (Param('useqacontact')) {
|
||||
foreach (@qa_contacts) {
|
||||
# QA Contact can be blank; ignore it if so.
|
||||
push(@{$recipients{$_}}, REL_QA) if $_;
|
||||
}
|
||||
}
|
||||
|
||||
# Assignee
|
||||
push(@{$recipients{$_}}, REL_ASSIGNEE) foreach (@assignees);
|
||||
|
||||
# The last relevant set of people are those who are being removed from
|
||||
# their roles in this change. We get their names out of the diffs.
|
||||
foreach my $ref (@$diffs) {
|
||||
my ($who, $what, $when, $old, $new) = (@$ref);
|
||||
if ($old) {
|
||||
# You can't stop being the reporter, and mail isn't sent if you
|
||||
# remove your vote.
|
||||
# Ignore people whose user account has been deleted or renamed.
|
||||
if ($what eq "CC") {
|
||||
foreach my $cc_user (split(/[\s,]+/, $old)) {
|
||||
my $uid = login_to_id($cc_user);
|
||||
push(@{$recipients{$uid}}, REL_CC) if $uid;
|
||||
}
|
||||
}
|
||||
elsif ($what eq "QAContact") {
|
||||
my $uid = login_to_id($old);
|
||||
push(@{$recipients{$uid}}, REL_QA) if $uid;
|
||||
}
|
||||
elsif ($what eq "AssignedTo") {
|
||||
my $uid = login_to_id($old);
|
||||
push(@{$recipients{$uid}}, REL_ASSIGNEE) if $uid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Param("supportwatchers")) {
|
||||
# Find all those user-watching anyone on the current list, who is not
|
||||
# on it already themselves.
|
||||
my $involved = join(",", keys %recipients);
|
||||
|
||||
my $userwatchers =
|
||||
$dbh->selectall_arrayref("SELECT watcher, watched FROM watch
|
||||
WHERE watched IN ($involved)
|
||||
AND watcher NOT IN ($involved)");
|
||||
|
||||
# Mark these people as having the role of the person they are watching
|
||||
foreach my $watch (@$userwatchers) {
|
||||
$recipients{$watch->[0]} = $recipients{$watch->[1]};
|
||||
}
|
||||
}
|
||||
|
||||
# We now have a complete set of all the users, and their relationships to
|
||||
# the bug in question. However, we are not necessarily going to mail them
|
||||
# all - there are preferences, permissions checks and all sorts to do yet.
|
||||
my @sent;
|
||||
my @excluded;
|
||||
|
||||
foreach my $user_id (keys %recipients) {
|
||||
my @rels_which_want;
|
||||
my $sent_mail = 0;
|
||||
|
||||
my $user = new Bugzilla::User($user_id);
|
||||
# Deleted users must be excluded.
|
||||
next unless $user;
|
||||
|
||||
if ($user->can_see_bug($id))
|
||||
{
|
||||
# Go through each role the user has and see if they want mail in
|
||||
# that role.
|
||||
foreach my $relationship (@{$recipients{$user_id}}) {
|
||||
if ($user->wants_bug_mail($id,
|
||||
$relationship,
|
||||
$diffs,
|
||||
$newcomments,
|
||||
$changer,
|
||||
!$start))
|
||||
{
|
||||
push(@rels_which_want, $relationship);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (scalar(@rels_which_want)) {
|
||||
# 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 (Param("insidergroup") &&
|
||||
($anyprivate != 0) &&
|
||||
(!$user->groups->{Param("insidergroup")}));
|
||||
|
||||
# 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
|
||||
# confidential bugs.
|
||||
my $dep_ok = 1;
|
||||
foreach my $dep_id (@depbugs) {
|
||||
if (!$user->can_see_bug($dep_id)) {
|
||||
$dep_ok = 0;
|
||||
last;
|
||||
}
|
||||
}
|
||||
|
||||
# Make sure the user isn't in the nomail list, and the insider and
|
||||
# dep checks passed.
|
||||
if ((!$nomail{$user->login}) &&
|
||||
$insider_ok &&
|
||||
$dep_ok)
|
||||
{
|
||||
# OK, OK, if we must. Email the user.
|
||||
$sent_mail = sendMail($user,
|
||||
\@headerlist,
|
||||
\@rels_which_want,
|
||||
\%values,
|
||||
\%defmailhead,
|
||||
\%fielddescription,
|
||||
\@diffparts,
|
||||
$newcomments,
|
||||
$anyprivate,
|
||||
$start,
|
||||
$id);
|
||||
}
|
||||
}
|
||||
|
||||
if ($sent_mail) {
|
||||
push(@sent, $user->login);
|
||||
}
|
||||
else {
|
||||
push(@excluded, $user->login);
|
||||
}
|
||||
}
|
||||
|
||||
$dbh->do('UPDATE bugs SET lastdiffed = ? WHERE bug_id = ?',
|
||||
undef, ($end, $id));
|
||||
|
||||
return {'sent' => \@sent, 'excluded' => \@excluded};
|
||||
}
|
||||
|
||||
sub sendMail {
|
||||
my ($user, $hlRef, $relRef, $valueRef, $dmhRef, $fdRef,
|
||||
$diffRef, $newcomments, $anyprivate, $start,
|
||||
$id) = @_;
|
||||
|
||||
my %values = %$valueRef;
|
||||
my @headerlist = @$hlRef;
|
||||
my %mailhead = %$dmhRef;
|
||||
my %fielddescription = %$fdRef;
|
||||
my @diffparts = @$diffRef;
|
||||
my $head = "";
|
||||
|
||||
foreach my $f (@headerlist) {
|
||||
if ($mailhead{$f}) {
|
||||
my $value = $values{$f};
|
||||
# If there isn't anything to show, don't include this header
|
||||
if (! $value) {
|
||||
next;
|
||||
}
|
||||
# Only send estimated_time if it is enabled and the user is in the group
|
||||
if (($f ne 'estimated_time' && $f ne 'deadline') ||
|
||||
$user->groups->{Param('timetrackinggroup')}) {
|
||||
|
||||
my $desc = $fielddescription{$f};
|
||||
$head .= FormatDouble($desc, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Build difftext (the actions) by verifying the user should see them
|
||||
my $difftext = "";
|
||||
my $diffheader = "";
|
||||
my $add_diff;
|
||||
|
||||
foreach my $diff (@diffparts) {
|
||||
$add_diff = 0;
|
||||
|
||||
if (exists($diff->{'fieldname'}) &&
|
||||
($diff->{'fieldname'} eq 'estimated_time' ||
|
||||
$diff->{'fieldname'} eq 'remaining_time' ||
|
||||
$diff->{'fieldname'} eq 'work_time' ||
|
||||
$diff->{'fieldname'} eq 'deadline')){
|
||||
if ($user->groups->{Param("timetrackinggroup")}) {
|
||||
$add_diff = 1;
|
||||
}
|
||||
} elsif (($diff->{'isprivate'})
|
||||
&& Param('insidergroup')
|
||||
&& !($user->groups->{Param('insidergroup')})
|
||||
) {
|
||||
$add_diff = 0;
|
||||
} else {
|
||||
$add_diff = 1;
|
||||
}
|
||||
|
||||
if ($add_diff) {
|
||||
if (exists($diff->{'header'}) &&
|
||||
($diffheader ne $diff->{'header'})) {
|
||||
$diffheader = $diff->{'header'};
|
||||
$difftext .= $diffheader;
|
||||
}
|
||||
$difftext .= $diff->{'text'};
|
||||
}
|
||||
}
|
||||
|
||||
if ($difftext eq "" && $newcomments eq "") {
|
||||
# Whoops, no differences!
|
||||
return 0;
|
||||
}
|
||||
|
||||
# XXX: This needs making localisable, probably by passing the role to
|
||||
# the email template and letting it choose the text.
|
||||
my $reasonsbody = "------- You are receiving this mail because: -------\n";
|
||||
|
||||
foreach my $relationship (@$relRef) {
|
||||
if ($relationship == REL_ASSIGNEE) {
|
||||
$reasonsbody .= "You are the assignee for the bug, or are watching the assignee.\n";
|
||||
} elsif ($relationship == REL_REPORTER) {
|
||||
$reasonsbody .= "You reported the bug, or are watching the reporter.\n";
|
||||
} elsif ($relationship == REL_QA) {
|
||||
$reasonsbody .= "You are the QA contact for the bug, or are watching the QA contact.\n";
|
||||
} elsif ($relationship == REL_CC) {
|
||||
$reasonsbody .= "You are on the CC list for the bug, or are watching someone who is.\n";
|
||||
} elsif ($relationship == REL_VOTER) {
|
||||
$reasonsbody .= "You are a voter for the bug, or are watching someone who is.\n";
|
||||
}
|
||||
}
|
||||
|
||||
my $isnew = !$start;
|
||||
|
||||
my %substs;
|
||||
|
||||
# If an attachment was created, then add an URL. (Note: the 'g'lobal
|
||||
# replace should work with comments with multiple attachments.)
|
||||
|
||||
if ( $newcomments =~ /Created an attachment \(/ ) {
|
||||
|
||||
my $showattachurlbase =
|
||||
Param('urlbase') . "attachment.cgi?id=";
|
||||
|
||||
$newcomments =~ s/(Created an attachment \(id=([0-9]+)\))/$1\n --> \(${showattachurlbase}$2&action=view\)/g;
|
||||
}
|
||||
|
||||
$substs{"neworchanged"} = $isnew ? 'New: ' : '';
|
||||
$substs{"to"} = $user->email;
|
||||
$substs{"cc"} = '';
|
||||
$substs{"bugid"} = $id;
|
||||
if ($isnew) {
|
||||
$substs{"diffs"} = $head . "\n\n" . $newcomments;
|
||||
} else {
|
||||
$substs{"diffs"} = $difftext . "\n\n" . $newcomments;
|
||||
}
|
||||
$substs{"product"} = $values{'product'};
|
||||
$substs{"component"} = $values{'component'};
|
||||
$substs{"keywords"} = $values{'keywords'};
|
||||
$substs{"severity"} = $values{'bug_severity'};
|
||||
$substs{"summary"} = $values{'short_desc'};
|
||||
$substs{"reasonsheader"} = join(" ", map { $rel_names{$_} } @$relRef);
|
||||
$substs{"reasonsbody"} = $reasonsbody;
|
||||
$substs{"space"} = " ";
|
||||
if ($isnew) {
|
||||
$substs{'threadingmarker'} = "Message-ID: <bug-$id-" .
|
||||
$user->id . "$sitespec>";
|
||||
} else {
|
||||
$substs{'threadingmarker'} = "In-Reply-To: <bug-$id-" .
|
||||
$user->id . "$sitespec>";
|
||||
}
|
||||
|
||||
my $template = Param("newchangedmail");
|
||||
|
||||
my $msg = perform_substs($template, \%substs);
|
||||
|
||||
MessageToMTA($msg);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
sub MessageToMTA {
|
||||
my ($msg) = (@_);
|
||||
return if (Param('mail_delivery_method') eq "none");
|
||||
|
||||
my ($header, $body) = $msg =~ /(.*?\n)\n(.*)/s ? ($1, $2) : ('', $msg);
|
||||
my $headers;
|
||||
|
||||
if (Param('utf8') and (!is_7bit_clean($header) or !is_7bit_clean($body))) {
|
||||
($headers, $body) = encode_message($msg);
|
||||
} else {
|
||||
my @header_lines = split(/\n/, $header);
|
||||
$headers = new Mail::Header \@header_lines, Modify => 0;
|
||||
}
|
||||
|
||||
# Use trim to remove any whitespace (incl. newlines)
|
||||
my $from = trim($headers->get('from'));
|
||||
|
||||
if (Param("mail_delivery_method") eq "sendmail" && $^O =~ /MSWin32/i) {
|
||||
my $cmd = '|' . SENDMAIL_EXE . ' -t -i';
|
||||
if ($from) {
|
||||
# We're on Windows, thus no danger of command injection
|
||||
# via $from. In other words, it is safe to embed $from.
|
||||
$cmd .= qq# -f"$from"#;
|
||||
}
|
||||
open(SENDMAIL, $cmd) ||
|
||||
die "Failed to execute " . SENDMAIL_EXE . ": $!\n";
|
||||
print SENDMAIL $headers->as_string;
|
||||
print SENDMAIL "\n";
|
||||
print SENDMAIL $body;
|
||||
close SENDMAIL;
|
||||
return;
|
||||
}
|
||||
|
||||
my @args;
|
||||
if (Param("mail_delivery_method") eq "sendmail") {
|
||||
push @args, "-i";
|
||||
if ($from) {
|
||||
push(@args, "-f$from");
|
||||
}
|
||||
}
|
||||
if (Param("mail_delivery_method") eq "sendmail" && !Param("sendmailnow")) {
|
||||
push @args, "-ODeliveryMode=deferred";
|
||||
}
|
||||
if (Param("mail_delivery_method") eq "smtp") {
|
||||
push @args, Server => Param("smtpserver");
|
||||
if ($from) {
|
||||
$ENV{'MAILADDRESS'} = $from;
|
||||
}
|
||||
}
|
||||
my $mailer = new Mail::Mailer Param("mail_delivery_method"), @args;
|
||||
if (Param("mail_delivery_method") eq "testfile") {
|
||||
$Mail::Mailer::testfile::config{outfile} = "$datadir/mailer.testfile";
|
||||
}
|
||||
|
||||
$mailer->open($headers->header_hashref);
|
||||
print $mailer $body;
|
||||
$mailer->close;
|
||||
}
|
||||
|
||||
sub encode_qp_words {
|
||||
my ($line) = (@_);
|
||||
my @encoded;
|
||||
foreach my $word (split / /, $line) {
|
||||
if (!is_7bit_clean($word)) {
|
||||
push @encoded, '=?UTF-8?Q?_' . encode_qp($word, '') . '?=';
|
||||
} else {
|
||||
push @encoded, $word;
|
||||
}
|
||||
}
|
||||
return join(' ', @encoded);
|
||||
}
|
||||
|
||||
sub encode_message {
|
||||
my ($msg) = @_;
|
||||
|
||||
my $parser = MIME::Parser->new;
|
||||
$parser->output_to_core(1);
|
||||
$parser->tmp_to_core(1);
|
||||
my $entity = $parser->parse_data($msg);
|
||||
$entity = encode_message_entity($entity);
|
||||
|
||||
my @header_lines = split(/\n/, $entity->header_as_string);
|
||||
my $head = new Mail::Header \@header_lines, Modify => 0;
|
||||
|
||||
my $body = $entity->body_as_string;
|
||||
|
||||
return ($head, $body);
|
||||
}
|
||||
|
||||
sub encode_message_entity {
|
||||
my ($entity) = @_;
|
||||
|
||||
my $head = $entity->head;
|
||||
|
||||
# encode the subject
|
||||
|
||||
my $subject = $head->get('subject');
|
||||
if (defined $subject && !is_7bit_clean($subject)) {
|
||||
$subject =~ s/[\r\n]+$//;
|
||||
$head->replace('subject', encode_qp_words($subject));
|
||||
}
|
||||
|
||||
# encode addresses
|
||||
|
||||
foreach my $field (qw(from to cc reply-to sender errors-to)) {
|
||||
my $high = $head->count($field) - 1;
|
||||
foreach my $index (0..$high) {
|
||||
my $value = $head->get($field, $index);
|
||||
my @addresses;
|
||||
my $changed = 0;
|
||||
foreach my $addr (Mail::Address->parse($value)) {
|
||||
my $phrase = $addr->phrase;
|
||||
if (is_7bit_clean($phrase)) {
|
||||
push @addresses, $addr->format;
|
||||
} else {
|
||||
push @addresses, encode_qp_phrase($phrase) .
|
||||
' <' . $addr->address . '>';
|
||||
$changed = 1;
|
||||
}
|
||||
}
|
||||
$changed && $head->replace($field, join(', ', @addresses), $index);
|
||||
}
|
||||
}
|
||||
|
||||
# process the body
|
||||
|
||||
if (scalar($entity->parts)) {
|
||||
my $newparts = [];
|
||||
foreach my $part ($entity->parts) {
|
||||
my $newpart = encode_message_entity($part);
|
||||
push @$newparts, $newpart;
|
||||
}
|
||||
$entity->parts($newparts);
|
||||
}
|
||||
else {
|
||||
# Extract the body from the entity, for examination
|
||||
# At this point, we can rely on MIME::Tools to do our encoding for us!
|
||||
my $bodyhandle = $entity->bodyhandle;
|
||||
my $body = $bodyhandle->as_string;
|
||||
if (!is_7bit_clean($body)) {
|
||||
# count number of 7-bit chars, and use quoted-printable if more
|
||||
# than half the message is 7-bit clean
|
||||
my $count = ($body =~ tr/\x20-\x7E\x0A\x0D//);
|
||||
if ($count > length($body) / 2) {
|
||||
$head->mime_attr('Content-Transfer-Encoding' => 'quoted-printable');
|
||||
} else {
|
||||
$head->mime_attr('Content-Transfer-Encoding' => 'base64');
|
||||
}
|
||||
}
|
||||
|
||||
# Set the content/type and charset of the part, if not set
|
||||
$head->mime_attr('Content-Type' => 'text/plain')
|
||||
unless defined $head->mime_attr('content-type');
|
||||
$head->mime_attr('Content-Type.charset' => 'UTF-8');
|
||||
}
|
||||
|
||||
$head->mime_attr('MIME-Version' => '1.0');
|
||||
$head->fold(75);
|
||||
return $entity;
|
||||
}
|
||||
|
||||
# Send the login name and password of the newly created account to the user.
|
||||
sub MailPassword {
|
||||
my ($login, $password) = (@_);
|
||||
my $template = Param("passwordmail");
|
||||
my $msg = perform_substs($template,
|
||||
{"mailaddress" => $login . Param('emailsuffix'),
|
||||
"login" => $login,
|
||||
"password" => $password});
|
||||
MessageToMTA($msg);
|
||||
}
|
||||
|
||||
# Get bug comments for the given period and format them to be used in emails.
|
||||
sub get_comments_by_bug {
|
||||
my ($id, $start, $end) = @_;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
my $result = "";
|
||||
my $count = 0;
|
||||
my $anyprivate = 0;
|
||||
|
||||
my $query = 'SELECT profiles.login_name, ' .
|
||||
$dbh->sql_date_format('longdescs.bug_when', '%Y.%m.%d %H:%i') . ',
|
||||
longdescs.thetext, longdescs.isprivate,
|
||||
longdescs.already_wrapped
|
||||
FROM longdescs
|
||||
INNER JOIN profiles
|
||||
ON profiles.userid = longdescs.who
|
||||
WHERE longdescs.bug_id = ? ';
|
||||
|
||||
my @args = ($id);
|
||||
|
||||
# $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));
|
||||
|
||||
$query .= ' AND longdescs.bug_when > ?
|
||||
AND longdescs.bug_when <= ? ';
|
||||
push @args, ($start, $end);
|
||||
}
|
||||
|
||||
$query .= ' ORDER BY longdescs.bug_when';
|
||||
my $comments = $dbh->selectall_arrayref($query, undef, @args);
|
||||
|
||||
foreach (@$comments) {
|
||||
my ($who, $when, $text, $isprivate, $already_wrapped) = @$_;
|
||||
if ($count) {
|
||||
$result .= "\n\n------- Comment #$count from $who" .
|
||||
Param('emailsuffix'). " " . format_time($when) .
|
||||
" -------\n";
|
||||
}
|
||||
if ($isprivate > 0 && Param('insidergroup')) {
|
||||
$anyprivate = 1;
|
||||
}
|
||||
$result .= ($already_wrapped ? $text : wrap_comment($text));
|
||||
$count++;
|
||||
}
|
||||
return ($result, $anyprivate);
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -1,326 +0,0 @@
|
||||
# -*- 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): Bradley Baetz <bbaetz@student.usyd.edu.au>
|
||||
# Byron Jones <bugzilla@glob.com.au>
|
||||
# Marc Schumann <wurblzap@gmail.com>
|
||||
|
||||
use strict;
|
||||
|
||||
package Bugzilla::CGI;
|
||||
|
||||
BEGIN {
|
||||
if ($^O =~ /MSWin32/i) {
|
||||
# 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 base qw(CGI);
|
||||
|
||||
use Bugzilla::Error;
|
||||
use Bugzilla::Util;
|
||||
use Bugzilla::Config;
|
||||
|
||||
# We need to disable output buffering - see bug 179174
|
||||
$| = 1;
|
||||
|
||||
# CGI.pm uses AUTOLOAD, but explicitly defines a DESTROY sub.
|
||||
# We need to do so, too, otherwise perl dies when the object is destroyed
|
||||
# and we don't have a DESTROY method (because CGI.pm's AUTOLOAD will |die|
|
||||
# on getting an unknown sub to try to call)
|
||||
sub DESTROY {
|
||||
my $self = shift;
|
||||
$self->SUPER::DESTROY(@_);
|
||||
};
|
||||
|
||||
sub new {
|
||||
my ($invocant, @args) = @_;
|
||||
my $class = ref($invocant) || $invocant;
|
||||
|
||||
my $self = $class->SUPER::new(@args);
|
||||
|
||||
# This happens here so that command-line scripts don't spit out
|
||||
# their errors in HTML format.
|
||||
require CGI::Carp;
|
||||
import CGI::Carp qw(fatalsToBrowser);
|
||||
|
||||
# Make sure our outgoing cookie list is empty on each invocation
|
||||
$self->{Bugzilla_cookie_list} = [];
|
||||
|
||||
# Send appropriate charset
|
||||
$self->charset(Param('utf8') ? 'UTF-8' : '');
|
||||
|
||||
# Redirect to SSL if required
|
||||
if (Param('sslbase') ne '' and Param('ssl') eq 'always' and i_am_cgi()) {
|
||||
$self->require_https(Param('sslbase'));
|
||||
}
|
||||
|
||||
# Check for errors
|
||||
# All of the Bugzilla code wants to do this, so do it here instead of
|
||||
# in each script
|
||||
|
||||
my $err = $self->cgi_error;
|
||||
|
||||
if ($err) {
|
||||
# XXX - under mod_perl we can use the request object to
|
||||
# enable the apache ErrorDocument stuff, which is localisable
|
||||
# (and localised by default under apache2).
|
||||
# This doesn't appear to be possible under mod_cgi.
|
||||
# Under mod_perl v2, though, this happens automatically, and the
|
||||
# message body is ignored.
|
||||
|
||||
# Note that this error block is only triggered by CGI.pm for malformed
|
||||
# multipart requests, and so should never happen unless there is a
|
||||
# browser bug.
|
||||
|
||||
print $self->header(-status => $err);
|
||||
|
||||
# ThrowCodeError wants to print the header, so it grabs Bugzilla->cgi
|
||||
# which creates a new Bugzilla::CGI object, which fails again, which
|
||||
# ends up here, and calls ThrowCodeError, and then recurses forever.
|
||||
# So don't use it.
|
||||
# In fact, we can't use templates at all, because we need a CGI object
|
||||
# to determine the template lang as well as the current url (from the
|
||||
# template)
|
||||
# Since this is an internal error which indicates a severe browser bug,
|
||||
# just die.
|
||||
die "CGI parsing error: $err";
|
||||
}
|
||||
|
||||
return $self;
|
||||
}
|
||||
|
||||
# We want this sorted plus the ability to exclude certain params
|
||||
sub canonicalise_query {
|
||||
my ($self, @exclude) = @_;
|
||||
|
||||
# Reconstruct the URL by concatenating the sorted param=value pairs
|
||||
my @parameters;
|
||||
foreach my $key (sort($self->param())) {
|
||||
# Leave this key out if it's in the exclude list
|
||||
next if lsearch(\@exclude, $key) != -1;
|
||||
|
||||
my $esc_key = url_quote($key);
|
||||
|
||||
foreach my $value ($self->param($key)) {
|
||||
if (defined($value)) {
|
||||
my $esc_value = url_quote($value);
|
||||
|
||||
push(@parameters, "$esc_key=$esc_value");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return join("&", @parameters);
|
||||
}
|
||||
|
||||
# Overwrite to ensure nph doesn't get set, and unset HEADERS_ONCE
|
||||
sub multipart_init {
|
||||
my $self = shift;
|
||||
|
||||
# Keys are case-insensitive, map to lowercase
|
||||
my %args = @_;
|
||||
my %param;
|
||||
foreach my $key (keys %args) {
|
||||
$param{lc $key} = $args{$key};
|
||||
}
|
||||
|
||||
# Set the MIME boundary and content-type
|
||||
my $boundary = $param{'-boundary'} || '------- =_aaaaaaaaaa0';
|
||||
delete $param{'-boundary'};
|
||||
$self->{'separator'} = "\r\n--$boundary\r\n";
|
||||
$self->{'final_separator'} = "\r\n--$boundary--\r\n";
|
||||
$param{'-type'} = SERVER_PUSH($boundary);
|
||||
|
||||
# Note: CGI.pm::multipart_init up to v3.04 explicitly set nph to 0
|
||||
# CGI.pm::multipart_init v3.05 explicitly sets nph to 1
|
||||
# CGI.pm's header() sets nph according to a param or $CGI::NPH, which
|
||||
# is the desired behaviour.
|
||||
|
||||
# Allow multiple calls to $cgi->header()
|
||||
$CGI::HEADERS_ONCE = 0;
|
||||
|
||||
return $self->header(
|
||||
%param,
|
||||
) . "WARNING: YOUR BROWSER DOESN'T SUPPORT THIS SERVER-PUSH TECHNOLOGY." . $self->multipart_end;
|
||||
}
|
||||
|
||||
# Override header so we can add the cookies in
|
||||
sub header {
|
||||
my $self = shift;
|
||||
|
||||
# Add the cookies in if we have any
|
||||
if (scalar(@{$self->{Bugzilla_cookie_list}})) {
|
||||
if (scalar(@_) == 1) {
|
||||
# if there's only one parameter, then it's a Content-Type.
|
||||
# Since we're adding parameters we have to name it.
|
||||
unshift(@_, '-type' => shift(@_));
|
||||
}
|
||||
unshift(@_, '-cookie' => $self->{Bugzilla_cookie_list});
|
||||
}
|
||||
|
||||
return $self->SUPER::header(@_) || "";
|
||||
}
|
||||
|
||||
# Override multipart_start to ensure our cookies are added and avoid bad quoting of
|
||||
# CGI's multipart_start (bug 275108)
|
||||
sub multipart_start {
|
||||
my $self = shift;
|
||||
return $self->header(@_);
|
||||
}
|
||||
|
||||
# The various parts of Bugzilla which create cookies don't want to have to
|
||||
# pass them around to all of the callers. Instead, store them locally here,
|
||||
# and then output as required from |header|.
|
||||
sub send_cookie {
|
||||
my $self = shift;
|
||||
|
||||
# Move the param list into a hash for easier handling.
|
||||
my %paramhash;
|
||||
my @paramlist;
|
||||
my ($key, $value);
|
||||
while ($key = shift) {
|
||||
$value = shift;
|
||||
$paramhash{$key} = $value;
|
||||
}
|
||||
|
||||
# Complain if -value is not given or empty (bug 268146).
|
||||
if (!exists($paramhash{'-value'}) || !$paramhash{'-value'}) {
|
||||
ThrowCodeError('cookies_need_value');
|
||||
}
|
||||
|
||||
# Add the default path and the domain in.
|
||||
$paramhash{'-path'} = Param('cookiepath');
|
||||
$paramhash{'-domain'} = Param('cookiedomain') if Param('cookiedomain');
|
||||
|
||||
# Move the param list back into an array for the call to cookie().
|
||||
foreach (keys(%paramhash)) {
|
||||
unshift(@paramlist, $_ => $paramhash{$_});
|
||||
}
|
||||
|
||||
push(@{$self->{'Bugzilla_cookie_list'}}, $self->cookie(@paramlist));
|
||||
}
|
||||
|
||||
# Cookies are removed by setting an expiry date in the past.
|
||||
# This method is a send_cookie wrapper doing exactly this.
|
||||
sub remove_cookie {
|
||||
my $self = shift;
|
||||
my ($cookiename) = (@_);
|
||||
|
||||
# Expire the cookie, giving a non-empty dummy value (bug 268146).
|
||||
$self->send_cookie('-name' => $cookiename,
|
||||
'-expires' => 'Tue, 15-Sep-1998 21:49:00 GMT',
|
||||
'-value' => 'X');
|
||||
}
|
||||
|
||||
# Redirect to https if required
|
||||
sub require_https {
|
||||
my $self = shift;
|
||||
if ($self->protocol ne 'https') {
|
||||
my $url = shift;
|
||||
if (defined $url) {
|
||||
$url .= $self->url('-path_info' => 1, '-query' => 1, '-relative' => 1);
|
||||
} else {
|
||||
$url = $self->self_url;
|
||||
$url =~ s/^http:/https:/i;
|
||||
}
|
||||
print $self->redirect(-location => $url);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::CGI - CGI handling for Bugzilla
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
use Bugzilla::CGI;
|
||||
|
||||
my $cgi = new Bugzilla::CGI();
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
This package inherits from the standard CGI module, to provide additional
|
||||
Bugzilla-specific functionality. In general, see L<the CGI.pm docs|CGI> for
|
||||
documention.
|
||||
|
||||
=head1 CHANGES FROM L<CGI.PM|CGI>
|
||||
|
||||
Bugzilla::CGI has some differences from L<CGI.pm|CGI>.
|
||||
|
||||
=over 4
|
||||
|
||||
=item C<cgi_error> is automatically checked
|
||||
|
||||
After creating the CGI object, C<Bugzilla::CGI> automatically checks
|
||||
I<cgi_error>, and throws a CodeError if a problem is detected.
|
||||
|
||||
=back
|
||||
|
||||
=head1 ADDITIONAL FUNCTIONS
|
||||
|
||||
I<Bugzilla::CGI> also includes additional functions.
|
||||
|
||||
=over 4
|
||||
|
||||
=item C<canonicalise_query(@exclude)>
|
||||
|
||||
This returns a sorted string of the parameters, suitable for use in a url.
|
||||
Values in C<@exclude> are not included in the result.
|
||||
|
||||
=item C<send_cookie>
|
||||
|
||||
This routine is identical to the cookie generation part of CGI.pm's C<cookie>
|
||||
routine, except that it knows about Bugzilla's cookie_path and cookie_domain
|
||||
parameters and takes them into account if necessary.
|
||||
This should be used by all Bugzilla code (instead of C<cookie> or the C<-cookie>
|
||||
argument to C<header>), so that under mod_perl the headers can be sent
|
||||
correctly, using C<print> or the mod_perl APIs as appropriate.
|
||||
|
||||
To remove (expire) a cookie, use C<remove_cookie>.
|
||||
|
||||
=item C<remove_cookie>
|
||||
|
||||
This is a wrapper around send_cookie, setting an expiry date in the past,
|
||||
effectively removing the cookie.
|
||||
|
||||
As its only argument, it takes the name of the cookie to expire.
|
||||
|
||||
=item C<require_https($baseurl)>
|
||||
|
||||
This routine checks if the current page is being served over https, and
|
||||
redirects to the https protocol if required, retaining QUERY_STRING.
|
||||
|
||||
It takes an option argument which will be used as the base URL. If $baseurl
|
||||
is not provided, the current URL is used.
|
||||
|
||||
=back
|
||||
|
||||
=head1 SEE ALSO
|
||||
|
||||
L<CGI|CGI>, L<CGI::Cookie|CGI::Cookie>
|
||||
@@ -1,444 +0,0 @@
|
||||
# -*- 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): Gervase Markham <gerv@gerv.net>
|
||||
# Albert Ting <altlst@sonic.net>
|
||||
# A. Karl Kornel <karl@kornel.name>
|
||||
|
||||
use strict;
|
||||
use lib ".";
|
||||
|
||||
# This module represents a chart.
|
||||
#
|
||||
# Note that it is perfectly legal for the 'lines' member variable of this
|
||||
# class (which is an array of Bugzilla::Series objects) to have empty members
|
||||
# in it. If this is true, the 'labels' array will also have empty members at
|
||||
# the same points.
|
||||
package Bugzilla::Chart;
|
||||
|
||||
use Bugzilla::Util;
|
||||
use Bugzilla::Series;
|
||||
|
||||
use Date::Format;
|
||||
|
||||
sub new {
|
||||
my $invocant = shift;
|
||||
my $class = ref($invocant) || $invocant;
|
||||
|
||||
# Create a ref to an empty hash and bless it
|
||||
my $self = {};
|
||||
bless($self, $class);
|
||||
|
||||
if ($#_ == 0) {
|
||||
# Construct from a CGI object.
|
||||
$self->init($_[0]);
|
||||
}
|
||||
else {
|
||||
die("CGI object not passed in - invalid number of args \($#_\)($_)");
|
||||
}
|
||||
|
||||
return $self;
|
||||
}
|
||||
|
||||
sub init {
|
||||
my $self = shift;
|
||||
my $cgi = shift;
|
||||
|
||||
# The data structure is a list of lists (lines) of Series objects.
|
||||
# There is a separate list for the labels.
|
||||
#
|
||||
# The URL encoding is:
|
||||
# line0=67&line0=73&line1=81&line2=67...
|
||||
# &label0=B+/+R+/+NEW&label1=...
|
||||
# &select0=1&select3=1...
|
||||
# &cumulate=1&datefrom=2002-02-03&dateto=2002-04-04&ctype=html...
|
||||
# >=1&labelgt=Grand+Total
|
||||
foreach my $param ($cgi->param()) {
|
||||
# Store all the lines
|
||||
if ($param =~ /^line(\d+)$/) {
|
||||
foreach my $series_id ($cgi->param($param)) {
|
||||
detaint_natural($series_id)
|
||||
|| &::ThrowCodeError("invalid_series_id");
|
||||
my $series = new Bugzilla::Series($series_id);
|
||||
push(@{$self->{'lines'}[$1]}, $series) if $series;
|
||||
}
|
||||
}
|
||||
|
||||
# Store all the labels
|
||||
if ($param =~ /^label(\d+)$/) {
|
||||
$self->{'labels'}[$1] = $cgi->param($param);
|
||||
}
|
||||
}
|
||||
|
||||
# Store the miscellaneous metadata
|
||||
$self->{'cumulate'} = $cgi->param('cumulate') ? 1 : 0;
|
||||
$self->{'gt'} = $cgi->param('gt') ? 1 : 0;
|
||||
$self->{'labelgt'} = $cgi->param('labelgt');
|
||||
$self->{'datefrom'} = $cgi->param('datefrom');
|
||||
$self->{'dateto'} = $cgi->param('dateto');
|
||||
|
||||
# If we are cumulating, a grand total makes no sense
|
||||
$self->{'gt'} = 0 if $self->{'cumulate'};
|
||||
|
||||
# Make sure the dates are ones we are able to interpret
|
||||
foreach my $date ('datefrom', 'dateto') {
|
||||
if ($self->{$date}) {
|
||||
$self->{$date} = &::str2time($self->{$date})
|
||||
|| &::ThrowUserError("illegal_date", { date => $self->{$date}});
|
||||
}
|
||||
}
|
||||
|
||||
# datefrom can't be after dateto
|
||||
if ($self->{'datefrom'} && $self->{'dateto'} &&
|
||||
$self->{'datefrom'} > $self->{'dateto'})
|
||||
{
|
||||
&::ThrowUserError("misarranged_dates",
|
||||
{'datefrom' => $cgi->param('datefrom'),
|
||||
'dateto' => $cgi->param('dateto')});
|
||||
}
|
||||
}
|
||||
|
||||
# Alter Chart so that the selected series are added to it.
|
||||
sub add {
|
||||
my $self = shift;
|
||||
my @series_ids = @_;
|
||||
|
||||
# Get the current size of the series; required for adding Grand Total later
|
||||
my $current_size = scalar($self->getSeriesIDs());
|
||||
|
||||
# Count the number of added series
|
||||
my $added = 0;
|
||||
# Create new Series and push them on to the list of lines.
|
||||
# Note that new lines have no label; the display template is responsible
|
||||
# for inventing something sensible.
|
||||
foreach my $series_id (@series_ids) {
|
||||
my $series = new Bugzilla::Series($series_id);
|
||||
if ($series) {
|
||||
push(@{$self->{'lines'}}, [$series]);
|
||||
push(@{$self->{'labels'}}, "");
|
||||
$added++;
|
||||
}
|
||||
}
|
||||
|
||||
# If we are going from < 2 to >= 2 series, add the Grand Total line.
|
||||
if (!$self->{'gt'}) {
|
||||
if ($current_size < 2 &&
|
||||
$current_size + $added >= 2)
|
||||
{
|
||||
$self->{'gt'} = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Alter Chart so that the selections are removed from it.
|
||||
sub remove {
|
||||
my $self = shift;
|
||||
my @line_ids = @_;
|
||||
|
||||
foreach my $line_id (@line_ids) {
|
||||
if ($line_id == 65536) {
|
||||
# Magic value - delete Grand Total.
|
||||
$self->{'gt'} = 0;
|
||||
}
|
||||
else {
|
||||
delete($self->{'lines'}->[$line_id]);
|
||||
delete($self->{'labels'}->[$line_id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Alter Chart so that the selections are summed.
|
||||
sub sum {
|
||||
my $self = shift;
|
||||
my @line_ids = @_;
|
||||
|
||||
# We can't add the Grand Total to things.
|
||||
@line_ids = grep(!/^65536$/, @line_ids);
|
||||
|
||||
# We can't add less than two things.
|
||||
return if scalar(@line_ids) < 2;
|
||||
|
||||
my @series;
|
||||
my $label = "";
|
||||
my $biggestlength = 0;
|
||||
|
||||
# We rescue the Series objects of all the series involved in the sum.
|
||||
foreach my $line_id (@line_ids) {
|
||||
my @line = @{$self->{'lines'}->[$line_id]};
|
||||
|
||||
foreach my $series (@line) {
|
||||
push(@series, $series);
|
||||
}
|
||||
|
||||
# We keep the label that labels the line with the most series.
|
||||
if (scalar(@line) > $biggestlength) {
|
||||
$biggestlength = scalar(@line);
|
||||
$label = $self->{'labels'}->[$line_id];
|
||||
}
|
||||
}
|
||||
|
||||
$self->remove(@line_ids);
|
||||
|
||||
push(@{$self->{'lines'}}, \@series);
|
||||
push(@{$self->{'labels'}}, $label);
|
||||
}
|
||||
|
||||
sub data {
|
||||
my $self = shift;
|
||||
$self->{'_data'} ||= $self->readData();
|
||||
return $self->{'_data'};
|
||||
}
|
||||
|
||||
# Convert the Chart's data into a plottable form in $self->{'_data'}.
|
||||
sub readData {
|
||||
my $self = shift;
|
||||
my @data;
|
||||
my @maxvals;
|
||||
|
||||
# Note: you get a bad image if getSeriesIDs returns nothing
|
||||
# We need to handle errors better.
|
||||
my $series_ids = join(",", $self->getSeriesIDs());
|
||||
|
||||
return [] unless $series_ids;
|
||||
|
||||
# Work out the date boundaries for our data.
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
# The date used is the one given if it's in a sensible range; otherwise,
|
||||
# it's the earliest or latest date in the database as appropriate.
|
||||
my $datefrom = $dbh->selectrow_array("SELECT MIN(series_date) " .
|
||||
"FROM series_data " .
|
||||
"WHERE series_id IN ($series_ids)");
|
||||
$datefrom = &::str2time($datefrom);
|
||||
|
||||
if ($self->{'datefrom'} && $self->{'datefrom'} > $datefrom) {
|
||||
$datefrom = $self->{'datefrom'};
|
||||
}
|
||||
|
||||
my $dateto = $dbh->selectrow_array("SELECT MAX(series_date) " .
|
||||
"FROM series_data " .
|
||||
"WHERE series_id IN ($series_ids)");
|
||||
$dateto = &::str2time($dateto);
|
||||
|
||||
if ($self->{'dateto'} && $self->{'dateto'} < $dateto) {
|
||||
$dateto = $self->{'dateto'};
|
||||
}
|
||||
|
||||
# Convert UNIX times back to a date format usable for SQL queries.
|
||||
my $sql_from = time2str('%Y-%m-%d', $datefrom);
|
||||
my $sql_to = time2str('%Y-%m-%d', $dateto);
|
||||
|
||||
# Prepare the query which retrieves the data for each series
|
||||
my $query = "SELECT " . $dbh->sql_to_days('series_date') . " - " .
|
||||
$dbh->sql_to_days('?') . ", series_value " .
|
||||
"FROM series_data " .
|
||||
"WHERE series_id = ? " .
|
||||
"AND series_date >= ?";
|
||||
if ($dateto) {
|
||||
$query .= " AND series_date <= ?";
|
||||
}
|
||||
|
||||
my $sth = $dbh->prepare($query);
|
||||
|
||||
my $gt_index = $self->{'gt'} ? scalar(@{$self->{'lines'}}) : undef;
|
||||
my $line_index = 0;
|
||||
|
||||
$maxvals[$gt_index] = 0 if $gt_index;
|
||||
|
||||
my @datediff_total;
|
||||
|
||||
foreach my $line (@{$self->{'lines'}}) {
|
||||
# Even if we end up with no data, we need an empty arrayref to prevent
|
||||
# errors in the PNG-generating code
|
||||
$data[$line_index] = [];
|
||||
$maxvals[$line_index] = 0;
|
||||
|
||||
foreach my $series (@$line) {
|
||||
|
||||
# Get the data for this series and add it on
|
||||
if ($dateto) {
|
||||
$sth->execute($sql_from, $series->{'series_id'}, $sql_from, $sql_to);
|
||||
}
|
||||
else {
|
||||
$sth->execute($sql_from, $series->{'series_id'}, $sql_from);
|
||||
}
|
||||
my $points = $sth->fetchall_arrayref();
|
||||
|
||||
foreach my $point (@$points) {
|
||||
my ($datediff, $value) = @$point;
|
||||
$data[$line_index][$datediff] ||= 0;
|
||||
$data[$line_index][$datediff] += $value;
|
||||
if ($data[$line_index][$datediff] > $maxvals[$line_index]) {
|
||||
$maxvals[$line_index] = $data[$line_index][$datediff];
|
||||
}
|
||||
|
||||
$datediff_total[$datediff] += $value;
|
||||
|
||||
# Add to the grand total, if we are doing that
|
||||
if ($gt_index) {
|
||||
$data[$gt_index][$datediff] ||= 0;
|
||||
$data[$gt_index][$datediff] += $value;
|
||||
if ($data[$gt_index][$datediff] > $maxvals[$gt_index]) {
|
||||
$maxvals[$gt_index] = $data[$gt_index][$datediff];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# We are done with the series making up this line, go to the next one
|
||||
$line_index++;
|
||||
}
|
||||
|
||||
# calculate maximum y value
|
||||
if ($self->{'cumulate'}) {
|
||||
# Make sure we do not try to take the max of an array with undef values
|
||||
my @processed_datediff;
|
||||
while (@datediff_total) {
|
||||
my $datediff = shift @datediff_total;
|
||||
push @processed_datediff, $datediff if defined($datediff);
|
||||
}
|
||||
$self->{'y_max_value'} = Bugzilla::Util::max(@processed_datediff);
|
||||
}
|
||||
else {
|
||||
$self->{'y_max_value'} = Bugzilla::Util::max(@maxvals);
|
||||
}
|
||||
$self->{'y_max_value'} |= 1; # For log()
|
||||
|
||||
# Align the max y value:
|
||||
# For one- or two-digit numbers, increase y_max_value until divisible by 8
|
||||
# For larger numbers, see the comments below to figure out what's going on
|
||||
if ($self->{'y_max_value'} < 100) {
|
||||
do {
|
||||
++$self->{'y_max_value'};
|
||||
} while ($self->{'y_max_value'} % 8 != 0);
|
||||
}
|
||||
else {
|
||||
# First, get the # of digits in the y_max_value
|
||||
my $num_digits = 1+int(log($self->{'y_max_value'})/log(10));
|
||||
|
||||
# We want to zero out all but the top 2 digits
|
||||
my $mask_length = $num_digits - 2;
|
||||
$self->{'y_max_value'} /= 10**$mask_length;
|
||||
$self->{'y_max_value'} = int($self->{'y_max_value'});
|
||||
$self->{'y_max_value'} *= 10**$mask_length;
|
||||
|
||||
# Add 10^$mask_length to the max value
|
||||
# Continue to increase until it's divisible by 8 * 10^($mask_length-1)
|
||||
# (Throwing in the -1 keeps at least the smallest digit at zero)
|
||||
do {
|
||||
$self->{'y_max_value'} += 10**$mask_length;
|
||||
} while ($self->{'y_max_value'} % (8*(10**($mask_length-1))) != 0);
|
||||
}
|
||||
|
||||
|
||||
# Add the x-axis labels into the data structure
|
||||
my $date_progression = generateDateProgression($datefrom, $dateto);
|
||||
unshift(@data, $date_progression);
|
||||
|
||||
if ($self->{'gt'}) {
|
||||
# Add Grand Total to label list
|
||||
push(@{$self->{'labels'}}, $self->{'labelgt'});
|
||||
|
||||
$data[$gt_index] ||= [];
|
||||
}
|
||||
|
||||
return \@data;
|
||||
}
|
||||
|
||||
# Flatten the data structure into a list of series_ids
|
||||
sub getSeriesIDs {
|
||||
my $self = shift;
|
||||
my @series_ids;
|
||||
|
||||
foreach my $line (@{$self->{'lines'}}) {
|
||||
foreach my $series (@$line) {
|
||||
push(@series_ids, $series->{'series_id'});
|
||||
}
|
||||
}
|
||||
|
||||
return @series_ids;
|
||||
}
|
||||
|
||||
# Class method to get the data necessary to populate the "select series"
|
||||
# widgets on various pages.
|
||||
sub getVisibleSeries {
|
||||
my %cats;
|
||||
|
||||
# List of groups the user is in; use -1 to make sure it's not empty.
|
||||
my $grouplist = join(", ", (-1, values(%{Bugzilla->user->groups})));
|
||||
|
||||
# Get all visible series
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $serieses = $dbh->selectall_arrayref("SELECT cc1.name, cc2.name, " .
|
||||
"series.name, series.series_id " .
|
||||
"FROM series " .
|
||||
"INNER JOIN series_categories AS cc1 " .
|
||||
" ON series.category = cc1.id " .
|
||||
"INNER JOIN series_categories AS cc2 " .
|
||||
" ON series.subcategory = cc2.id " .
|
||||
"LEFT JOIN category_group_map AS cgm " .
|
||||
" ON series.category = cgm.category_id " .
|
||||
" AND cgm.group_id NOT IN($grouplist) " .
|
||||
"WHERE creator = " . Bugzilla->user->id . " OR " .
|
||||
" cgm.category_id IS NULL " .
|
||||
$dbh->sql_group_by('series.series_id', 'cc1.name, cc2.name, ' .
|
||||
'series.name'));
|
||||
foreach my $series (@$serieses) {
|
||||
my ($cat, $subcat, $name, $series_id) = @$series;
|
||||
$cats{$cat}{$subcat}{$name} = $series_id;
|
||||
}
|
||||
|
||||
return \%cats;
|
||||
}
|
||||
|
||||
sub generateDateProgression {
|
||||
my ($datefrom, $dateto) = @_;
|
||||
my @progression;
|
||||
|
||||
$dateto = $dateto || time();
|
||||
my $oneday = 60 * 60 * 24;
|
||||
|
||||
# When the from and to dates are converted by str2time(), you end up with
|
||||
# a time figure representing midnight at the beginning of that day. We
|
||||
# adjust the times by 1/3 and 2/3 of a day respectively to prevent
|
||||
# edge conditions in time2str().
|
||||
$datefrom += $oneday / 3;
|
||||
$dateto += (2 * $oneday) / 3;
|
||||
|
||||
while ($datefrom < $dateto) {
|
||||
push (@progression, &::time2str("%Y-%m-%d", $datefrom));
|
||||
$datefrom += $oneday;
|
||||
}
|
||||
|
||||
return \@progression;
|
||||
}
|
||||
|
||||
sub dump {
|
||||
my $self = shift;
|
||||
|
||||
# Make sure we've read in our data
|
||||
my $data = $self->data;
|
||||
|
||||
require Data::Dumper;
|
||||
print "<pre>Bugzilla::Chart object:\n";
|
||||
print Data::Dumper::Dumper($self);
|
||||
print "</pre>";
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -1,254 +0,0 @@
|
||||
# -*- 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): Tiago R. Mello <timello@async.com.br>
|
||||
#
|
||||
|
||||
use strict;
|
||||
|
||||
package Bugzilla::Classification;
|
||||
|
||||
use Bugzilla::Util;
|
||||
use Bugzilla::Error;
|
||||
use Bugzilla::Product;
|
||||
|
||||
###############################
|
||||
#### Initialization ####
|
||||
###############################
|
||||
|
||||
use constant DB_COLUMNS => qw(
|
||||
classifications.id
|
||||
classifications.name
|
||||
classifications.description
|
||||
);
|
||||
|
||||
our $columns = join(", ", DB_COLUMNS);
|
||||
|
||||
###############################
|
||||
#### Methods ####
|
||||
###############################
|
||||
|
||||
sub new {
|
||||
my $invocant = shift;
|
||||
my $class = ref($invocant) || $invocant;
|
||||
my $self = {};
|
||||
bless($self, $class);
|
||||
return $self->_init(@_);
|
||||
}
|
||||
|
||||
sub _init {
|
||||
my $self = shift;
|
||||
my ($param) = @_;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
my $id = $param unless (ref $param eq 'HASH');
|
||||
my $classification;
|
||||
|
||||
if (defined $id) {
|
||||
detaint_natural($id)
|
||||
|| ThrowCodeError('param_must_be_numeric',
|
||||
{function => 'Bugzilla::Classification::_init'});
|
||||
|
||||
$classification = $dbh->selectrow_hashref(qq{
|
||||
SELECT $columns FROM classifications
|
||||
WHERE id = ?}, undef, $id);
|
||||
|
||||
} elsif (defined $param->{'name'}) {
|
||||
|
||||
trick_taint($param->{'name'});
|
||||
$classification = $dbh->selectrow_hashref(qq{
|
||||
SELECT $columns FROM classifications
|
||||
WHERE name = ?}, undef, $param->{'name'});
|
||||
} else {
|
||||
ThrowCodeError('bad_arg',
|
||||
{argument => 'param',
|
||||
function => 'Bugzilla::Classification::_init'});
|
||||
}
|
||||
|
||||
return undef unless (defined $classification);
|
||||
|
||||
foreach my $field (keys %$classification) {
|
||||
$self->{$field} = $classification->{$field};
|
||||
}
|
||||
return $self;
|
||||
}
|
||||
|
||||
sub product_count {
|
||||
my $self = shift;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
if (!defined $self->{'product_count'}) {
|
||||
$self->{'product_count'} = $dbh->selectrow_array(q{
|
||||
SELECT COUNT(*) FROM products
|
||||
WHERE classification_id = ?}, undef, $self->id) || 0;
|
||||
}
|
||||
return $self->{'product_count'};
|
||||
}
|
||||
|
||||
sub products {
|
||||
my $self = shift;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
if (!$self->{'products'}) {
|
||||
my $product_ids = $dbh->selectcol_arrayref(q{
|
||||
SELECT id FROM products
|
||||
WHERE classification_id = ?
|
||||
ORDER BY name}, undef, $self->id);
|
||||
|
||||
my @products;
|
||||
foreach my $product_id (@$product_ids) {
|
||||
push (@products, new Bugzilla::Product($product_id));
|
||||
}
|
||||
$self->{'products'} = \@products;
|
||||
}
|
||||
return $self->{'products'};
|
||||
}
|
||||
|
||||
###############################
|
||||
#### Accessors ####
|
||||
###############################
|
||||
|
||||
sub id { return $_[0]->{'id'}; }
|
||||
sub name { return $_[0]->{'name'}; }
|
||||
sub description { return $_[0]->{'description'}; }
|
||||
|
||||
###############################
|
||||
#### Subroutines ####
|
||||
###############################
|
||||
|
||||
sub get_all_classifications {
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
my $ids = $dbh->selectcol_arrayref(q{
|
||||
SELECT id FROM classifications ORDER BY name});
|
||||
|
||||
my @classifications;
|
||||
foreach my $id (@$ids) {
|
||||
push @classifications, new Bugzilla::Classification($id);
|
||||
}
|
||||
return @classifications;
|
||||
}
|
||||
|
||||
sub check_classification {
|
||||
my ($class_name) = @_;
|
||||
|
||||
unless ($class_name) {
|
||||
ThrowUserError("classification_not_specified");
|
||||
}
|
||||
|
||||
my $classification =
|
||||
new Bugzilla::Classification({name => $class_name});
|
||||
|
||||
unless ($classification) {
|
||||
ThrowUserError("classification_doesnt_exist",
|
||||
{ name => $class_name });
|
||||
}
|
||||
|
||||
return $classification;
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::Classification - Bugzilla classification class.
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
use Bugzilla::Classification;
|
||||
|
||||
my $classification = new Bugzilla::Classification(1);
|
||||
my $classification = new Bugzilla::Classification({name => 'Acme'});
|
||||
|
||||
my $id = $classification->id;
|
||||
my $name = $classification->name;
|
||||
my $description = $classification->description;
|
||||
my $product_count = $classification->product_count;
|
||||
my $products = $classification->products;
|
||||
|
||||
my $hash_ref = Bugzilla::Classification::get_all_classifications();
|
||||
my $classification = $hash_ref->{1};
|
||||
|
||||
my $classification =
|
||||
Bugzilla::Classification::check_classification('AcmeClass');
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
Classification.pm represents a Classification object.
|
||||
|
||||
A Classification is a higher-level grouping of Products.
|
||||
|
||||
=head1 METHODS
|
||||
|
||||
=over
|
||||
|
||||
=item C<new($param)>
|
||||
|
||||
Description: The constructor is used to load an existing
|
||||
classification by passing a classification
|
||||
id or classification name using a hash.
|
||||
|
||||
Params: $param - If you pass an integer, the integer is the
|
||||
classification_id from the database that we
|
||||
want to read in. If you pass in a hash with
|
||||
'name' key, then the value of the name key
|
||||
is the name of a classification from the DB.
|
||||
|
||||
Returns: A Bugzilla::Classification object.
|
||||
|
||||
=item C<product_count()>
|
||||
|
||||
Description: Returns the total number of products that belong to
|
||||
the classification.
|
||||
|
||||
Params: none.
|
||||
|
||||
Returns: Integer - The total of products inside the classification.
|
||||
|
||||
=item C<products>
|
||||
|
||||
Description: Returns all products of the classification.
|
||||
|
||||
Params: none.
|
||||
|
||||
Returns: A reference to an array of Bugzilla::Product objects.
|
||||
|
||||
=back
|
||||
|
||||
=head1 SUBROUTINES
|
||||
|
||||
=over
|
||||
|
||||
=item C<get_all_classifications()>
|
||||
|
||||
Description: Returns all classifications.
|
||||
|
||||
Params: none.
|
||||
|
||||
Returns: Bugzilla::Classification object list.
|
||||
|
||||
=item C<check_classification($classification_name)>
|
||||
|
||||
Description: Checks if the classification name passed in is a
|
||||
valid classification.
|
||||
|
||||
Params: $classification_name - String with a classification name.
|
||||
|
||||
Returns: Bugzilla::Classification object.
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
@@ -1,273 +0,0 @@
|
||||
# -*- 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): Tiago R. Mello <timello@async.com.br>
|
||||
#
|
||||
|
||||
use strict;
|
||||
|
||||
package Bugzilla::Component;
|
||||
|
||||
use Bugzilla::Util;
|
||||
use Bugzilla::Error;
|
||||
use Bugzilla::User;
|
||||
|
||||
###############################
|
||||
#### Initialization ####
|
||||
###############################
|
||||
|
||||
use constant DB_COLUMNS => qw(
|
||||
components.id
|
||||
components.name
|
||||
components.product_id
|
||||
components.initialowner
|
||||
components.initialqacontact
|
||||
components.description
|
||||
);
|
||||
|
||||
our $columns = join(", ", DB_COLUMNS);
|
||||
|
||||
###############################
|
||||
#### Methods ####
|
||||
###############################
|
||||
|
||||
sub new {
|
||||
my $invocant = shift;
|
||||
my $class = ref($invocant) || $invocant;
|
||||
my $self = {};
|
||||
bless($self, $class);
|
||||
return $self->_init(@_);
|
||||
}
|
||||
|
||||
sub _init {
|
||||
my $self = shift;
|
||||
my ($param) = (@_);
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
my $id = $param unless (ref $param eq 'HASH');
|
||||
my $component;
|
||||
|
||||
if (defined $id) {
|
||||
detaint_natural($id)
|
||||
|| ThrowCodeError('param_must_be_numeric',
|
||||
{function => 'Bugzilla::Component::_init'});
|
||||
|
||||
$component = $dbh->selectrow_hashref(qq{
|
||||
SELECT $columns FROM components
|
||||
WHERE id = ?}, undef, $id);
|
||||
|
||||
} elsif (defined $param->{'product_id'}
|
||||
&& detaint_natural($param->{'product_id'})
|
||||
&& defined $param->{'name'}) {
|
||||
|
||||
trick_taint($param->{'name'});
|
||||
|
||||
$component = $dbh->selectrow_hashref(qq{
|
||||
SELECT $columns FROM components
|
||||
WHERE name = ? AND product_id = ?}, undef,
|
||||
($param->{'name'}, $param->{'product_id'}));
|
||||
} else {
|
||||
ThrowCodeError('bad_arg',
|
||||
{argument => 'param',
|
||||
function => 'Bugzilla::Component::_init'});
|
||||
}
|
||||
|
||||
return undef unless (defined $component);
|
||||
|
||||
foreach my $field (keys %$component) {
|
||||
$self->{$field} = $component->{$field};
|
||||
}
|
||||
return $self;
|
||||
}
|
||||
|
||||
sub bug_count {
|
||||
my $self = shift;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
if (!defined $self->{'bug_count'}) {
|
||||
$self->{'bug_count'} = $dbh->selectrow_array(q{
|
||||
SELECT COUNT(*) FROM bugs
|
||||
WHERE component_id = ?}, undef, $self->id) || 0;
|
||||
}
|
||||
return $self->{'bug_count'};
|
||||
}
|
||||
|
||||
sub bug_ids {
|
||||
my $self = shift;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
if (!defined $self->{'bugs_ids'}) {
|
||||
$self->{'bugs_ids'} = $dbh->selectcol_arrayref(q{
|
||||
SELECT bug_id FROM bugs
|
||||
WHERE component_id = ?}, undef, $self->id);
|
||||
}
|
||||
return $self->{'bugs_ids'};
|
||||
}
|
||||
|
||||
sub default_assignee {
|
||||
my $self = shift;
|
||||
|
||||
if (!defined $self->{'default_assignee'}) {
|
||||
$self->{'default_assignee'} =
|
||||
new Bugzilla::User($self->{'initialowner'});
|
||||
}
|
||||
return $self->{'default_assignee'};
|
||||
}
|
||||
|
||||
sub default_qa_contact {
|
||||
my $self = shift;
|
||||
|
||||
if (!defined $self->{'default_qa_contact'}) {
|
||||
$self->{'default_qa_contact'} =
|
||||
new Bugzilla::User($self->{'initialqacontact'});
|
||||
}
|
||||
return $self->{'default_qa_contact'};
|
||||
}
|
||||
|
||||
###############################
|
||||
#### Accessors ####
|
||||
###############################
|
||||
|
||||
sub id { return $_[0]->{'id'}; }
|
||||
sub name { return $_[0]->{'name'}; }
|
||||
sub description { return $_[0]->{'description'}; }
|
||||
sub product_id { return $_[0]->{'product_id'}; }
|
||||
|
||||
###############################
|
||||
#### Subroutines ####
|
||||
###############################
|
||||
|
||||
sub check_component {
|
||||
my ($product, $comp_name) = @_;
|
||||
|
||||
$comp_name || ThrowUserError('component_blank_name');
|
||||
|
||||
if (length($comp_name) > 64) {
|
||||
ThrowUserError('component_name_too_long',
|
||||
{'name' => $comp_name});
|
||||
}
|
||||
|
||||
my $component =
|
||||
new Bugzilla::Component({product_id => $product->id,
|
||||
name => $comp_name});
|
||||
unless ($component) {
|
||||
ThrowUserError('component_not_valid',
|
||||
{'product' => $product->name,
|
||||
'name' => $comp_name});
|
||||
}
|
||||
return $component;
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::Component - Bugzilla product component class.
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
use Bugzilla::Component;
|
||||
|
||||
my $component = new Bugzilla::Component(1);
|
||||
my $component = new Bugzilla::Component({product_id => 1,
|
||||
name => 'AcmeComp'});
|
||||
|
||||
my $bug_count = $component->bug_count();
|
||||
my $bug_ids = $component->bug_ids();
|
||||
my $id = $component->id;
|
||||
my $name = $component->name;
|
||||
my $description = $component->description;
|
||||
my $product_id = $component->product_id;
|
||||
my $default_assignee = $component->default_assignee;
|
||||
my $default_qa_contact = $component->default_qa_contact;
|
||||
|
||||
my $component = Bugzilla::Component::check_component($product, 'AcmeComp');
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
Component.pm represents a Product Component object.
|
||||
|
||||
=head1 METHODS
|
||||
|
||||
=over
|
||||
|
||||
=item C<new($param)>
|
||||
|
||||
Description: The constructor is used to load an existing component
|
||||
by passing a component id or a hash with the product
|
||||
id and the component name.
|
||||
|
||||
Params: $param - If you pass an integer, the integer is the
|
||||
component id from the database that we want to
|
||||
read in. If you pass in a hash with 'name' key,
|
||||
then the value of the name key is the name of a
|
||||
component from the DB.
|
||||
|
||||
Returns: A Bugzilla::Component object.
|
||||
|
||||
=item C<bug_count()>
|
||||
|
||||
Description: Returns the total of bugs that belong to the component.
|
||||
|
||||
Params: none.
|
||||
|
||||
Returns: Integer with the number of bugs.
|
||||
|
||||
=item C<bugs_ids()>
|
||||
|
||||
Description: Returns all bug IDs that belong to the component.
|
||||
|
||||
Params: none.
|
||||
|
||||
Returns: A reference to an array of bug IDs.
|
||||
|
||||
=item C<default_assignee()>
|
||||
|
||||
Description: Returns a user object that represents the default assignee for
|
||||
the component.
|
||||
|
||||
Params: none.
|
||||
|
||||
Returns: A Bugzilla::User object.
|
||||
|
||||
=item C<default_qa_contact()>
|
||||
|
||||
Description: Returns a user object that represents the default QA contact for
|
||||
the component.
|
||||
|
||||
Params: none.
|
||||
|
||||
Returns: A Bugzilla::User object.
|
||||
|
||||
=back
|
||||
|
||||
=head1 SUBROUTINES
|
||||
|
||||
=over
|
||||
|
||||
=item C<check_component($product, $comp_name)>
|
||||
|
||||
Description: Checks if the component name was passed in and if it is a valid
|
||||
component.
|
||||
|
||||
Params: $product - A Bugzilla::Product object.
|
||||
$comp_name - String with a component name.
|
||||
|
||||
Returns: Bugzilla::Component object.
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
@@ -1,417 +0,0 @@
|
||||
# -*- 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>
|
||||
# Jake <jake@bugzilla.org>
|
||||
# J. Paul Reed <preed@sigkill.com>
|
||||
# Bradley Baetz <bbaetz@student.usyd.edu.au>
|
||||
# Christopher Aillon <christopher@aillon.com>
|
||||
# Erik Stambaugh <erik@dasbistro.com>
|
||||
# Frédéric Buclin <LpSolit@gmail.com>
|
||||
# Guzman Braso <gbn@hqso.net>
|
||||
|
||||
package Bugzilla::Config;
|
||||
|
||||
use strict;
|
||||
|
||||
use base qw(Exporter);
|
||||
|
||||
# Under mod_perl, get this from a .htaccess config variable,
|
||||
# and/or default from the current 'real' dir
|
||||
# At some stage after this, it may be possible for these dir locations
|
||||
# to go into localconfig. localconfig can't be specified in a config file,
|
||||
# except possibly with mod_perl. If you move localconfig, you need to change
|
||||
# the define here.
|
||||
# $libpath is really only for mod_perl; its not yet possible to move the
|
||||
# .pms elsewhere.
|
||||
# $webdotdir must be in the webtree somewhere. Even if you use a local dot,
|
||||
# we output images to there. Also, if $webdot dir is not relative to the
|
||||
# bugzilla root directory, you'll need to change showdependencygraph.cgi to
|
||||
# set image_url to the correct location.
|
||||
# The script should really generate these graphs directly...
|
||||
# Note that if $libpath is changed, some stuff will break, notably dependency
|
||||
# graphs (since the path will be wrong in the HTML). This will be fixed at
|
||||
# some point.
|
||||
|
||||
# constant paths
|
||||
our $libpath = '.';
|
||||
|
||||
# importxml.pl, when run by a mail daemon, sets the bugzilla path explicitly.
|
||||
# This then mucks it up, so if we are coming from importxml.pl, set $libpath
|
||||
# back to the way it was.
|
||||
if ($::path) {
|
||||
$libpath = $::path;
|
||||
}
|
||||
|
||||
our $templatedir = "$libpath/template";
|
||||
|
||||
# variable paths
|
||||
our $project;
|
||||
our $localconfig;
|
||||
our $datadir;
|
||||
if ($ENV{'PROJECT'} && $ENV{'PROJECT'} =~ /^(\w+)$/) {
|
||||
$project = $1;
|
||||
$localconfig = "$libpath/localconfig.$project";
|
||||
$datadir = "$libpath/data/$project";
|
||||
} else {
|
||||
$localconfig = "$libpath/localconfig";
|
||||
$datadir = "$libpath/data";
|
||||
}
|
||||
our $attachdir = "$datadir/attachments";
|
||||
our $webdotdir = "$datadir/webdot";
|
||||
|
||||
our @parampanels = ();
|
||||
|
||||
# Module stuff
|
||||
@Bugzilla::Config::EXPORT = qw(Param);
|
||||
|
||||
# Don't export localvars by default - people should have to explicitly
|
||||
# ask for it, as a (probably futile) attempt to stop code using it
|
||||
# when it shouldn't
|
||||
# ChmodDataFile is here until that stuff all moves out of globals.pl
|
||||
# into this file
|
||||
@Bugzilla::Config::EXPORT_OK = qw(ChmodDataFile);
|
||||
|
||||
%Bugzilla::Config::EXPORT_TAGS =
|
||||
(
|
||||
admin => [qw(UpdateParams SetParam WriteParams)],
|
||||
db => [qw($db_driver $db_host $db_port $db_name $db_user $db_pass $db_sock)],
|
||||
locations => [qw($libpath $localconfig $attachdir $datadir $templatedir
|
||||
$webdotdir $project)],
|
||||
params => [qw(@parampanels)],
|
||||
);
|
||||
Exporter::export_ok_tags('admin', 'db', 'locations', 'params');
|
||||
|
||||
# Bugzilla version
|
||||
$Bugzilla::Config::VERSION = "2.22.7";
|
||||
|
||||
use Safe;
|
||||
|
||||
use vars qw(@param_list);
|
||||
|
||||
# Data::Dumper is required as needed, below. The problem is that then when
|
||||
# the code locally sets $Data::Dumper::Foo, this triggers 'used only once'
|
||||
# warnings.
|
||||
# We can't predeclare another package's vars, though, so just use them
|
||||
{
|
||||
local $Data::Dumper::Sortkeys;
|
||||
local $Data::Dumper::Terse;
|
||||
local $Data::Dumper::Indent;
|
||||
}
|
||||
|
||||
my %param;
|
||||
|
||||
# INITIALISATION CODE
|
||||
|
||||
# XXX - mod_perl - need to register Apache init handler for params
|
||||
sub _load_datafiles {
|
||||
# read in localconfig variables
|
||||
do $localconfig;
|
||||
|
||||
if (-e "$datadir/params") {
|
||||
# Handle reading old param files by munging the symbol table
|
||||
# Don't have to do this if we use safe mode, since its evaled
|
||||
# in a sandbox where $foo is in the same module as $::foo
|
||||
#local *::param = \%param;
|
||||
|
||||
# Note that checksetup.pl sets file permissions on '$datadir/params'
|
||||
|
||||
# Using Safe mode is _not_ a guarantee of safety if someone does
|
||||
# manage to write to the file. However, it won't hurt...
|
||||
# See bug 165144 for not needing to eval this at all
|
||||
my $s = new Safe;
|
||||
|
||||
$s->rdo("$datadir/params");
|
||||
die "Error reading $datadir/params: $!" if $!;
|
||||
die "Error evaluating $datadir/params: $@" if $@;
|
||||
|
||||
# Now read the param back out from the sandbox
|
||||
%param = %{$s->varglob('param')};
|
||||
}
|
||||
}
|
||||
|
||||
# Load in the datafiles
|
||||
_load_datafiles();
|
||||
|
||||
# Stick the params into a hash
|
||||
my %params;
|
||||
|
||||
# Load in the param definitions
|
||||
foreach my $item ((glob "$libpath/Bugzilla/Config/*.pm")) {
|
||||
$item =~ m#/([^/]+)\.pm$#;
|
||||
my $module = $1;
|
||||
next if ($module eq 'Common');
|
||||
require "Bugzilla/Config/$module.pm";
|
||||
my @new_param_list = "Bugzilla::Config::$module"->get_param_list();
|
||||
foreach my $item (@new_param_list) {
|
||||
$params{$item->{'name'}} = $item;
|
||||
}
|
||||
push(@parampanels, $module);
|
||||
push(@param_list, @new_param_list);
|
||||
}
|
||||
|
||||
# END INIT CODE
|
||||
|
||||
# Subroutines go here
|
||||
|
||||
sub Param {
|
||||
my ($param) = @_;
|
||||
|
||||
# By this stage, the param must be in the hash
|
||||
die "Can't find param named $param" unless (exists $params{$param});
|
||||
|
||||
# When module startup code runs (which is does even via -c, when using
|
||||
# |use|), we may try to grab params which don't exist yet. This affects
|
||||
# tests, so have this as a fallback for the -c case
|
||||
return $params{$param}->{default} if ($^C && not exists $param{$param});
|
||||
|
||||
# If we have a value for the param, return it
|
||||
return $param{$param} if exists $param{$param};
|
||||
|
||||
# Else error out
|
||||
die "No value for param $param (try running checksetup.pl again)";
|
||||
}
|
||||
|
||||
sub SetParam {
|
||||
my ($name, $value) = @_;
|
||||
|
||||
die "Unknown param $name" unless (exists $params{$name});
|
||||
|
||||
my $entry = $params{$name};
|
||||
|
||||
# sanity check the value
|
||||
|
||||
# XXX - This runs the checks. Which would be good, except that
|
||||
# check_shadowdb creates the database as a side effect, and so the
|
||||
# checker fails the second time around...
|
||||
if ($name ne 'shadowdb' && exists $entry->{'checker'}) {
|
||||
my $err = $entry->{'checker'}->($value, $entry);
|
||||
die "Param $name is not valid: $err" unless $err eq '';
|
||||
}
|
||||
|
||||
$param{$name} = $value;
|
||||
}
|
||||
|
||||
sub UpdateParams {
|
||||
# --- PARAM CONVERSION CODE ---
|
||||
|
||||
# Note that this isn't particularly 'clean' in terms of separating
|
||||
# the backend code (ie this) from the actual params.
|
||||
# We don't care about that, though
|
||||
|
||||
# Old Bugzilla versions stored the version number in the params file
|
||||
# We don't want it, so get rid of it
|
||||
delete $param{'version'};
|
||||
|
||||
# Change from usebrowserinfo to defaultplatform/defaultopsys combo
|
||||
if (exists $param{'usebrowserinfo'}) {
|
||||
if (!$param{'usebrowserinfo'}) {
|
||||
if (!exists $param{'defaultplatform'}) {
|
||||
$param{'defaultplatform'} = 'Other';
|
||||
}
|
||||
if (!exists $param{'defaultopsys'}) {
|
||||
$param{'defaultopsys'} = 'Other';
|
||||
}
|
||||
}
|
||||
delete $param{'usebrowserinfo'};
|
||||
}
|
||||
|
||||
# Change from a boolean for quips to multi-state
|
||||
if (exists $param{'usequip'} && !exists $param{'enablequips'}) {
|
||||
$param{'enablequips'} = $param{'usequip'} ? 'on' : 'off';
|
||||
delete $param{'usequip'};
|
||||
}
|
||||
|
||||
# Change from old product groups to controls for group_control_map
|
||||
# 2002-10-14 bug 147275 bugreport@peshkin.net
|
||||
if (exists $param{'usebuggroups'} && !exists $param{'makeproductgroups'}) {
|
||||
$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'}) {
|
||||
$param{'loginmethod'} = $param{'useLDAP'} ? "LDAP" : "DB";
|
||||
}
|
||||
|
||||
# set verify method to whatever loginmethod was
|
||||
if (exists $param{'loginmethod'} && !exists $param{'user_verify_class'}) {
|
||||
$param{'user_verify_class'} = $param{'loginmethod'};
|
||||
delete $param{'loginmethod'};
|
||||
}
|
||||
|
||||
# Remove quip-display control from parameters
|
||||
# and give it to users via User Settings (Bug 41972)
|
||||
if ( exists $param{'enablequips'}
|
||||
&& !exists $param{'quip_list_entry_control'})
|
||||
{
|
||||
my $new_value;
|
||||
($param{'enablequips'} eq 'on') && do {$new_value = 'open';};
|
||||
($param{'enablequips'} eq 'approved') && do {$new_value = 'moderated';};
|
||||
($param{'enablequips'} eq 'frozen') && do {$new_value = 'closed';};
|
||||
($param{'enablequips'} eq 'off') && do {$new_value = 'closed';};
|
||||
$param{'quip_list_entry_control'} = $new_value;
|
||||
delete $param{'enablequips'};
|
||||
}
|
||||
|
||||
# --- DEFAULTS FOR NEW PARAMS ---
|
||||
|
||||
foreach my $item (@param_list) {
|
||||
my $name = $item->{'name'};
|
||||
$param{$name} = $item->{'default'} unless exists $param{$name};
|
||||
}
|
||||
|
||||
# --- REMOVE OLD PARAMS ---
|
||||
|
||||
my @oldparams = ();
|
||||
|
||||
# Remove any old params
|
||||
foreach my $item (keys %param) {
|
||||
if (!grep($_ eq $item, map ($_->{'name'}, @param_list))) {
|
||||
require Data::Dumper;
|
||||
|
||||
local $Data::Dumper::Terse = 1;
|
||||
local $Data::Dumper::Indent = 0;
|
||||
push (@oldparams, [$item, Data::Dumper->Dump([$param{$item}])]);
|
||||
delete $param{$item};
|
||||
}
|
||||
}
|
||||
|
||||
return @oldparams;
|
||||
}
|
||||
|
||||
sub WriteParams {
|
||||
require Data::Dumper;
|
||||
|
||||
# This only has an affect for Data::Dumper >= 2.12 (ie perl >= 5.8.0)
|
||||
# Its just cosmetic, though, so that doesn't matter
|
||||
local $Data::Dumper::Sortkeys = 1;
|
||||
|
||||
require File::Temp;
|
||||
my ($fh, $tmpname) = File::Temp::tempfile('params.XXXXX',
|
||||
DIR => $datadir );
|
||||
|
||||
print $fh (Data::Dumper->Dump([ \%param ], [ '*param' ]))
|
||||
|| die "Can't write param file: $!";
|
||||
|
||||
close $fh;
|
||||
|
||||
rename $tmpname, "$datadir/params"
|
||||
or die "Can't rename $tmpname to $datadir/params: $!";
|
||||
|
||||
ChmodDataFile("$datadir/params", 0666);
|
||||
}
|
||||
|
||||
# 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($datadir))[2] & 0002) {
|
||||
$perm = 0777;
|
||||
}
|
||||
$perm = $perm & $mask;
|
||||
chmod $perm,$file;
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::Config - Configuration parameters for Bugzilla
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
# Getting parameters
|
||||
use Bugzilla::Config;
|
||||
|
||||
my $fooSetting = Param('foo');
|
||||
|
||||
# Administration functions
|
||||
use Bugzilla::Config qw(:admin);
|
||||
|
||||
my @removed_params = UpgradeParams();
|
||||
SetParam($param, $value);
|
||||
WriteParams();
|
||||
|
||||
# Localconfig variables may also be imported
|
||||
use Bugzilla::Config qw(:db);
|
||||
print "Connecting to $db_name as $db_user with $db_pass\n";
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
This package contains ways to access Bugzilla configuration parameters.
|
||||
|
||||
=head1 FUNCTIONS
|
||||
|
||||
=head2 Parameters
|
||||
|
||||
Parameters can be set, retrieved, and updated.
|
||||
|
||||
=over 4
|
||||
|
||||
=item C<Param($name)>
|
||||
|
||||
Returns the Param with the specified name. Either a string, or, in the case
|
||||
of multiple-choice parameters, an array reference.
|
||||
|
||||
=item C<SetParam($name, $value)>
|
||||
|
||||
Sets the param named $name to $value. Values are checked using the checker
|
||||
function for the given param if one exists.
|
||||
|
||||
=item C<UpdateParams()>
|
||||
|
||||
Updates the parameters, by transitioning old params to new formats, setting
|
||||
defaults for new params, and removing obsolete ones.
|
||||
|
||||
Any removed params are returned in a list, with elements [$item, $oldvalue]
|
||||
where $item is the entry in the param list.
|
||||
|
||||
=item C<WriteParams()>
|
||||
|
||||
Writes the parameters to disk.
|
||||
|
||||
=back
|
||||
|
||||
=over
|
||||
|
||||
=item *
|
||||
|
||||
The new value for the parameter
|
||||
|
||||
=item *
|
||||
|
||||
A reference to the entry in the param list for this parameter
|
||||
|
||||
Functions should return error text, or the empty string if there was no error.
|
||||
|
||||
=back
|
||||
@@ -1,69 +0,0 @@
|
||||
# -*- 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>
|
||||
#
|
||||
|
||||
package Bugzilla::Config::Admin;
|
||||
|
||||
use strict;
|
||||
|
||||
use Bugzilla::Config::Common;
|
||||
|
||||
$Bugzilla::Config::Admin::sortkey = "01";
|
||||
|
||||
sub get_param_list {
|
||||
my $class = shift;
|
||||
my @param_list = (
|
||||
{
|
||||
name => 'allowbugdeletion',
|
||||
type => 'b',
|
||||
default => 0
|
||||
},
|
||||
|
||||
{
|
||||
name => 'allowemailchange',
|
||||
type => 'b',
|
||||
default => 0
|
||||
},
|
||||
|
||||
{
|
||||
name => 'allowuserdeletion',
|
||||
type => 'b',
|
||||
default => 0
|
||||
},
|
||||
|
||||
{
|
||||
name => 'supportwatchers',
|
||||
type => 'b',
|
||||
default => 0
|
||||
} );
|
||||
return @param_list;
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -1,92 +0,0 @@
|
||||
# -*- 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>
|
||||
#
|
||||
|
||||
package Bugzilla::Config::Attachment;
|
||||
|
||||
use strict;
|
||||
|
||||
use Bugzilla::Config::Common;
|
||||
|
||||
$Bugzilla::Config::Attachment::sortkey = "025";
|
||||
|
||||
sub get_param_list {
|
||||
my $class = shift;
|
||||
my @param_list = (
|
||||
{
|
||||
name => 'allow_attachment_display',
|
||||
type => 'b',
|
||||
default => 0
|
||||
},
|
||||
|
||||
{
|
||||
name => 'allow_attach_url',
|
||||
type => 'b',
|
||||
default => 0
|
||||
},
|
||||
{
|
||||
name => 'maxpatchsize',
|
||||
type => 't',
|
||||
default => '1000',
|
||||
checker => \&check_numeric
|
||||
},
|
||||
|
||||
{
|
||||
name => 'maxattachmentsize',
|
||||
type => 't',
|
||||
default => '1000',
|
||||
checker => \&check_numeric
|
||||
},
|
||||
|
||||
# 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 => 'maxlocalattachment',
|
||||
type => 't',
|
||||
default => '0',
|
||||
checker => \&check_numeric
|
||||
},
|
||||
|
||||
{
|
||||
name => 'convert_uncompressed_images',
|
||||
type => 'b',
|
||||
default => 0,
|
||||
checker => \&check_image_converter
|
||||
} );
|
||||
return @param_list;
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -1,135 +0,0 @@
|
||||
# -*- 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>
|
||||
#
|
||||
|
||||
package Bugzilla::Config::Auth;
|
||||
|
||||
use strict;
|
||||
|
||||
use Bugzilla::Config::Common;
|
||||
|
||||
$Bugzilla::Config::Auth::sortkey = "02";
|
||||
|
||||
sub get_param_list {
|
||||
my $class = shift;
|
||||
my @param_list = (
|
||||
{
|
||||
name => 'auth_env_id',
|
||||
type => 't',
|
||||
default => '',
|
||||
},
|
||||
|
||||
{
|
||||
name => 'auth_env_email',
|
||||
type => 't',
|
||||
default => '',
|
||||
},
|
||||
|
||||
{
|
||||
name => 'auth_env_realname',
|
||||
type => 't',
|
||||
default => '',
|
||||
},
|
||||
|
||||
# XXX in the future:
|
||||
#
|
||||
# user_verify_class and user_info_class should have choices gathered from
|
||||
# whatever sits in their respective directories
|
||||
#
|
||||
# rather than comma-separated lists, these two should eventually become
|
||||
# arrays, but that requires alterations to editparams first
|
||||
|
||||
{
|
||||
name => 'user_info_class',
|
||||
type => 's',
|
||||
choices => [ 'CGI', 'Env', 'Env,CGI' ],
|
||||
default => 'CGI',
|
||||
checker => \&check_multi
|
||||
},
|
||||
|
||||
{
|
||||
name => 'user_verify_class',
|
||||
type => 's',
|
||||
choices => [ 'DB', 'LDAP', 'DB,LDAP', 'LDAP,DB' ],
|
||||
default => 'DB',
|
||||
checker => \&check_user_verify_class
|
||||
},
|
||||
|
||||
{
|
||||
name => 'rememberlogin',
|
||||
type => 's',
|
||||
choices => ['on', 'defaulton', 'defaultoff', 'off'],
|
||||
default => 'on',
|
||||
checker => \&check_multi
|
||||
},
|
||||
|
||||
{
|
||||
name => 'loginnetmask',
|
||||
type => 't',
|
||||
default => '32',
|
||||
checker => \&check_netmask
|
||||
},
|
||||
|
||||
{
|
||||
name => 'requirelogin',
|
||||
type => 'b',
|
||||
default => '0'
|
||||
},
|
||||
|
||||
{
|
||||
name => 'emailregexp',
|
||||
type => 't',
|
||||
default => q:^[\\w\\.\\+\\-=]+@[\\w\\.\\-]+\\.[\\w\\-]+$:,
|
||||
checker => \&check_regexp
|
||||
},
|
||||
|
||||
{
|
||||
name => 'emailregexpdesc',
|
||||
type => 'l',
|
||||
default => 'A legal address must contain exactly one \'@\', and at least ' .
|
||||
'one \'.\' after the @.'
|
||||
},
|
||||
|
||||
{
|
||||
name => 'emailsuffix',
|
||||
type => 't',
|
||||
default => ''
|
||||
},
|
||||
|
||||
{
|
||||
name => 'createemailregexp',
|
||||
type => 't',
|
||||
default => q:.*:,
|
||||
checker => \&check_regexp
|
||||
} );
|
||||
return @param_list;
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -1,135 +0,0 @@
|
||||
# -*- 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>
|
||||
#
|
||||
|
||||
package Bugzilla::Config::BugChange;
|
||||
|
||||
use strict;
|
||||
|
||||
use Bugzilla::Config::Common;
|
||||
|
||||
$Bugzilla::Config::BugChange::sortkey = "03";
|
||||
|
||||
sub get_param_list {
|
||||
my $class = shift;
|
||||
my @param_list = (
|
||||
{
|
||||
name => 'letsubmitterchoosepriority',
|
||||
type => 'b',
|
||||
default => 1
|
||||
},
|
||||
|
||||
{
|
||||
name => 'letsubmitterchoosemilestone',
|
||||
type => 'b',
|
||||
default => 1
|
||||
},
|
||||
|
||||
{
|
||||
name => 'musthavemilestoneonaccept',
|
||||
type => 'b',
|
||||
default => 0
|
||||
},
|
||||
|
||||
{
|
||||
name => 'commentoncreate',
|
||||
type => 'b',
|
||||
default => 0
|
||||
},
|
||||
|
||||
{
|
||||
name => 'commentonaccept',
|
||||
type => 'b',
|
||||
default => 0
|
||||
},
|
||||
|
||||
{
|
||||
name => 'commentonclearresolution',
|
||||
type => 'b',
|
||||
default => 0
|
||||
},
|
||||
|
||||
{
|
||||
name => 'commentonconfirm',
|
||||
type => 'b',
|
||||
default => 0
|
||||
},
|
||||
|
||||
{
|
||||
name => 'commentonresolve',
|
||||
type => 'b',
|
||||
default => 0
|
||||
},
|
||||
|
||||
{
|
||||
name => 'commentonreassign',
|
||||
type => 'b',
|
||||
default => 0
|
||||
},
|
||||
|
||||
{
|
||||
name => 'commentonreassignbycomponent',
|
||||
type => 'b',
|
||||
default => 0
|
||||
},
|
||||
|
||||
{
|
||||
name => 'commentonreopen',
|
||||
type => 'b',
|
||||
default => 0
|
||||
},
|
||||
|
||||
{
|
||||
name => 'commentonverify',
|
||||
type => 'b',
|
||||
default => 0
|
||||
},
|
||||
|
||||
{
|
||||
name => 'commentonclose',
|
||||
type => 'b',
|
||||
default => 0
|
||||
},
|
||||
|
||||
{
|
||||
name => 'commentonduplicate',
|
||||
type => 'b',
|
||||
default => 0
|
||||
},
|
||||
|
||||
{
|
||||
name => 'noresolveonopenblockers',
|
||||
type => 'b',
|
||||
default => 0,
|
||||
} );
|
||||
return @param_list;
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -1,119 +0,0 @@
|
||||
# -*- 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>
|
||||
#
|
||||
|
||||
package Bugzilla::Config::BugFields;
|
||||
|
||||
use strict;
|
||||
|
||||
use Bugzilla::Config::Common;
|
||||
|
||||
$Bugzilla::Config::BugFields::sortkey = "04";
|
||||
|
||||
sub get_param_list {
|
||||
my $class = shift;
|
||||
my @param_list = (
|
||||
{
|
||||
name => 'useclassification',
|
||||
type => 'b',
|
||||
default => 0
|
||||
},
|
||||
|
||||
{
|
||||
name => 'showallproducts',
|
||||
type => 'b',
|
||||
default => 0
|
||||
},
|
||||
|
||||
{
|
||||
name => 'usetargetmilestone',
|
||||
type => 'b',
|
||||
default => 0
|
||||
},
|
||||
|
||||
{
|
||||
name => 'useqacontact',
|
||||
type => 'b',
|
||||
default => 0
|
||||
},
|
||||
|
||||
{
|
||||
name => 'usestatuswhiteboard',
|
||||
type => 'b',
|
||||
default => 0
|
||||
},
|
||||
|
||||
{
|
||||
name => 'usevotes',
|
||||
type => 'b',
|
||||
default => 1
|
||||
},
|
||||
|
||||
{
|
||||
name => 'usebugaliases',
|
||||
type => 'b',
|
||||
default => 0
|
||||
},
|
||||
|
||||
{
|
||||
name => 'defaultpriority',
|
||||
type => 's',
|
||||
choices => \@::legal_priority,
|
||||
default => $::legal_priority[-1],
|
||||
checker => \&check_priority
|
||||
},
|
||||
|
||||
{
|
||||
name => 'defaultseverity',
|
||||
type => 's',
|
||||
choices => \@::legal_severity,
|
||||
default => $::legal_severity[-1],
|
||||
checker => \&check_severity
|
||||
},
|
||||
|
||||
{
|
||||
name => 'defaultplatform',
|
||||
type => 's',
|
||||
choices => ['', @::legal_platform],
|
||||
default => '',
|
||||
checker => \&check_platform
|
||||
},
|
||||
|
||||
{
|
||||
name => 'defaultopsys',
|
||||
type => 's',
|
||||
choices => ['', @::legal_opsys],
|
||||
default => '',
|
||||
checker => \&check_opsys
|
||||
} );
|
||||
return @param_list;
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -1,93 +0,0 @@
|
||||
# -*- 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>
|
||||
#
|
||||
|
||||
package Bugzilla::Config::BugMove;
|
||||
|
||||
use strict;
|
||||
|
||||
use Bugzilla::Config::Common;
|
||||
|
||||
$Bugzilla::Config::BugMove::sortkey = "05";
|
||||
|
||||
sub get_param_list {
|
||||
my $class = shift;
|
||||
my @param_list = (
|
||||
{
|
||||
name => 'move-enabled',
|
||||
type => 'b',
|
||||
default => 0
|
||||
},
|
||||
|
||||
{
|
||||
name => 'move-button-text',
|
||||
type => 't',
|
||||
default => 'Move To Bugscape'
|
||||
},
|
||||
|
||||
{
|
||||
name => 'move-to-url',
|
||||
type => 't',
|
||||
default => ''
|
||||
},
|
||||
|
||||
{
|
||||
name => 'move-to-address',
|
||||
type => 't',
|
||||
default => 'bugzilla-import'
|
||||
},
|
||||
|
||||
{
|
||||
name => 'moved-from-address',
|
||||
type => 't',
|
||||
default => 'bugzilla-admin'
|
||||
},
|
||||
|
||||
{
|
||||
name => 'movers',
|
||||
type => 't',
|
||||
default => ''
|
||||
},
|
||||
|
||||
{
|
||||
name => 'moved-default-product',
|
||||
type => 't',
|
||||
default => ''
|
||||
},
|
||||
|
||||
{
|
||||
name => 'moved-default-component',
|
||||
type => 't',
|
||||
default => ''
|
||||
} );
|
||||
return @param_list;
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -1,387 +0,0 @@
|
||||
# -*- 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>
|
||||
#
|
||||
|
||||
# This file defines all the parameters that we have a GUI to edit within
|
||||
# Bugzilla.
|
||||
|
||||
# ATTENTION!!!! THIS FILE ONLY CONTAINS THE DEFAULTS.
|
||||
# You cannot change your live settings by editing this file.
|
||||
# Only adding new parameters is done here. Once the parameter exists, you
|
||||
# must use %baseurl%/editparams.cgi from the web to edit the settings.
|
||||
|
||||
# This file is included via |do|, mainly because of circular dependency issues
|
||||
# (such as globals.pl -> Bugzilla::Config -> this -> Bugzilla::Config)
|
||||
# which preclude compile time loading.
|
||||
|
||||
# Those issues may go away at some point, and the contents of this file
|
||||
# moved somewhere else. Please try to avoid more dependencies from here
|
||||
# to other code
|
||||
|
||||
# (Note that these aren't just added directly to Bugzilla::Config, because
|
||||
# the backend prefs code is separate to this...)
|
||||
|
||||
package Bugzilla::Config::Common;
|
||||
|
||||
use strict;
|
||||
|
||||
use Socket;
|
||||
|
||||
use Bugzilla::Config qw(:DEFAULT $templatedir $webdotdir);
|
||||
use Bugzilla::Util;
|
||||
use Bugzilla::Constants;
|
||||
|
||||
use base qw(Exporter);
|
||||
@Bugzilla::Config::Common::EXPORT =
|
||||
qw(check_multi check_numeric check_regexp
|
||||
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_languages check_mail_delivery_method
|
||||
);
|
||||
|
||||
# Checking functions for the various values
|
||||
|
||||
sub check_multi {
|
||||
my ($value, $param) = (@_);
|
||||
|
||||
if ($param->{'type'} eq "s") {
|
||||
unless (scalar(grep {$_ eq $value} (@{$param->{'choices'}}))) {
|
||||
return "Invalid choice '$value' for single-select list param '$param->{'name'}'";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
elsif ($param->{'type'} eq "m") {
|
||||
foreach my $chkParam (@$value) {
|
||||
unless (scalar(grep {$_ eq $chkParam} (@{$param->{'choices'}}))) {
|
||||
return "Invalid choice '$chkParam' for multi-select list param '$param->{'name'}'";
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
else {
|
||||
return "Invalid param type '$param->{'type'}' for check_multi(); " .
|
||||
"contact your Bugzilla administrator";
|
||||
}
|
||||
}
|
||||
|
||||
sub check_numeric {
|
||||
my ($value) = (@_);
|
||||
if ($value !~ /^[0-9]+$/) {
|
||||
return "must be a numeric value";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
sub check_regexp {
|
||||
my ($value) = (@_);
|
||||
eval { qr/$value/ };
|
||||
return $@;
|
||||
}
|
||||
|
||||
sub check_sslbase {
|
||||
my $url = shift;
|
||||
if ($url ne '') {
|
||||
if ($url !~ m#^https://([^/]+).*/$#) {
|
||||
return "must be a legal URL, that starts with https and ends with a slash.";
|
||||
}
|
||||
my $host = $1;
|
||||
if ($host =~ /:\d+$/) {
|
||||
return "must not contain a port.";
|
||||
}
|
||||
local *SOCK;
|
||||
my $proto = getprotobyname('tcp');
|
||||
socket(SOCK, PF_INET, SOCK_STREAM, $proto);
|
||||
my $sin = sockaddr_in(443, inet_aton($host));
|
||||
if (!connect(SOCK, $sin)) {
|
||||
return "Failed to connect to " . html_quote($host) .
|
||||
":443, unable to enable SSL.";
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
sub check_priority {
|
||||
my ($value) = (@_);
|
||||
&::GetVersionTable();
|
||||
if (lsearch(\@::legal_priority, $value) < 0) {
|
||||
return "Must be a legal priority value: one of " .
|
||||
join(", ", @::legal_priority);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
sub check_severity {
|
||||
my ($value) = (@_);
|
||||
&::GetVersionTable();
|
||||
if (lsearch(\@::legal_severity, $value) < 0) {
|
||||
return "Must be a legal severity value: one of " .
|
||||
join(", ", @::legal_severity);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
sub check_platform {
|
||||
my ($value) = (@_);
|
||||
&::GetVersionTable();
|
||||
if (lsearch(['', @::legal_platform], $value) < 0) {
|
||||
return "Must be empty or a legal platform value: one of " .
|
||||
join(", ", @::legal_platform);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
sub check_opsys {
|
||||
my ($value) = (@_);
|
||||
&::GetVersionTable();
|
||||
if (lsearch(['', @::legal_opsys], $value) < 0) {
|
||||
return "Must be empty or a legal operating system value: one of " .
|
||||
join(", ", @::legal_opsys);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
sub check_shadowdb {
|
||||
my ($value) = (@_);
|
||||
$value = trim($value);
|
||||
if ($value eq "") {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (!Param('shadowdbhost')) {
|
||||
return "You need to specify a host when using a shadow database";
|
||||
}
|
||||
|
||||
# Can't test existence of this because ConnectToDatabase uses the param,
|
||||
# but we can't set this before testing....
|
||||
# This can really only be fixed after we can use the DBI more openly
|
||||
return "";
|
||||
}
|
||||
|
||||
sub check_urlbase {
|
||||
my ($url) = (@_);
|
||||
if ($url && $url !~ m:^http.*/$:) {
|
||||
return "must be a legal URL, that starts with http and ends with a slash.";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
sub check_webdotbase {
|
||||
my ($value) = (@_);
|
||||
$value = trim($value);
|
||||
if ($value eq "") {
|
||||
return "";
|
||||
}
|
||||
if($value !~ /^https?:/) {
|
||||
if(! -x $value) {
|
||||
return "The file path \"$value\" is not a valid executable. Please specify the complete file path to 'dot' if you intend to generate graphs locally.";
|
||||
}
|
||||
# Check .htaccess allows access to generated images
|
||||
if(-e "$webdotdir/.htaccess") {
|
||||
open HTACCESS, "$webdotdir/.htaccess";
|
||||
if(! grep(/ \\\.png\$/,<HTACCESS>)) {
|
||||
return "Dependency graph images are not accessible.\nAssuming that you have not modified the file, delete $webdotdir/.htaccess and re-run checksetup.pl to rectify.\n";
|
||||
}
|
||||
close HTACCESS;
|
||||
}
|
||||
}
|
||||
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
|
||||
# other params, it must be below that other one. So you can't have two
|
||||
# params mutually dependent on each other.
|
||||
# This means that if someone clears the LDAP config params after setting
|
||||
# the login method as LDAP, we won't notice, but all logins will fail.
|
||||
# So don't do that.
|
||||
|
||||
my ($list, $entry) = @_;
|
||||
for my $class (split /,\s*/, $list) {
|
||||
my $res = check_multi($class, $entry);
|
||||
return $res if $res;
|
||||
if ($class eq 'DB') {
|
||||
# No params
|
||||
} elsif ($class eq 'LDAP') {
|
||||
eval "require Net::LDAP";
|
||||
return "Error requiring Net::LDAP: '$@'" if $@;
|
||||
return "LDAP servername is missing" unless Param("LDAPserver");
|
||||
return "LDAPBaseDN is empty" unless Param("LDAPBaseDN");
|
||||
} else {
|
||||
return "Unknown user_verify_class '$class' in check_user_verify_class";
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
sub check_image_converter {
|
||||
my ($value, $hash) = @_;
|
||||
if ($value == 1){
|
||||
eval "require Image::Magick";
|
||||
return "Error requiring Image::Magick: '$@'" if $@;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
sub check_languages {
|
||||
my @languages = split /[,\s]+/, trim($_[0]);
|
||||
if(!scalar(@languages)) {
|
||||
return "You need to specify a language tag."
|
||||
}
|
||||
foreach my $language (@languages) {
|
||||
if( ! -d "$templatedir/$language/custom"
|
||||
&& ! -d "$templatedir/$language/default") {
|
||||
return "The template directory for $language does not exist";
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
sub check_mail_delivery_method {
|
||||
my $check = check_multi(@_);
|
||||
return $check if $check;
|
||||
my $mailer = shift;
|
||||
if ($mailer eq 'sendmail' && $^O =~ /MSWin32/i) {
|
||||
# look for sendmail.exe
|
||||
return "Failed to locate " . SENDMAIL_EXE
|
||||
unless -e SENDMAIL_EXE;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
# OK, here are the parameter definitions themselves.
|
||||
#
|
||||
# Each definition is a hash with keys:
|
||||
#
|
||||
# name - name of the param
|
||||
# desc - description of the param (for editparams.cgi)
|
||||
# type - see below
|
||||
# choices - (optional) see below
|
||||
# default - default value for the param
|
||||
# checker - (optional) checking function for validating parameter entry
|
||||
# It is called with the value of the param as the first arg and a
|
||||
# reference to the param's hash as the second argument
|
||||
#
|
||||
# The type value can be one of the following:
|
||||
#
|
||||
# t -- A short text entry field (suitable for a single line)
|
||||
# l -- A long text field (suitable for many lines)
|
||||
# b -- A boolean value (either 1 or 0)
|
||||
# m -- A list of values, with many selectable (shows up as a select box)
|
||||
# To specify the list of values, make the 'choices' key be an array
|
||||
# reference of the valid choices. The 'default' key should be an array
|
||||
# reference for the list of selected values (which must appear in the
|
||||
# first anonymous array), i.e.:
|
||||
# {
|
||||
# name => 'multiselect',
|
||||
# desc => 'A list of options, choose many',
|
||||
# type => 'm',
|
||||
# choices => [ 'a', 'b', 'c', 'd' ],
|
||||
# default => [ 'a', 'd' ],
|
||||
# checker => \&check_multi
|
||||
# }
|
||||
#
|
||||
# Here, 'a' and 'd' are the default options, and the user may pick any
|
||||
# combination of a, b, c, and d as valid options.
|
||||
#
|
||||
# &check_multi should always be used as the param verification function
|
||||
# for list (single and multiple) parameter types.
|
||||
#
|
||||
# s -- A list of values, with one selectable (shows up as a select box)
|
||||
# To specify the list of values, make the 'choices' key be an array
|
||||
# reference of the valid choices. The 'default' key should be one of
|
||||
# those values, i.e.:
|
||||
# {
|
||||
# name => 'singleselect',
|
||||
# desc => 'A list of options, choose one',
|
||||
# type => 's',
|
||||
# choices => [ 'a', 'b', 'c' ],
|
||||
# default => 'b',
|
||||
# checker => \&check_multi
|
||||
# }
|
||||
#
|
||||
# Here, 'b' is the default option, and 'a' and 'c' are other possible
|
||||
# options, but only one at a time!
|
||||
#
|
||||
# &check_multi should always be used as the param verification function
|
||||
# for list (single and multiple) parameter types.
|
||||
|
||||
sub get_param_list {
|
||||
return;
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::Config::Common - Parameter checking functions
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
All parameter checking functions are called with two parameters:
|
||||
|
||||
=head2 Functions
|
||||
|
||||
=over
|
||||
|
||||
=item C<check_multi>
|
||||
|
||||
Checks that a multi-valued parameter (ie type C<s> or type C<m>) satisfies
|
||||
its contraints.
|
||||
|
||||
=item C<check_numeric>
|
||||
|
||||
Checks that the value is a valid number
|
||||
|
||||
=item C<check_regexp>
|
||||
|
||||
Checks that the value is a valid regexp
|
||||
|
||||
=back
|
||||
@@ -1,103 +0,0 @@
|
||||
# -*- 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>
|
||||
#
|
||||
|
||||
package Bugzilla::Config::Core;
|
||||
|
||||
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'
|
||||
},
|
||||
|
||||
{
|
||||
name => 'urlbase',
|
||||
type => 't',
|
||||
default => '',
|
||||
checker => \&check_urlbase
|
||||
},
|
||||
|
||||
{
|
||||
name => 'sslbase',
|
||||
type => 't',
|
||||
default => '',
|
||||
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 => 'timezone',
|
||||
type => 't',
|
||||
default => '',
|
||||
},
|
||||
|
||||
{
|
||||
name => 'utf8',
|
||||
type => 'b',
|
||||
default => '0',
|
||||
},
|
||||
|
||||
{
|
||||
name => 'shutdownhtml',
|
||||
type => 'l',
|
||||
default => ''
|
||||
} );
|
||||
return @param_list;
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -1,52 +0,0 @@
|
||||
# -*- 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>
|
||||
#
|
||||
|
||||
package Bugzilla::Config::DependencyGraph;
|
||||
|
||||
use strict;
|
||||
|
||||
use Bugzilla::Config::Common;
|
||||
|
||||
$Bugzilla::Config::DependencyGraph::sortkey = "06";
|
||||
|
||||
sub get_param_list {
|
||||
my $class = shift;
|
||||
my @param_list = (
|
||||
{
|
||||
name => 'webdotbase',
|
||||
type => 't',
|
||||
default => 'http://www.research.att.com/~north/cgi-bin/webdot.cgi/%urlbase%',
|
||||
checker => \&check_webdotbase
|
||||
} );
|
||||
return @param_list;
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -1,87 +0,0 @@
|
||||
# -*- 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>
|
||||
#
|
||||
|
||||
package Bugzilla::Config::GroupSecurity;
|
||||
|
||||
use strict;
|
||||
|
||||
use Bugzilla::Config::Common;
|
||||
|
||||
$Bugzilla::Config::GroupSecurity::sortkey = "07";
|
||||
|
||||
sub get_param_list {
|
||||
my $class = shift;
|
||||
my @param_list = (
|
||||
{
|
||||
name => 'makeproductgroups',
|
||||
type => 'b',
|
||||
default => 0
|
||||
},
|
||||
|
||||
{
|
||||
name => 'useentrygroupdefault',
|
||||
type => 'b',
|
||||
default => 0
|
||||
},
|
||||
|
||||
{
|
||||
name => 'chartgroup',
|
||||
type => 't',
|
||||
default => 'editbugs'
|
||||
},
|
||||
|
||||
{
|
||||
name => 'insidergroup',
|
||||
type => 't',
|
||||
default => ''
|
||||
},
|
||||
|
||||
{
|
||||
name => 'timetrackinggroup',
|
||||
type => 't',
|
||||
default => ''
|
||||
},
|
||||
|
||||
{
|
||||
name => 'usevisibilitygroups',
|
||||
type => 'b',
|
||||
default => 0
|
||||
},
|
||||
|
||||
{
|
||||
name => 'strict_isolation',
|
||||
type => 'b',
|
||||
default => 0
|
||||
} );
|
||||
return @param_list;
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -1,78 +0,0 @@
|
||||
# -*- 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>
|
||||
#
|
||||
|
||||
package Bugzilla::Config::L10n;
|
||||
|
||||
use strict;
|
||||
|
||||
use File::Spec; # for find_languages
|
||||
|
||||
use Bugzilla::Config qw($templatedir);
|
||||
use Bugzilla::Config::Common;
|
||||
|
||||
$Bugzilla::Config::L10n::sortkey = "08";
|
||||
|
||||
sub get_param_list {
|
||||
my $class = shift;
|
||||
my @param_list = (
|
||||
{
|
||||
name => 'languages' ,
|
||||
extra_desc => { available_languages => find_languages() },
|
||||
type => 't' ,
|
||||
default => 'en' ,
|
||||
checker => \&check_languages
|
||||
},
|
||||
|
||||
{
|
||||
name => 'defaultlanguage',
|
||||
type => 't' ,
|
||||
default => 'en' ,
|
||||
checker => \&check_languages
|
||||
} );
|
||||
return @param_list;
|
||||
}
|
||||
|
||||
sub find_languages {
|
||||
my @languages = ();
|
||||
opendir(DIR, $templatedir) || return "Can't open 'template' directory: $!";
|
||||
foreach my $dir (readdir(DIR)) {
|
||||
next unless $dir =~ /^([a-z-]+)$/i;
|
||||
my $lang = $1;
|
||||
next if($lang =~ /^CVS$/i);
|
||||
my $deft_path = File::Spec->catdir('template', $lang, 'default');
|
||||
my $cust_path = File::Spec->catdir('template', $lang, 'custom');
|
||||
push(@languages, $lang) if(-d $deft_path or -d $cust_path);
|
||||
}
|
||||
closedir DIR;
|
||||
return join(', ', @languages);
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -1,81 +0,0 @@
|
||||
# -*- 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>
|
||||
#
|
||||
|
||||
package Bugzilla::Config::LDAP;
|
||||
|
||||
use strict;
|
||||
|
||||
use Bugzilla::Config::Common;
|
||||
|
||||
$Bugzilla::Config::LDAP::sortkey = "09";
|
||||
|
||||
sub get_param_list {
|
||||
my $class = shift;
|
||||
my @param_list = (
|
||||
{
|
||||
name => 'LDAPserver',
|
||||
type => 't',
|
||||
default => ''
|
||||
},
|
||||
|
||||
{
|
||||
name => 'LDAPbinddn',
|
||||
type => 't',
|
||||
default => ''
|
||||
},
|
||||
|
||||
{
|
||||
name => 'LDAPBaseDN',
|
||||
type => 't',
|
||||
default => ''
|
||||
},
|
||||
|
||||
{
|
||||
name => 'LDAPuidattribute',
|
||||
type => 't',
|
||||
default => 'uid'
|
||||
},
|
||||
|
||||
{
|
||||
name => 'LDAPmailattribute',
|
||||
type => 't',
|
||||
default => 'mail'
|
||||
},
|
||||
|
||||
{
|
||||
name => 'LDAPfilter',
|
||||
type => 't',
|
||||
default => '',
|
||||
} );
|
||||
return @param_list;
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -1,176 +0,0 @@
|
||||
# -*- 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>
|
||||
#
|
||||
|
||||
package Bugzilla::Config::MTA;
|
||||
|
||||
use strict;
|
||||
|
||||
use Bugzilla::Config::Common;
|
||||
|
||||
$Bugzilla::Config::MTA::sortkey = "10";
|
||||
|
||||
sub get_param_list {
|
||||
my $class = shift;
|
||||
my @param_list = (
|
||||
{
|
||||
name => 'mail_delivery_method',
|
||||
type => 's',
|
||||
choices => $^O =~ /MSWin32/i
|
||||
? ['smtp', 'testfile', 'sendmail', 'none']
|
||||
: ['sendmail', 'smtp', 'qmail', 'testfile', 'none'],
|
||||
default => 'sendmail',
|
||||
checker => \&check_mail_delivery_method
|
||||
},
|
||||
|
||||
{
|
||||
name => 'sendmailnow',
|
||||
type => 'b',
|
||||
default => 1
|
||||
},
|
||||
|
||||
{
|
||||
name => 'smtpserver',
|
||||
type => 't',
|
||||
default => 'localhost'
|
||||
},
|
||||
|
||||
{
|
||||
name => 'passwordmail',
|
||||
type => 'l',
|
||||
default => 'From: bugzilla-daemon
|
||||
To: %mailaddress%
|
||||
Subject: Your Bugzilla password.
|
||||
|
||||
To use the wonders of Bugzilla, you can use the following:
|
||||
|
||||
E-mail address: %login%
|
||||
Password: %password%
|
||||
|
||||
To change your password, go to:
|
||||
%urlbase%userprefs.cgi
|
||||
'
|
||||
},
|
||||
|
||||
{
|
||||
name => 'newchangedmail',
|
||||
type => 'l',
|
||||
default => 'From: bugzilla-daemon
|
||||
To: %to%
|
||||
Subject: [Bug %bugid%] %neworchanged%%summary%
|
||||
%threadingmarker%
|
||||
X-Bugzilla-Reason: %reasonsheader%
|
||||
X-Bugzilla-Product: %product%
|
||||
X-Bugzilla-Component: %component%
|
||||
X-Bugzilla-Keywords: %keywords%
|
||||
X-Bugzilla-Severity: %severity%
|
||||
|
||||
%urlbase%show_bug.cgi?id=%bugid%
|
||||
|
||||
%diffs%
|
||||
|
||||
--%space%
|
||||
Configure bugmail: %urlbase%userprefs.cgi?tab=email
|
||||
%reasonsbody%'
|
||||
},
|
||||
|
||||
{
|
||||
name => 'whinedays',
|
||||
type => 't',
|
||||
default => 7,
|
||||
checker => \&check_numeric
|
||||
},
|
||||
|
||||
{
|
||||
name => 'whinemail',
|
||||
type => 'l',
|
||||
default => 'From: %maintainer%
|
||||
To: %email%
|
||||
Subject: Your Bugzilla buglist needs attention.
|
||||
|
||||
[This e-mail has been automatically generated.]
|
||||
|
||||
You have one or more bugs assigned to you in the Bugzilla
|
||||
bugsystem (%urlbase%) that require
|
||||
attention.
|
||||
|
||||
All of these bugs are in the NEW or REOPENED state, and have not
|
||||
been touched in %whinedays% days or more. You need to take a look
|
||||
at them, and decide on an initial action.
|
||||
|
||||
Generally, this means one of three things:
|
||||
|
||||
(1) You decide this bug is really quick to deal with (like, it\'s INVALID),
|
||||
and so you get rid of it immediately.
|
||||
(2) You decide the bug doesn\'t belong to you, and you reassign it to someone
|
||||
else. (Hint: if you don\'t know who to reassign it to, make sure that
|
||||
the Component field seems reasonable, and then use the "Reassign bug to
|
||||
default assignee of selected component" option.)
|
||||
(3) You decide the bug belongs to you, but you can\'t solve it this moment.
|
||||
Just use the "Accept bug" command.
|
||||
|
||||
To get a list of all NEW/REOPENED bugs, you can use this URL (bookmark
|
||||
it if you like!):
|
||||
|
||||
%urlbase%buglist.cgi?bug_status=NEW&bug_status=REOPENED&assigned_to=%userid%
|
||||
|
||||
Or, you can use the general query page, at
|
||||
%urlbase%query.cgi
|
||||
|
||||
Appended below are the individual URLs to get to all of your NEW bugs that
|
||||
haven\'t been touched for a week or more.
|
||||
|
||||
You will get this message once a day until you\'ve dealt with these bugs!
|
||||
|
||||
'
|
||||
},
|
||||
|
||||
{
|
||||
name => 'voteremovedmail',
|
||||
type => 'l',
|
||||
default => 'From: bugzilla-daemon
|
||||
To: %to%
|
||||
Subject: [Bug %bugid%] Some or all of your votes have been removed.
|
||||
|
||||
Some or all of your votes have been removed from bug %bugid%.
|
||||
|
||||
%votesoldtext%
|
||||
|
||||
%votesnewtext%
|
||||
|
||||
Reason: %reason%
|
||||
|
||||
%urlbase%show_bug.cgi?id=%bugid%
|
||||
'
|
||||
} );
|
||||
return @param_list;
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -1,75 +0,0 @@
|
||||
# -*- 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>
|
||||
#
|
||||
|
||||
package Bugzilla::Config::PatchViewer;
|
||||
|
||||
use strict;
|
||||
|
||||
use Bugzilla::Config::Common;
|
||||
|
||||
$Bugzilla::Config::PatchViewer::sortkey = "11";
|
||||
|
||||
sub get_param_list {
|
||||
my $class = shift;
|
||||
my @param_list = (
|
||||
{
|
||||
name => 'cvsroot',
|
||||
type => 't',
|
||||
default => '',
|
||||
},
|
||||
|
||||
{
|
||||
name => 'cvsroot_get',
|
||||
type => 't',
|
||||
default => '',
|
||||
},
|
||||
|
||||
{
|
||||
name => 'bonsai_url',
|
||||
type => 't',
|
||||
default => ''
|
||||
},
|
||||
|
||||
{
|
||||
name => 'lxr_url',
|
||||
type => 't',
|
||||
default => ''
|
||||
},
|
||||
|
||||
{
|
||||
name => 'lxr_root',
|
||||
type => 't',
|
||||
default => '',
|
||||
} );
|
||||
return @param_list;
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -1,79 +0,0 @@
|
||||
# -*- 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>
|
||||
#
|
||||
|
||||
package Bugzilla::Config::Query;
|
||||
|
||||
use strict;
|
||||
|
||||
use Bugzilla::Config::Common;
|
||||
|
||||
$Bugzilla::Config::Query::sortkey = "12";
|
||||
|
||||
sub get_param_list {
|
||||
my $class = shift;
|
||||
my @param_list = (
|
||||
{
|
||||
name => 'quip_list_entry_control',
|
||||
type => 's',
|
||||
choices => ['open', 'moderated', 'closed'],
|
||||
default => 'open',
|
||||
checker => \&check_multi
|
||||
},
|
||||
|
||||
{
|
||||
name => 'mostfreqthreshold',
|
||||
type => 't',
|
||||
default => '2',
|
||||
checker => \&check_numeric
|
||||
},
|
||||
|
||||
{
|
||||
name => 'mybugstemplate',
|
||||
type => 't',
|
||||
default => 'buglist.cgi?bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&emailassigned_to1=1&emailreporter1=1&emailtype1=exact&email1=%userid%&field0-0-0=bug_status&type0-0-0=notequals&value0-0-0=UNCONFIRMED&field0-0-1=reporter&type0-0-1=equals&value0-0-1=%userid%'
|
||||
},
|
||||
|
||||
{
|
||||
name => 'defaultquery',
|
||||
type => 't',
|
||||
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
|
||||
} );
|
||||
return @param_list;
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -1,73 +0,0 @@
|
||||
# -*- 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>
|
||||
#
|
||||
|
||||
package Bugzilla::Config::ShadowDB;
|
||||
|
||||
use strict;
|
||||
|
||||
use Bugzilla::Config::Common;
|
||||
|
||||
$Bugzilla::Config::ShadowDB::sortkey = "13";
|
||||
|
||||
sub get_param_list {
|
||||
my $class = shift;
|
||||
my @param_list = (
|
||||
{
|
||||
name => 'shadowdbhost',
|
||||
type => 't',
|
||||
default => '',
|
||||
},
|
||||
|
||||
{
|
||||
name => 'shadowdbport',
|
||||
type => 't',
|
||||
default => '3306',
|
||||
checker => \&check_numeric,
|
||||
},
|
||||
|
||||
{
|
||||
name => 'shadowdbsock',
|
||||
type => 't',
|
||||
default => '',
|
||||
},
|
||||
|
||||
# This entry must be _after_ the shadowdb{host,port,sock} settings so that
|
||||
# they can be used in the validation here
|
||||
{
|
||||
name => 'shadowdb',
|
||||
type => 't',
|
||||
default => '',
|
||||
checker => \&check_shadowdb
|
||||
} );
|
||||
return @param_list;
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -1,71 +0,0 @@
|
||||
# -*- 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>
|
||||
#
|
||||
|
||||
package Bugzilla::Config::UserMatch;
|
||||
|
||||
use strict;
|
||||
|
||||
use Bugzilla::Config::Common;
|
||||
|
||||
$Bugzilla::Config::UserMatch::sortkey = "14";
|
||||
|
||||
sub get_param_list {
|
||||
my $class = shift;
|
||||
my @param_list = (
|
||||
{
|
||||
name => 'usemenuforusers',
|
||||
type => 'b',
|
||||
default => '0'
|
||||
},
|
||||
|
||||
{
|
||||
name => 'usermatchmode',
|
||||
type => 's',
|
||||
choices => ['off', 'wildcard', 'search'],
|
||||
default => 'off'
|
||||
},
|
||||
|
||||
{
|
||||
name => 'maxusermatches',
|
||||
type => 't',
|
||||
default => '1000',
|
||||
checker => \&check_numeric
|
||||
},
|
||||
|
||||
{
|
||||
name => 'confirmuniqueusermatch',
|
||||
type => 'b',
|
||||
default => 1,
|
||||
} );
|
||||
return @param_list;
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -1,253 +0,0 @@
|
||||
# -*- 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>
|
||||
# Jake <jake@bugzilla.org>
|
||||
# J. Paul Reed <preed@sigkill.com>
|
||||
# Bradley Baetz <bbaetz@student.usyd.edu.au>
|
||||
# Christopher Aillon <christopher@aillon.com>
|
||||
# Shane H. W. Travis <travis@sedsystems.ca>
|
||||
# Max Kanat-Alexander <mkanat@bugzilla.org>
|
||||
|
||||
package Bugzilla::Constants;
|
||||
use strict;
|
||||
use base qw(Exporter);
|
||||
|
||||
@Bugzilla::Constants::EXPORT = qw(
|
||||
CONTROLMAPNA
|
||||
CONTROLMAPSHOWN
|
||||
CONTROLMAPDEFAULT
|
||||
CONTROLMAPMANDATORY
|
||||
|
||||
AUTH_OK
|
||||
AUTH_NODATA
|
||||
AUTH_ERROR
|
||||
AUTH_LOGINFAILED
|
||||
AUTH_DISABLED
|
||||
|
||||
LOGIN_OPTIONAL
|
||||
LOGIN_NORMAL
|
||||
LOGIN_REQUIRED
|
||||
|
||||
LOGOUT_ALL
|
||||
LOGOUT_CURRENT
|
||||
LOGOUT_KEEP_CURRENT
|
||||
|
||||
GRANT_DIRECT
|
||||
GRANT_REGEXP
|
||||
|
||||
GROUP_MEMBERSHIP
|
||||
GROUP_BLESS
|
||||
GROUP_VISIBLE
|
||||
|
||||
MAILTO_USER
|
||||
MAILTO_GROUP
|
||||
|
||||
DEFAULT_COLUMN_LIST
|
||||
DEFAULT_QUERY_NAME
|
||||
|
||||
QUERY_LIST
|
||||
LIST_OF_BUGS
|
||||
|
||||
COMMENT_COLS
|
||||
|
||||
UNLOCK_ABORT
|
||||
|
||||
RELATIONSHIPS
|
||||
REL_ASSIGNEE REL_QA REL_REPORTER REL_CC REL_VOTER
|
||||
REL_ANY
|
||||
|
||||
POS_EVENTS
|
||||
EVT_OTHER EVT_ADDED_REMOVED EVT_COMMENT EVT_ATTACHMENT EVT_ATTACHMENT_DATA
|
||||
EVT_PROJ_MANAGEMENT EVT_OPENED_CLOSED EVT_KEYWORD EVT_CC EVT_DEPEND_BLOCK
|
||||
|
||||
NEG_EVENTS
|
||||
EVT_UNCONFIRMED EVT_CHANGED_BY_ME
|
||||
|
||||
GLOBAL_EVENTS
|
||||
EVT_FLAG_REQUESTED EVT_REQUESTED_FLAG
|
||||
|
||||
FULLTEXT_BUGLIST_LIMIT
|
||||
|
||||
ADMIN_GROUP_NAME
|
||||
|
||||
SENDMAIL_EXE
|
||||
|
||||
SAFE_PROTOCOLS
|
||||
);
|
||||
|
||||
@Bugzilla::Constants::EXPORT_OK = qw(contenttypes);
|
||||
|
||||
# CONSTANTS
|
||||
#
|
||||
# ControlMap constants for group_control_map.
|
||||
# membercontol:othercontrol => meaning
|
||||
# Na:Na => Bugs in this product may not be restricted to this
|
||||
# group.
|
||||
# Shown:Na => Members of the group may restrict bugs
|
||||
# in this product to this group.
|
||||
# Shown:Shown => Members of the group may restrict bugs
|
||||
# in this product to this group.
|
||||
# Anyone who can enter bugs in this product may initially
|
||||
# restrict bugs in this product to this group.
|
||||
# Shown:Mandatory => Members of the group may restrict bugs
|
||||
# in this product to this group.
|
||||
# Non-members who can enter bug in this product
|
||||
# will be forced to restrict it.
|
||||
# Default:Na => Members of the group may restrict bugs in this
|
||||
# product to this group and do so by default.
|
||||
# Default:Default => Members of the group may restrict bugs in this
|
||||
# product to this group and do so by default and
|
||||
# nonmembers have this option on entry.
|
||||
# Default:Mandatory => Members of the group may restrict bugs in this
|
||||
# product to this group and do so by default.
|
||||
# Non-members who can enter bug in this product
|
||||
# will be forced to restrict it.
|
||||
# Mandatory:Mandatory => Bug will be forced into this group regardless.
|
||||
# All other combinations are illegal.
|
||||
|
||||
use constant CONTROLMAPNA => 0;
|
||||
use constant CONTROLMAPSHOWN => 1;
|
||||
use constant CONTROLMAPDEFAULT => 2;
|
||||
use constant CONTROLMAPMANDATORY => 3;
|
||||
|
||||
# See Bugzilla::Auth for docs on AUTH_*, LOGIN_* and LOGOUT_*
|
||||
|
||||
use constant AUTH_OK => 0;
|
||||
use constant AUTH_NODATA => 1;
|
||||
use constant AUTH_ERROR => 2;
|
||||
use constant AUTH_LOGINFAILED => 3;
|
||||
use constant AUTH_DISABLED => 4;
|
||||
|
||||
use constant LOGIN_OPTIONAL => 0;
|
||||
use constant LOGIN_NORMAL => 1;
|
||||
use constant LOGIN_REQUIRED => 2;
|
||||
|
||||
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/plain" ,
|
||||
"png" => "image/png" ,
|
||||
"ics" => "text/calendar" ,
|
||||
};
|
||||
|
||||
use constant GRANT_DIRECT => 0;
|
||||
use constant GRANT_REGEXP => 2;
|
||||
|
||||
use constant GROUP_MEMBERSHIP => 0;
|
||||
use constant GROUP_BLESS => 1;
|
||||
use constant GROUP_VISIBLE => 2;
|
||||
|
||||
use constant MAILTO_USER => 0;
|
||||
use constant MAILTO_GROUP => 1;
|
||||
|
||||
# The default list of columns for buglist.cgi
|
||||
use constant DEFAULT_COLUMN_LIST => (
|
||||
"bug_severity", "priority", "op_sys","assigned_to",
|
||||
"bug_status", "resolution", "short_short_desc"
|
||||
);
|
||||
|
||||
# Used by query.cgi and buglist.cgi as the named-query name
|
||||
# for the default settings.
|
||||
use constant DEFAULT_QUERY_NAME => '(Default query)';
|
||||
|
||||
# The possible types for saved searches.
|
||||
use constant QUERY_LIST => 0;
|
||||
use constant LIST_OF_BUGS => 1;
|
||||
|
||||
# The column length for displayed (and wrapped) bug comments.
|
||||
use constant COMMENT_COLS => 80;
|
||||
|
||||
# used by Bugzilla::DB to indicate that tables are being unlocked
|
||||
# because of error
|
||||
use constant UNLOCK_ABORT => 1;
|
||||
|
||||
use constant REL_ASSIGNEE => 0;
|
||||
use constant REL_QA => 1;
|
||||
use constant REL_REPORTER => 2;
|
||||
use constant REL_CC => 3;
|
||||
use constant REL_VOTER => 4;
|
||||
|
||||
use constant RELATIONSHIPS => REL_ASSIGNEE, REL_QA, REL_REPORTER, REL_CC,
|
||||
REL_VOTER;
|
||||
|
||||
# Used for global events like EVT_FLAG_REQUESTED
|
||||
use constant REL_ANY => 100;
|
||||
|
||||
# There are two sorts of event - positive and negative. Positive events are
|
||||
# those for which the user says "I want mail if this happens." Negative events
|
||||
# are those for which the user says "I don't want mail if this happens."
|
||||
#
|
||||
# Exactly when each event fires is defined in wants_bug_mail() in User.pm; I'm
|
||||
# not commenting them here in case the comments and the code get out of sync.
|
||||
use constant EVT_OTHER => 0;
|
||||
use constant EVT_ADDED_REMOVED => 1;
|
||||
use constant EVT_COMMENT => 2;
|
||||
use constant EVT_ATTACHMENT => 3;
|
||||
use constant EVT_ATTACHMENT_DATA => 4;
|
||||
use constant EVT_PROJ_MANAGEMENT => 5;
|
||||
use constant EVT_OPENED_CLOSED => 6;
|
||||
use constant EVT_KEYWORD => 7;
|
||||
use constant EVT_CC => 8;
|
||||
use constant EVT_DEPEND_BLOCK => 9;
|
||||
|
||||
use constant POS_EVENTS => EVT_OTHER, EVT_ADDED_REMOVED, EVT_COMMENT,
|
||||
EVT_ATTACHMENT, EVT_ATTACHMENT_DATA,
|
||||
EVT_PROJ_MANAGEMENT, EVT_OPENED_CLOSED, EVT_KEYWORD,
|
||||
EVT_CC, EVT_DEPEND_BLOCK;
|
||||
|
||||
use constant EVT_UNCONFIRMED => 50;
|
||||
use constant EVT_CHANGED_BY_ME => 51;
|
||||
|
||||
use constant NEG_EVENTS => EVT_UNCONFIRMED, EVT_CHANGED_BY_ME;
|
||||
|
||||
# These are the "global" flags, which aren't tied to a particular relationship.
|
||||
# and so use REL_ANY.
|
||||
use constant EVT_FLAG_REQUESTED => 100; # Flag has been requested of me
|
||||
use constant EVT_REQUESTED_FLAG => 101; # I have requested a flag
|
||||
|
||||
use constant GLOBAL_EVENTS => EVT_FLAG_REQUESTED, EVT_REQUESTED_FLAG;
|
||||
|
||||
# Number of bugs to return in a buglist when performing
|
||||
# a fulltext search.
|
||||
use constant FULLTEXT_BUGLIST_LIMIT => 200;
|
||||
|
||||
# Default administration group name.
|
||||
use constant ADMIN_GROUP_NAME => 'admin';
|
||||
|
||||
# Path to sendmail.exe (Windows only)
|
||||
use constant SENDMAIL_EXE => '/usr/lib/sendmail.exe';
|
||||
|
||||
# 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');
|
||||
|
||||
1;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,758 +0,0 @@
|
||||
# -*- 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): Dave Miller <davem00@aol.com>
|
||||
# Gayathri Swaminath <gayathrik00@aol.com>
|
||||
# Jeroen Ruigrok van der Werven <asmodai@wxs.nl>
|
||||
# Dave Lawrence <dkl@redhat.com>
|
||||
# Tomas Kopal <Tomas.Kopal@altap.cz>
|
||||
# Max Kanat-Alexander <mkanat@bugzilla.org>
|
||||
# Lance Larsh <lance.larsh@oracle.com>
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::DB::Mysql - Bugzilla database compatibility layer for MySQL
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
This module overrides methods of the Bugzilla::DB module with MySQL specific
|
||||
implementation. It is instantiated by the Bugzilla::DB module and should never
|
||||
be used directly.
|
||||
|
||||
For interface details see L<Bugzilla::DB> and L<DBI>.
|
||||
|
||||
=cut
|
||||
|
||||
package Bugzilla::DB::Mysql;
|
||||
|
||||
use strict;
|
||||
|
||||
use Bugzilla::Util;
|
||||
use Bugzilla::Error;
|
||||
|
||||
# This module extends the DB interface via inheritance
|
||||
use base qw(Bugzilla::DB);
|
||||
|
||||
use constant REQUIRED_VERSION => '4.0.14';
|
||||
use constant PROGRAM_NAME => 'MySQL';
|
||||
use constant MODULE_NAME => 'Mysql';
|
||||
use constant DBD_VERSION => '2.9003';
|
||||
|
||||
sub new {
|
||||
my ($class, $user, $pass, $host, $dbname, $port, $sock) = @_;
|
||||
|
||||
# construct the DSN from the parameters we got
|
||||
my $dsn = "DBI:mysql:host=$host;database=$dbname";
|
||||
$dsn .= ";port=$port" if $port;
|
||||
$dsn .= ";mysql_socket=$sock" if $sock;
|
||||
|
||||
my $self = $class->db_new($dsn, $user, $pass);
|
||||
|
||||
# all class local variables stored in DBI derived class needs to have
|
||||
# a prefix 'private_'. See DBI documentation.
|
||||
$self->{private_bz_tables_locked} = "";
|
||||
|
||||
bless ($self, $class);
|
||||
|
||||
# Bug 321645 - disable MySQL strict mode, if set
|
||||
my ($var, $sql_mode) = $self->selectrow_array(
|
||||
"SHOW VARIABLES LIKE 'sql\\_mode'");
|
||||
|
||||
if ($sql_mode) {
|
||||
# STRICT_TRANS_TABLE or STRICT_ALL_TABLES enable MySQL strict mode,
|
||||
# causing bug 321645. TRADITIONAL sets these modes (among others) as
|
||||
# well, so it has to be stipped as well
|
||||
my $new_sql_mode =
|
||||
join(",", grep {$_ !~ /^STRICT_(?:TRANS|ALL)_TABLES|TRADITIONAL$/}
|
||||
split(/,/, $sql_mode));
|
||||
|
||||
if ($sql_mode ne $new_sql_mode) {
|
||||
$self->do("SET SESSION sql_mode = ?", undef, $new_sql_mode);
|
||||
}
|
||||
}
|
||||
|
||||
return $self;
|
||||
}
|
||||
|
||||
# when last_insert_id() is supported on MySQL by lowest DBI/DBD version
|
||||
# required by Bugzilla, this implementation can be removed.
|
||||
sub bz_last_key {
|
||||
my ($self) = @_;
|
||||
|
||||
my ($last_insert_id) = $self->selectrow_array('SELECT LAST_INSERT_ID()');
|
||||
|
||||
return $last_insert_id;
|
||||
}
|
||||
|
||||
sub sql_regexp {
|
||||
my ($self, $expr, $pattern) = @_;
|
||||
|
||||
return "$expr REGEXP $pattern";
|
||||
}
|
||||
|
||||
sub sql_not_regexp {
|
||||
my ($self, $expr, $pattern) = @_;
|
||||
|
||||
return "$expr NOT REGEXP $pattern";
|
||||
}
|
||||
|
||||
sub sql_limit {
|
||||
my ($self, $limit, $offset) = @_;
|
||||
|
||||
if (defined($offset)) {
|
||||
return "LIMIT $offset, $limit";
|
||||
} else {
|
||||
return "LIMIT $limit";
|
||||
}
|
||||
}
|
||||
|
||||
sub sql_string_concat {
|
||||
my ($self, @params) = @_;
|
||||
|
||||
return 'CONCAT(' . join(', ', @params) . ')';
|
||||
}
|
||||
|
||||
sub sql_fulltext_search {
|
||||
my ($self, $column, $text) = @_;
|
||||
|
||||
# Add the boolean mode modifier if the search string contains
|
||||
# boolean operators.
|
||||
my $mode = ($text =~ /[+-<>()~*"]/ ? "IN BOOLEAN MODE" : "");
|
||||
|
||||
# quote the text for use in the MATCH AGAINST expression
|
||||
$text = $self->quote($text);
|
||||
|
||||
# untaint the text, since it's safe to use now that we've quoted it
|
||||
trick_taint($text);
|
||||
|
||||
return "MATCH($column) AGAINST($text $mode)";
|
||||
}
|
||||
|
||||
sub sql_istring {
|
||||
my ($self, $string) = @_;
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
sub sql_from_days {
|
||||
my ($self, $days) = @_;
|
||||
|
||||
return "FROM_DAYS($days)";
|
||||
}
|
||||
|
||||
sub sql_to_days {
|
||||
my ($self, $date) = @_;
|
||||
|
||||
return "TO_DAYS($date)";
|
||||
}
|
||||
|
||||
sub sql_date_format {
|
||||
my ($self, $date, $format) = @_;
|
||||
|
||||
$format = "%Y.%m.%d %H:%i:%s" if !$format;
|
||||
|
||||
return "DATE_FORMAT($date, " . $self->quote($format) . ")";
|
||||
}
|
||||
|
||||
sub sql_interval {
|
||||
my ($self, $interval, $units) = @_;
|
||||
|
||||
return "INTERVAL $interval $units";
|
||||
}
|
||||
|
||||
sub sql_position {
|
||||
my ($self, $fragment, $text) = @_;
|
||||
|
||||
# mysql 4.0.1 and lower do not support CAST
|
||||
# mysql 3.*.* had a case-sensitive INSTR
|
||||
# (checksetup has a check for unsupported versions)
|
||||
my $server_version = $self->bz_server_version;
|
||||
if ($server_version =~ /^3\./) {
|
||||
return "INSTR($text, $fragment)";
|
||||
} else {
|
||||
return "INSTR(CAST($text AS BINARY), CAST($fragment AS BINARY))";
|
||||
}
|
||||
}
|
||||
|
||||
sub sql_group_by {
|
||||
my ($self, $needed_columns, $optional_columns) = @_;
|
||||
|
||||
# MySQL allows to specify all columns as ANSI SQL requires, but also
|
||||
# allow you to specify just minimal subset to get unique result.
|
||||
# According to MySQL documentation, the less columns you specify
|
||||
# the faster the query runs.
|
||||
return "GROUP BY $needed_columns";
|
||||
}
|
||||
|
||||
|
||||
sub bz_lock_tables {
|
||||
my ($self, @tables) = @_;
|
||||
|
||||
my $list = join(', ', @tables);
|
||||
# Check first if there was no lock before
|
||||
if ($self->{private_bz_tables_locked}) {
|
||||
ThrowCodeError("already_locked", { current => $self->{private_bz_tables_locked},
|
||||
new => $list });
|
||||
} else {
|
||||
$self->do('LOCK TABLE ' . $list);
|
||||
|
||||
$self->{private_bz_tables_locked} = $list;
|
||||
}
|
||||
}
|
||||
|
||||
sub bz_unlock_tables {
|
||||
my ($self, $abort) = @_;
|
||||
|
||||
# Check first if there was previous matching lock
|
||||
if (!$self->{private_bz_tables_locked}) {
|
||||
# Abort is allowed even without previous lock for error handling
|
||||
return if $abort;
|
||||
ThrowCodeError("no_matching_lock");
|
||||
} else {
|
||||
$self->do("UNLOCK TABLES");
|
||||
|
||||
$self->{private_bz_tables_locked} = "";
|
||||
}
|
||||
}
|
||||
|
||||
# As Bugzilla currently runs on MyISAM storage, which does not support
|
||||
# transactions, these functions die when called.
|
||||
# Maybe we should just ignore these calls for now, but as we are not
|
||||
# using transactions in MySQL yet, this just hints the developers.
|
||||
sub bz_start_transaction {
|
||||
die("Attempt to start transaction on DB without transaction support");
|
||||
}
|
||||
|
||||
sub bz_commit_transaction {
|
||||
die("Attempt to commit transaction on DB without transaction support");
|
||||
}
|
||||
|
||||
sub bz_rollback_transaction {
|
||||
die("Attempt to rollback transaction on DB without transaction support");
|
||||
}
|
||||
|
||||
|
||||
sub _bz_get_initial_schema {
|
||||
my ($self) = @_;
|
||||
return $self->_bz_build_schema_from_disk();
|
||||
}
|
||||
|
||||
#####################################################################
|
||||
# Database Setup
|
||||
#####################################################################
|
||||
|
||||
sub bz_setup_database {
|
||||
my ($self) = @_;
|
||||
|
||||
# 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 $sth = $self->prepare("SHOW TABLE STATUS");
|
||||
$sth->execute;
|
||||
my @isam_tables = ();
|
||||
while (my ($name, $type) = $sth->fetchrow_array) {
|
||||
push(@isam_tables, $name) if $type eq "ISAM";
|
||||
}
|
||||
|
||||
if(scalar(@isam_tables)) {
|
||||
print "One or more of the tables in your existing MySQL database are\n"
|
||||
. "of type ISAM. ISAM tables are deprecated in MySQL 3.23 and\n"
|
||||
. "don't support more than 16 indexes per table, which \n"
|
||||
. "Bugzilla needs.\n Converting your ISAM tables to type"
|
||||
. " MyISAM:\n\n";
|
||||
foreach my $table (@isam_tables) {
|
||||
print "Converting table $table... ";
|
||||
$self->do("ALTER TABLE $table TYPE = MYISAM");
|
||||
print "done.\n";
|
||||
}
|
||||
print "\nISAM->MyISAM table conversion done.\n\n";
|
||||
}
|
||||
|
||||
# There is a bug in MySQL 4.1.0 - 4.1.15 that makes certain SELECT
|
||||
# statements fail after a SHOW TABLE STATUS:
|
||||
# http://bugs.mysql.com/bug.php?id=13535
|
||||
# This is a workaround, a dummy SELECT to reset the LAST_INSERT_ID.
|
||||
my @tables = $self->bz_table_list_real();
|
||||
if (grep($_ eq 'bugs', @tables)
|
||||
&& $self->bz_column_info_real("bugs", "bug_id"))
|
||||
{
|
||||
$self->do('SELECT 1 FROM bugs WHERE bug_id IS NULL');
|
||||
}
|
||||
|
||||
# Versions of Bugzilla before the existence of Bugzilla::DB::Schema did
|
||||
# not provide explicit names for the table indexes. This means
|
||||
# that our upgrades will not be reliable, because we look for the name
|
||||
# of the index, not what fields it is on, when doing upgrades.
|
||||
# (using the name is much better for cross-database compatibility
|
||||
# and general reliability). It's also very important that our
|
||||
# Schema object be consistent with what is on the disk.
|
||||
#
|
||||
# While we're at it, we also fix some inconsistent index naming
|
||||
# from the original checkin of Bugzilla::DB::Schema.
|
||||
|
||||
# We check for the existence of a particular "short name" index that
|
||||
# has existed at least since Bugzilla 2.8, and probably earlier.
|
||||
# For fixing the inconsistent naming of Schema indexes,
|
||||
# we also check for one of those inconsistently-named indexes.
|
||||
if (grep($_ eq 'bugs', @tables)
|
||||
&& ($self->bz_index_info_real('bugs', 'assigned_to')
|
||||
|| $self->bz_index_info_real('flags', 'flags_bidattid_idx')) )
|
||||
{
|
||||
|
||||
# This is a check unrelated to the indexes, to see if people are
|
||||
# upgrading from 2.18 or below, but somehow have a bz_schema table
|
||||
# already. This only happens if they have done a mysqldump into
|
||||
# a database without doing a DROP DATABASE first.
|
||||
# We just do the check here since this check is a reliable way
|
||||
# of telling that we are upgrading from a version pre-2.20.
|
||||
if (grep($_ eq 'bz_schema', $self->bz_table_list_real())) {
|
||||
die("\nYou are upgrading from a version before 2.20, but the"
|
||||
. " bz_schema\ntable already exists. This means that you"
|
||||
. " restored a mysqldump into\nthe Bugzilla database without"
|
||||
. " first dropping the already-existing\nBugzilla database,"
|
||||
. " at some point. Whenever you restore a Bugzilla\ndatabase"
|
||||
. " backup, you must always drop the entire database first.\n\n"
|
||||
. "Please drop your Bugzilla database and restore it from a"
|
||||
. " backup that\ndoes not contain the bz_schema table. If for"
|
||||
. " some reason you cannot\ndo this, you can connect to your"
|
||||
. " MySQL database and drop the bz_schema\ntable, as a last"
|
||||
. " resort.\n");
|
||||
}
|
||||
|
||||
my $bug_count = $self->selectrow_array("SELECT COUNT(*) FROM bugs");
|
||||
# We estimate one minute for each 3000 bugs, plus 3 minutes just
|
||||
# to handle basic MySQL stuff.
|
||||
my $rename_time = int($bug_count / 3000) + 3;
|
||||
# And 45 minutes for every 15,000 attachments, per some experiments.
|
||||
my ($attachment_count) =
|
||||
$self->selectrow_array("SELECT COUNT(*) FROM attachments");
|
||||
$rename_time += int(($attachment_count * 45) / 15000);
|
||||
# If we're going to take longer than 5 minutes, we let the user know
|
||||
# and allow them to abort.
|
||||
if ($rename_time > 5) {
|
||||
print "\nWe are about to rename old indexes.\n"
|
||||
. "The estimated time to complete renaming is "
|
||||
. "$rename_time minutes.\n"
|
||||
. "You cannot interrupt this action once it has begun.\n"
|
||||
. "If you would like to cancel, press Ctrl-C now..."
|
||||
. " (Waiting 45 seconds...)\n\n";
|
||||
# Wait 45 seconds for them to respond.
|
||||
sleep(45);
|
||||
}
|
||||
print "Renaming indexes...\n";
|
||||
|
||||
# We can't be interrupted, because of how the "if"
|
||||
# works above.
|
||||
local $SIG{INT} = 'IGNORE';
|
||||
local $SIG{TERM} = 'IGNORE';
|
||||
local $SIG{PIPE} = 'IGNORE';
|
||||
|
||||
# Certain indexes had names in Schema that did not easily conform
|
||||
# to a standard. We store those names here, so that they
|
||||
# can be properly renamed.
|
||||
# Also, sometimes an old mysqldump would incorrectly rename
|
||||
# unique indexes to "PRIMARY", so we address that here, also.
|
||||
my $bad_names = {
|
||||
# 'when' is a possible leftover from Bugzillas before 2.8
|
||||
bugs_activity => ['when', 'bugs_activity_bugid_idx',
|
||||
'bugs_activity_bugwhen_idx'],
|
||||
cc => ['PRIMARY'],
|
||||
longdescs => ['longdescs_bugid_idx',
|
||||
'longdescs_bugwhen_idx'],
|
||||
flags => ['flags_bidattid_idx'],
|
||||
flaginclusions => ['flaginclusions_tpcid_idx'],
|
||||
flagexclusions => ['flagexclusions_tpc_id_idx'],
|
||||
keywords => ['PRIMARY'],
|
||||
milestones => ['PRIMARY'],
|
||||
profiles_activity => ['profiles_activity_when_idx'],
|
||||
group_control_map => ['group_control_map_gid_idx', 'PRIMARY'],
|
||||
user_group_map => ['PRIMARY'],
|
||||
group_group_map => ['PRIMARY'],
|
||||
email_setting => ['PRIMARY'],
|
||||
bug_group_map => ['PRIMARY'],
|
||||
category_group_map => ['PRIMARY'],
|
||||
watch => ['PRIMARY'],
|
||||
namedqueries => ['PRIMARY'],
|
||||
series_data => ['PRIMARY'],
|
||||
# series_categories is dealt with below, not here.
|
||||
};
|
||||
|
||||
# The series table is broken and needs to have one index
|
||||
# dropped before we begin the renaming, because it had a
|
||||
# useless index on it that would cause a naming conflict here.
|
||||
if (grep($_ eq 'series', @tables)) {
|
||||
my $dropname;
|
||||
# This is what the bad index was called before Schema.
|
||||
if ($self->bz_index_info_real('series', 'creator_2')) {
|
||||
$dropname = 'creator_2';
|
||||
}
|
||||
# This is what the bad index is called in Schema.
|
||||
elsif ($self->bz_index_info_real('series', 'series_creator_idx')) {
|
||||
$dropname = 'series_creator_idx';
|
||||
}
|
||||
$self->bz_drop_index_raw('series', $dropname) if $dropname;
|
||||
}
|
||||
|
||||
# The email_setting table also had the same problem.
|
||||
if( grep($_ eq 'email_setting', @tables)
|
||||
&& $self->bz_index_info_real('email_setting',
|
||||
'email_settings_user_id_idx') )
|
||||
{
|
||||
$self->bz_drop_index_raw('email_setting',
|
||||
'email_settings_user_id_idx');
|
||||
}
|
||||
|
||||
# Go through all the tables.
|
||||
foreach my $table (@tables) {
|
||||
# Will contain the names of old indexes as keys, and the
|
||||
# definition of the new indexes as a value. The values
|
||||
# include an extra hash key, NAME, with the new name of
|
||||
# the index.
|
||||
my %rename_indexes;
|
||||
# And go through all the columns on each table.
|
||||
my @columns = $self->bz_table_columns_real($table);
|
||||
|
||||
# We also want to fix the silly naming of unique indexes
|
||||
# that happened when we first checked-in Bugzilla::DB::Schema.
|
||||
if ($table eq 'series_categories') {
|
||||
# The series_categories index had a nonstandard name.
|
||||
push(@columns, 'series_cats_unique_idx');
|
||||
}
|
||||
elsif ($table eq 'email_setting') {
|
||||
# The email_setting table had a similar problem.
|
||||
push(@columns, 'email_settings_unique_idx');
|
||||
}
|
||||
else {
|
||||
push(@columns, "${table}_unique_idx");
|
||||
}
|
||||
# And this is how we fix the other inconsistent Schema naming.
|
||||
push(@columns, @{$bad_names->{$table}})
|
||||
if (exists $bad_names->{$table});
|
||||
foreach my $column (@columns) {
|
||||
# If we have an index named after this column, it's an
|
||||
# old-style-name index.
|
||||
if (my $index = $self->bz_index_info_real($table, $column)) {
|
||||
# Fix the name to fit in with the new naming scheme.
|
||||
$index->{NAME} = $table . "_" .
|
||||
$index->{FIELDS}->[0] . "_idx";
|
||||
print "Renaming index $column to "
|
||||
. $index->{NAME} . "...\n";
|
||||
$rename_indexes{$column} = $index;
|
||||
} # if
|
||||
} # foreach column
|
||||
|
||||
my @rename_sql = $self->_bz_schema->get_rename_indexes_ddl(
|
||||
$table, %rename_indexes);
|
||||
$self->do($_) foreach (@rename_sql);
|
||||
|
||||
} # foreach table
|
||||
} # if old-name indexes
|
||||
|
||||
|
||||
# And now we create the tables and the Schema object.
|
||||
$self->SUPER::bz_setup_database();
|
||||
|
||||
|
||||
# The old timestamp fields need to be adjusted here instead of in
|
||||
# checksetup. Otherwise the UPDATE statements inside of bz_add_column
|
||||
# will cause accidental timestamp updates.
|
||||
# The code that does this was moved here from checksetup.
|
||||
|
||||
# 2002-08-14 - bbaetz@student.usyd.edu.au - bug 153578
|
||||
# attachments creation time needs to be a datetime, not a timestamp
|
||||
my $attach_creation =
|
||||
$self->bz_column_info("attachments", "creation_ts");
|
||||
if ($attach_creation && $attach_creation->{TYPE} =~ /^TIMESTAMP/i) {
|
||||
print "Fixing creation time on attachments...\n";
|
||||
|
||||
my $sth = $self->prepare("SELECT COUNT(attach_id) FROM attachments");
|
||||
$sth->execute();
|
||||
my ($attach_count) = $sth->fetchrow_array();
|
||||
|
||||
if ($attach_count > 1000) {
|
||||
print "This may take a while...\n";
|
||||
}
|
||||
my $i = 0;
|
||||
|
||||
# This isn't just as simple as changing the field type, because
|
||||
# the creation_ts was previously updated when an attachment was made
|
||||
# obsolete from the attachment creation screen. So we have to go
|
||||
# and recreate these times from the comments..
|
||||
$sth = $self->prepare("SELECT bug_id, attach_id, submitter_id " .
|
||||
"FROM attachments");
|
||||
$sth->execute();
|
||||
|
||||
# Restrict this as much as possible in order to avoid false
|
||||
# positives, and keep the db search time down
|
||||
my $sth2 = $self->prepare("SELECT bug_when FROM longdescs
|
||||
WHERE bug_id=? AND who=?
|
||||
AND thetext LIKE ?
|
||||
ORDER BY bug_when " . $self->sql_limit(1));
|
||||
while (my ($bug_id, $attach_id, $submitter_id)
|
||||
= $sth->fetchrow_array())
|
||||
{
|
||||
$sth2->execute($bug_id, $submitter_id,
|
||||
"Created an attachment (id=$attach_id)%");
|
||||
my ($when) = $sth2->fetchrow_array();
|
||||
if ($when) {
|
||||
$self->do("UPDATE attachments " .
|
||||
"SET creation_ts='$when' " .
|
||||
"WHERE attach_id=$attach_id");
|
||||
} else {
|
||||
print "Warning - could not determine correct creation"
|
||||
. " time for attachment $attach_id on bug $bug_id\n";
|
||||
}
|
||||
++$i;
|
||||
print "Converted $i of $attach_count attachments\n" if !($i % 1000);
|
||||
}
|
||||
print "Done - converted $i attachments\n";
|
||||
|
||||
$self->bz_alter_column("attachments", "creation_ts",
|
||||
{TYPE => 'DATETIME', NOTNULL => 1});
|
||||
}
|
||||
|
||||
# 2004-08-29 - Tomas.Kopal@altap.cz, bug 257303
|
||||
# Change logincookies.lastused type from timestamp to datetime
|
||||
my $login_lastused = $self->bz_column_info("logincookies", "lastused");
|
||||
if ($login_lastused && $login_lastused->{TYPE} =~ /^TIMESTAMP/i) {
|
||||
$self->bz_alter_column('logincookies', 'lastused',
|
||||
{ TYPE => 'DATETIME', NOTNULL => 1});
|
||||
}
|
||||
|
||||
# 2005-01-17 - Tomas.Kopal@altap.cz, bug 257315
|
||||
# Change bugs.delta_ts type from timestamp to datetime
|
||||
my $bugs_deltats = $self->bz_column_info("bugs", "delta_ts");
|
||||
if ($bugs_deltats && $bugs_deltats->{TYPE} =~ /^TIMESTAMP/i) {
|
||||
$self->bz_alter_column('bugs', 'delta_ts',
|
||||
{TYPE => 'DATETIME', NOTNULL => 1});
|
||||
}
|
||||
|
||||
# 2005-09-24 - bugreport@peshkin.net, bug 307602
|
||||
# Make sure that default 4G table limit is overridden
|
||||
my $row = $self->selectrow_hashref("SHOW TABLE STATUS LIKE 'attach_data'");
|
||||
if ($$row{'Create_options'} !~ /MAX_ROWS/i) {
|
||||
print "Converting attach_data maximum size to 100G...\n";
|
||||
$self->do("ALTER TABLE attach_data
|
||||
AVG_ROW_LENGTH=1000000,
|
||||
MAX_ROWS=100000");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
sub bz_enum_initial_values {
|
||||
my ($self, $enum_defaults) = @_;
|
||||
my %enum_values = %$enum_defaults;
|
||||
# Get a complete description of the 'bugs' table; with DBD::MySQL
|
||||
# there isn't a column-by-column way of doing this. Could use
|
||||
# $dbh->column_info, but it would go slower and we would have to
|
||||
# use the undocumented mysql_type_name accessor to get the type
|
||||
# of each row.
|
||||
my $sth = $self->prepare("DESCRIBE bugs");
|
||||
$sth->execute();
|
||||
# Look for the particular columns we are interested in.
|
||||
while (my ($thiscol, $thistype) = $sth->fetchrow_array()) {
|
||||
if (defined $enum_values{$thiscol}) {
|
||||
# this is a column of interest.
|
||||
my @value_list;
|
||||
if ($thistype and ($thistype =~ /^enum\(/)) {
|
||||
# it has an enum type; get the set of values.
|
||||
while ($thistype =~ /'([^']*)'(.*)/) {
|
||||
push(@value_list, $1);
|
||||
$thistype = $2;
|
||||
}
|
||||
}
|
||||
if (@value_list) {
|
||||
# record the enum values found.
|
||||
$enum_values{$thiscol} = \@value_list;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return \%enum_values;
|
||||
}
|
||||
|
||||
#####################################################################
|
||||
# MySQL-specific Database-Reading Methods
|
||||
#####################################################################
|
||||
|
||||
=begin private
|
||||
|
||||
=head1 MYSQL-SPECIFIC DATABASE-READING METHODS
|
||||
|
||||
These methods read information about the database from the disk,
|
||||
instead of from a Schema object. They are only reliable for MySQL
|
||||
(see bug 285111 for the reasons why not all DBs use/have functions
|
||||
like this), but that's OK because we only need them for
|
||||
backwards-compatibility anyway, for versions of Bugzilla before 2.20.
|
||||
|
||||
=over 4
|
||||
|
||||
=item C<bz_column_info_real($table, $column)>
|
||||
|
||||
Description: Returns an abstract column definition for a column
|
||||
as it actually exists on disk in the database.
|
||||
Params: $table - The name of the table the column is on.
|
||||
$column - The name of the column you want info about.
|
||||
Returns: An abstract column definition.
|
||||
If the column does not exist, returns undef.
|
||||
|
||||
=cut
|
||||
|
||||
sub bz_column_info_real {
|
||||
my ($self, $table, $column) = @_;
|
||||
|
||||
# DBD::mysql does not support selecting a specific column,
|
||||
# so we have to get all the columns on the table and find
|
||||
# the one we want.
|
||||
my $info_sth = $self->column_info(undef, undef, $table, '%');
|
||||
|
||||
# Don't use fetchall_hashref as there's a Win32 DBI bug (292821)
|
||||
my $col_data;
|
||||
while ($col_data = $info_sth->fetchrow_hashref) {
|
||||
last if $col_data->{'COLUMN_NAME'} eq $column;
|
||||
}
|
||||
|
||||
if (!defined $col_data) {
|
||||
return undef;
|
||||
}
|
||||
return $self->_bz_schema->column_info_to_column($col_data);
|
||||
}
|
||||
|
||||
=item C<bz_index_info_real($table, $index)>
|
||||
|
||||
Description: Returns information about an index on a table in the database.
|
||||
Params: $table = name of table containing the index
|
||||
$index = name of an index
|
||||
Returns: An abstract index definition, always in hashref format.
|
||||
If the index does not exist, the function returns undef.
|
||||
=cut
|
||||
sub bz_index_info_real {
|
||||
my ($self, $table, $index) = @_;
|
||||
|
||||
my $sth = $self->prepare("SHOW INDEX FROM $table");
|
||||
$sth->execute;
|
||||
|
||||
my @fields;
|
||||
my $index_type;
|
||||
# $raw_def will be an arrayref containing the following information:
|
||||
# 0 = name of the table that the index is on
|
||||
# 1 = 0 if unique, 1 if not unique
|
||||
# 2 = name of the index
|
||||
# 3 = seq_in_index (The order of the current field in the index).
|
||||
# 4 = Name of ONE column that the index is on
|
||||
# 5 = 'Collation' of the index. Usually 'A'.
|
||||
# 6 = Cardinality. Either a number or undef.
|
||||
# 7 = sub_part. Usually undef. Sometimes 1.
|
||||
# 8 = "packed". Usually undef.
|
||||
# MySQL 3
|
||||
# -------
|
||||
# 9 = comments. Usually an empty string. Sometimes 'FULLTEXT'.
|
||||
# MySQL 4
|
||||
# -------
|
||||
# 9 = Null. Sometimes undef, sometimes 'YES'.
|
||||
# 10 = Index_type. The type of the index. Usually either 'BTREE' or 'FULLTEXT'
|
||||
# 11 = 'Comment.' Usually undef.
|
||||
my $is_mysql3 = ($self->bz_server_version() =~ /^3/);
|
||||
my $index_type_loc = $is_mysql3 ? 9 : 10;
|
||||
while (my $raw_def = $sth->fetchrow_arrayref) {
|
||||
if ($raw_def->[2] eq $index) {
|
||||
push(@fields, $raw_def->[4]);
|
||||
# No index can be both UNIQUE and FULLTEXT, that's why
|
||||
# this is written this way.
|
||||
$index_type = $raw_def->[1] ? '' : 'UNIQUE';
|
||||
$index_type = $raw_def->[$index_type_loc] eq 'FULLTEXT'
|
||||
? 'FULLTEXT' : $index_type;
|
||||
}
|
||||
}
|
||||
|
||||
my $retval;
|
||||
if (scalar(@fields)) {
|
||||
$retval = {FIELDS => \@fields, TYPE => $index_type};
|
||||
}
|
||||
return $retval;
|
||||
}
|
||||
|
||||
=item C<bz_index_list_real($table)>
|
||||
|
||||
Description: Returns a list of index names on a table in
|
||||
the database, as it actually exists on disk.
|
||||
Params: $table - The name of the table you want info about.
|
||||
Returns: An array of index names.
|
||||
|
||||
=cut
|
||||
|
||||
sub bz_index_list_real {
|
||||
my ($self, $table) = @_;
|
||||
my $sth = $self->prepare("SHOW INDEX FROM $table");
|
||||
# Column 3 of a SHOW INDEX statement contains the name of the index.
|
||||
return @{ $self->selectcol_arrayref($sth, {Columns => [3]}) };
|
||||
}
|
||||
|
||||
#####################################################################
|
||||
# MySQL-Specific "Schema Builder"
|
||||
#####################################################################
|
||||
|
||||
=back
|
||||
|
||||
=head1 MYSQL-SPECIFIC "SCHEMA BUILDER"
|
||||
|
||||
MySQL needs to be able to read in a legacy database (from before
|
||||
Schema existed) and create a Schema object out of it. That's what
|
||||
this code does.
|
||||
|
||||
=end private
|
||||
|
||||
=cut
|
||||
|
||||
# This sub itself is actually written generically, but the subroutines
|
||||
# that it depends on are database-specific. In particular, the
|
||||
# bz_column_info_real function would be very difficult to create
|
||||
# properly for any other DB besides MySQL.
|
||||
sub _bz_build_schema_from_disk {
|
||||
my ($self) = @_;
|
||||
|
||||
print "Building Schema object from database...\n";
|
||||
|
||||
my $schema = $self->_bz_schema->get_empty_schema();
|
||||
|
||||
my @tables = $self->bz_table_list_real();
|
||||
foreach my $table (@tables) {
|
||||
$schema->add_table($table);
|
||||
my @columns = $self->bz_table_columns_real($table);
|
||||
foreach my $column (@columns) {
|
||||
my $type_info = $self->bz_column_info_real($table, $column);
|
||||
$schema->set_column($table, $column, $type_info);
|
||||
}
|
||||
|
||||
my @indexes = $self->bz_index_list_real($table);
|
||||
foreach my $index (@indexes) {
|
||||
unless ($index eq 'PRIMARY') {
|
||||
my $index_info = $self->bz_index_info_real($table, $index);
|
||||
($index_info = $index_info->{FIELDS})
|
||||
if (!$index_info->{TYPE});
|
||||
$schema->set_index($table, $index, $index_info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $schema;
|
||||
}
|
||||
1;
|
||||
@@ -1,252 +0,0 @@
|
||||
# -*- 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): Dave Miller <davem00@aol.com>
|
||||
# Gayathri Swaminath <gayathrik00@aol.com>
|
||||
# Jeroen Ruigrok van der Werven <asmodai@wxs.nl>
|
||||
# Dave Lawrence <dkl@redhat.com>
|
||||
# Tomas Kopal <Tomas.Kopal@altap.cz>
|
||||
# Max Kanat-Alexander <mkanat@bugzilla.org>
|
||||
# Lance Larsh <lance.larsh@oracle.com>
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::DB::Pg - Bugzilla database compatibility layer for PostgreSQL
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
This module overrides methods of the Bugzilla::DB module with PostgreSQL
|
||||
specific implementation. It is instantiated by the Bugzilla::DB module
|
||||
and should never be used directly.
|
||||
|
||||
For interface details see L<Bugzilla::DB> and L<DBI>.
|
||||
|
||||
=cut
|
||||
|
||||
package Bugzilla::DB::Pg;
|
||||
|
||||
use strict;
|
||||
|
||||
use Bugzilla::Error;
|
||||
use DBD::Pg;
|
||||
|
||||
# This module extends the DB interface via inheritance
|
||||
use base qw(Bugzilla::DB);
|
||||
|
||||
use constant BLOB_TYPE => { pg_type => DBD::Pg::PG_BYTEA };
|
||||
use constant REQUIRED_VERSION => '7.03.0000';
|
||||
use constant PROGRAM_NAME => 'PostgreSQL';
|
||||
use constant MODULE_NAME => 'Pg';
|
||||
use constant DBD_VERSION => '1.31';
|
||||
|
||||
sub new {
|
||||
my ($class, $user, $pass, $host, $dbname, $port) = @_;
|
||||
|
||||
# The default database name for PostgreSQL. We have
|
||||
# to connect to SOME database, even if we have
|
||||
# no $dbname parameter.
|
||||
$dbname ||= 'template1';
|
||||
|
||||
# construct the DSN from the parameters we got
|
||||
my $dsn = "DBI:Pg:host=$host;dbname=$dbname";
|
||||
$dsn .= ";port=$port" if $port;
|
||||
|
||||
# This stops Pg from printing out lots of "NOTICE" messages when
|
||||
# creating tables.
|
||||
$dsn .= ";options='-c client_min_messages=warning'";
|
||||
|
||||
my $self = $class->db_new($dsn, $user, $pass);
|
||||
|
||||
# all class local variables stored in DBI derived class needs to have
|
||||
# a prefix 'private_'. See DBI documentation.
|
||||
$self->{private_bz_tables_locked} = "";
|
||||
|
||||
bless ($self, $class);
|
||||
|
||||
return $self;
|
||||
}
|
||||
|
||||
# if last_insert_id is supported on PostgreSQL by lowest DBI/DBD version
|
||||
# supported by Bugzilla, this implementation can be removed.
|
||||
sub bz_last_key {
|
||||
my ($self, $table, $column) = @_;
|
||||
|
||||
my $seq = $table . "_" . $column . "_seq";
|
||||
my ($last_insert_id) = $self->selectrow_array("SELECT CURRVAL('$seq')");
|
||||
|
||||
return $last_insert_id;
|
||||
}
|
||||
|
||||
sub sql_regexp {
|
||||
my ($self, $expr, $pattern) = @_;
|
||||
|
||||
return "$expr ~* $pattern";
|
||||
}
|
||||
|
||||
sub sql_not_regexp {
|
||||
my ($self, $expr, $pattern) = @_;
|
||||
|
||||
return "$expr !~* $pattern"
|
||||
}
|
||||
|
||||
sub sql_limit {
|
||||
my ($self, $limit, $offset) = @_;
|
||||
|
||||
if (defined($offset)) {
|
||||
return "LIMIT $limit OFFSET $offset";
|
||||
} else {
|
||||
return "LIMIT $limit";
|
||||
}
|
||||
}
|
||||
|
||||
sub sql_from_days {
|
||||
my ($self, $days) = @_;
|
||||
|
||||
return "TO_TIMESTAMP(${days}::int, 'J')::date";
|
||||
}
|
||||
|
||||
sub sql_to_days {
|
||||
my ($self, $date) = @_;
|
||||
|
||||
return "TO_CHAR(${date}::date, 'J')::int";
|
||||
}
|
||||
|
||||
sub sql_date_format {
|
||||
my ($self, $date, $format) = @_;
|
||||
|
||||
$format = "%Y.%m.%d %H:%i:%s" if !$format;
|
||||
|
||||
$format =~ s/\%Y/YYYY/g;
|
||||
$format =~ s/\%y/YY/g;
|
||||
$format =~ s/\%m/MM/g;
|
||||
$format =~ s/\%d/DD/g;
|
||||
$format =~ s/\%a/Dy/g;
|
||||
$format =~ s/\%H/HH24/g;
|
||||
$format =~ s/\%i/MI/g;
|
||||
$format =~ s/\%s/SS/g;
|
||||
|
||||
return "TO_CHAR($date, " . $self->quote($format) . ")";
|
||||
}
|
||||
|
||||
sub sql_interval {
|
||||
my ($self, $interval, $units) = @_;
|
||||
|
||||
return "$interval * INTERVAL '1 $units'";
|
||||
}
|
||||
|
||||
sub sql_string_concat {
|
||||
my ($self, @params) = @_;
|
||||
|
||||
# Postgres 7.3 does not support concatenating of different types, so we
|
||||
# need to cast both parameters to text. Version 7.4 seems to handle this
|
||||
# properly, so when we stop support 7.3, this can be removed.
|
||||
return '(CAST(' . join(' AS text) || CAST(', @params) . ' AS text))';
|
||||
}
|
||||
|
||||
sub bz_lock_tables {
|
||||
my ($self, @tables) = @_;
|
||||
|
||||
my $list = join(', ', @tables);
|
||||
# Check first if there was no lock before
|
||||
if ($self->{private_bz_tables_locked}) {
|
||||
ThrowCodeError("already_locked", { current => $self->{private_bz_tables_locked},
|
||||
new => $list });
|
||||
} else {
|
||||
my %read_tables;
|
||||
my %write_tables;
|
||||
foreach my $table (@tables) {
|
||||
$table =~ /^([\d\w]+)([\s]+AS[\s]+[\d\w]+)?[\s]+(WRITE|READ)$/i;
|
||||
my $table_name = $1;
|
||||
if ($3 =~ /READ/i) {
|
||||
if (!exists $read_tables{$table_name}) {
|
||||
$read_tables{$table_name} = undef;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!exists $write_tables{$table_name}) {
|
||||
$write_tables{$table_name} = undef;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Begin Transaction
|
||||
$self->bz_start_transaction();
|
||||
|
||||
Bugzilla->dbh->do('LOCK TABLE ' . join(', ', keys %read_tables) .
|
||||
' IN ROW SHARE MODE') if keys %read_tables;
|
||||
Bugzilla->dbh->do('LOCK TABLE ' . join(', ', keys %write_tables) .
|
||||
' IN ROW EXCLUSIVE MODE') if keys %write_tables;
|
||||
$self->{private_bz_tables_locked} = $list;
|
||||
}
|
||||
}
|
||||
|
||||
sub bz_unlock_tables {
|
||||
my ($self, $abort) = @_;
|
||||
|
||||
# Check first if there was previous matching lock
|
||||
if (!$self->{private_bz_tables_locked}) {
|
||||
# Abort is allowed even without previous lock for error handling
|
||||
return if $abort;
|
||||
ThrowCodeError("no_matching_lock");
|
||||
} else {
|
||||
$self->{private_bz_tables_locked} = "";
|
||||
# End transaction, tables will be unlocked automatically
|
||||
if ($abort) {
|
||||
$self->bz_rollback_transaction();
|
||||
} else {
|
||||
$self->bz_commit_transaction();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#####################################################################
|
||||
# Custom Database Setup
|
||||
#####################################################################
|
||||
|
||||
sub bz_setup_database {
|
||||
my $self = shift;
|
||||
$self->SUPER::bz_setup_database(@_);
|
||||
|
||||
# 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.
|
||||
$self->bz_drop_index('longdescs', 'longdescs_thetext_idx');
|
||||
|
||||
# PostgreSQL also wants an index for calling LOWER on
|
||||
# login_name, which we do with sql_istrcmp all over the place.
|
||||
$self->bz_add_index('profiles', 'profiles_login_name_lower_idx',
|
||||
{FIELDS => ['LOWER(login_name)'], TYPE => 'UNIQUE'});
|
||||
}
|
||||
|
||||
#####################################################################
|
||||
# Custom Schema Information Functions
|
||||
#####################################################################
|
||||
|
||||
# Pg includes the PostgreSQL system tables in table_list_real, so
|
||||
# we need to remove those.
|
||||
sub bz_table_list_real {
|
||||
my $self = shift;
|
||||
|
||||
my @full_table_list = $self->SUPER::bz_table_list_real(@_);
|
||||
# All PostgreSQL system tables start with "pg_" or "sql_"
|
||||
my @table_list = grep(!/(^pg_)|(^sql_)/, @full_table_list);
|
||||
return @table_list;
|
||||
}
|
||||
|
||||
1;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,335 +0,0 @@
|
||||
# -*- 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): Andrew Dunstan <andrew@dunslane.net>,
|
||||
# Edward J. Sabol <edwardjsabol@iname.com>
|
||||
# Max Kanat-Alexander <mkanat@bugzilla.org>
|
||||
|
||||
package Bugzilla::DB::Schema::Mysql;
|
||||
|
||||
###############################################################################
|
||||
#
|
||||
# DB::Schema implementation for MySQL
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
use strict;
|
||||
use Bugzilla::Error;
|
||||
|
||||
use base qw(Bugzilla::DB::Schema);
|
||||
|
||||
# This is for column_info_to_column, to know when a tinyint is a
|
||||
# boolean and when it's really a tinyint. This only has to be accurate
|
||||
# up to and through 2.19.3, because that's the only time we need
|
||||
# column_info_to_column.
|
||||
#
|
||||
# This is basically a hash of tables/columns, with one entry for each column
|
||||
# that should be interpreted as a BOOLEAN instead of as an INT1 when
|
||||
# reading in the Schema from the disk. The values are discarded; I just
|
||||
# used "1" for simplicity.
|
||||
use constant BOOLEAN_MAP => {
|
||||
bugs => {everconfirmed => 1, reporter_accessible => 1,
|
||||
cclist_accessible => 1, qacontact_accessible => 1,
|
||||
assignee_accessible => 1},
|
||||
longdescs => {isprivate => 1, already_wrapped => 1},
|
||||
attachments => {ispatch => 1, isobsolete => 1, isprivate => 1},
|
||||
flags => {is_active => 1},
|
||||
flagtypes => {is_active => 1, is_requestable => 1,
|
||||
is_requesteeble => 1, is_multiplicable => 1},
|
||||
fielddefs => {mailhead => 1, obsolete => 1},
|
||||
bug_status => {isactive => 1},
|
||||
resolution => {isactive => 1},
|
||||
bug_severity => {isactive => 1},
|
||||
priority => {isactive => 1},
|
||||
rep_platform => {isactive => 1},
|
||||
op_sys => {isactive => 1},
|
||||
profiles => {mybugslink => 1, newemailtech => 1},
|
||||
namedqueries => {linkinfooter => 1, watchfordiffs => 1},
|
||||
groups => {isbuggroup => 1, isactive => 1},
|
||||
group_control_map => {entry => 1, membercontrol => 1, othercontrol => 1,
|
||||
canedit => 1},
|
||||
group_group_map => {isbless => 1},
|
||||
user_group_map => {isbless => 1, isderived => 1},
|
||||
products => {disallownew => 1},
|
||||
series => {public => 1},
|
||||
whine_queries => {onemailperbug => 1},
|
||||
quips => {approved => 1},
|
||||
setting => {is_enabled => 1}
|
||||
};
|
||||
|
||||
# Maps the db_specific hash backwards, for use in column_info_to_column.
|
||||
use constant REVERSE_MAPPING => {
|
||||
# Boolean and the SERIAL fields are handled in column_info_to_column,
|
||||
# and so don't have an entry here.
|
||||
TINYINT => 'INT1',
|
||||
SMALLINT => 'INT2',
|
||||
MEDIUMINT => 'INT3',
|
||||
INTEGER => 'INT4',
|
||||
# All the other types have the same name in their abstract version
|
||||
# as in their db-specific version, so no reverse mapping is needed.
|
||||
};
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
sub _initialize {
|
||||
|
||||
my $self = shift;
|
||||
|
||||
$self = $self->SUPER::_initialize(@_);
|
||||
|
||||
$self->{db_specific} = {
|
||||
|
||||
BOOLEAN => 'tinyint',
|
||||
FALSE => '0',
|
||||
TRUE => '1',
|
||||
|
||||
INT1 => 'tinyint',
|
||||
INT2 => 'smallint',
|
||||
INT3 => 'mediumint',
|
||||
INT4 => 'integer',
|
||||
|
||||
SMALLSERIAL => 'smallint auto_increment',
|
||||
MEDIUMSERIAL => 'mediumint auto_increment',
|
||||
INTSERIAL => 'integer auto_increment',
|
||||
|
||||
TINYTEXT => 'tinytext',
|
||||
MEDIUMTEXT => 'mediumtext',
|
||||
TEXT => 'text',
|
||||
|
||||
LONGBLOB => 'longblob',
|
||||
|
||||
DATETIME => 'datetime',
|
||||
|
||||
};
|
||||
|
||||
$self->_adjust_schema;
|
||||
|
||||
return $self;
|
||||
|
||||
} #eosub--_initialize
|
||||
#------------------------------------------------------------------------------
|
||||
sub _get_create_table_ddl {
|
||||
# Extend superclass method to specify the MYISAM storage engine.
|
||||
# Returns a "create table" SQL statement.
|
||||
|
||||
my($self, $table) = @_;
|
||||
|
||||
return($self->SUPER::_get_create_table_ddl($table) . ' TYPE = MYISAM');
|
||||
|
||||
} #eosub--_get_create_table_ddl
|
||||
#------------------------------------------------------------------------------
|
||||
sub _get_create_index_ddl {
|
||||
# Extend superclass method to create FULLTEXT indexes on text fields.
|
||||
# Returns a "create index" SQL statement.
|
||||
|
||||
my($self, $table_name, $index_name, $index_fields, $index_type) = @_;
|
||||
|
||||
my $sql = "CREATE ";
|
||||
$sql .= "$index_type " if ($index_type eq 'UNIQUE'
|
||||
|| $index_type eq 'FULLTEXT');
|
||||
$sql .= "INDEX \`$index_name\` ON $table_name \(" .
|
||||
join(", ", @$index_fields) . "\)";
|
||||
|
||||
return($sql);
|
||||
|
||||
} #eosub--_get_create_index_ddl
|
||||
#--------------------------------------------------------------------
|
||||
|
||||
# MySQL has a simpler ALTER TABLE syntax than ANSI.
|
||||
sub get_alter_column_ddl {
|
||||
my ($self, $table, $column, $new_def, $set_nulls_to) = @_;
|
||||
my $old_def = $self->get_column($table, $column);
|
||||
my %new_def_copy = %$new_def;
|
||||
if ($old_def->{PRIMARYKEY} && $new_def->{PRIMARYKEY}) {
|
||||
# If a column stays a primary key do NOT specify PRIMARY KEY in the
|
||||
# ALTER TABLE statement. This avoids a MySQL error that two primary
|
||||
# keys are not allowed.
|
||||
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
|
||||
$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");
|
||||
}
|
||||
return @statements;
|
||||
}
|
||||
|
||||
sub get_drop_index_ddl {
|
||||
my ($self, $table, $name) = @_;
|
||||
return ("DROP INDEX \`$name\` ON $table");
|
||||
}
|
||||
|
||||
# A special function for MySQL, for renaming a lot of indexes.
|
||||
# Index renames is a hash, where the key is a string - the
|
||||
# old names of the index, and the value is a hash - the index
|
||||
# definition that we're renaming to, with an extra key of "NAME"
|
||||
# that contains the new index name.
|
||||
# The indexes in %indexes must be in hashref format.
|
||||
sub get_rename_indexes_ddl {
|
||||
my ($self, $table, %indexes) = @_;
|
||||
my @keys = keys %indexes or return ();
|
||||
|
||||
my $sql = "ALTER TABLE $table ";
|
||||
|
||||
foreach my $old_name (@keys) {
|
||||
my $name = $indexes{$old_name}->{NAME};
|
||||
my $type = $indexes{$old_name}->{TYPE};
|
||||
$type ||= 'INDEX';
|
||||
my $fields = join(',', @{$indexes{$old_name}->{FIELDS}});
|
||||
# $old_name needs to be escaped, sometimes, because it was
|
||||
# a reserved word.
|
||||
$old_name = '`' . $old_name . '`';
|
||||
$sql .= " ADD $type $name ($fields), DROP INDEX $old_name,";
|
||||
}
|
||||
# Remove the last comma.
|
||||
chop($sql);
|
||||
return ($sql);
|
||||
}
|
||||
|
||||
# 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
|
||||
# elsewhere.
|
||||
sub column_info_to_column {
|
||||
my ($self, $column_info) = @_;
|
||||
|
||||
# Unfortunately, we have to break Schema's normal "no database"
|
||||
# barrier a few times in this function.
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
my $table = $column_info->{TABLE_NAME};
|
||||
my $col_name = $column_info->{COLUMN_NAME};
|
||||
|
||||
my $column = {};
|
||||
|
||||
($column->{NOTNULL} = 1) if $column_info->{NULLABLE} == 0;
|
||||
|
||||
if ($column_info->{mysql_is_pri_key}) {
|
||||
# In MySQL, if a table has no PK, but it has a UNIQUE index,
|
||||
# that index will show up as the PK. So we have to eliminate
|
||||
# that possibility.
|
||||
# Unfortunately, the only way to definitely solve this is
|
||||
# to break Schema's standard of not touching the live database
|
||||
# and check if the index called PRIMARY is on that field.
|
||||
my $pri_index = $dbh->bz_index_info_real($table, 'PRIMARY');
|
||||
if ( $pri_index && grep($_ eq $col_name, @{$pri_index->{FIELDS}}) ) {
|
||||
$column->{PRIMARYKEY} = 1;
|
||||
}
|
||||
}
|
||||
|
||||
# MySQL frequently defines a default for a field even when we
|
||||
# didn't explicitly set one. So we have to have some special
|
||||
# hacks to determine whether or not we should actually put
|
||||
# a default in the abstract schema for this field.
|
||||
if (defined $column_info->{COLUMN_DEF}) {
|
||||
# The defaults that MySQL inputs automatically are usually
|
||||
# something that would be considered "false" by perl, either
|
||||
# a 0 or an empty string. (Except for datetime and decimal
|
||||
# fields, which have their own special auto-defaults.)
|
||||
#
|
||||
# Here's how we handle this: If it exists in the schema
|
||||
# without a default, then we don't use the default. If it
|
||||
# doesn't exist in the schema, then we're either going to
|
||||
# be dropping it soon, or it's a custom end-user column, in which
|
||||
# case having a bogus default won't harm anything.
|
||||
my $schema_column = $self->get_column($table, $col_name);
|
||||
unless ( (!$column_info->{COLUMN_DEF}
|
||||
|| $column_info->{COLUMN_DEF} eq '0000-00-00 00:00:00'
|
||||
|| $column_info->{COLUMN_DEF} eq '0.00')
|
||||
&& $schema_column
|
||||
&& !exists $schema_column->{DEFAULT}) {
|
||||
|
||||
my $default = $column_info->{COLUMN_DEF};
|
||||
# Schema uses '0' for the defaults for decimal fields.
|
||||
$default = 0 if $default =~ /^0\.0+$/;
|
||||
# If we're not a number, we're a string and need to be
|
||||
# quoted.
|
||||
$default = $dbh->quote($default) if !($default =~ /^(-)?(\d+)(.\d+)?$/);
|
||||
$column->{DEFAULT} = $default;
|
||||
}
|
||||
}
|
||||
|
||||
my $type = $column_info->{TYPE_NAME};
|
||||
|
||||
# Certain types of columns need the size/precision appended.
|
||||
if ($type =~ /CHAR$/ || $type eq 'DECIMAL') {
|
||||
# This is nicely lowercase and has the size/precision appended.
|
||||
$type = $column_info->{mysql_type_name};
|
||||
}
|
||||
|
||||
# If we're a tinyint, we could be either a BOOLEAN or an INT1.
|
||||
# Only the BOOLEAN_MAP knows the difference.
|
||||
elsif ($type eq 'TINYINT' && exists BOOLEAN_MAP->{$table}
|
||||
&& exists BOOLEAN_MAP->{$table}->{$col_name}) {
|
||||
$type = 'BOOLEAN';
|
||||
if (exists $column->{DEFAULT}) {
|
||||
$column->{DEFAULT} = $column->{DEFAULT} ? 'TRUE' : 'FALSE';
|
||||
}
|
||||
}
|
||||
|
||||
# We also need to check if we're an auto_increment field.
|
||||
elsif ($type =~ /INT/) {
|
||||
# Unfortunately, the only way to do this in DBI is to query the
|
||||
# database, so we have to break the rule here that Schema normally
|
||||
# doesn't touch the live DB.
|
||||
my $ref_sth = $dbh->prepare(
|
||||
"SELECT $col_name FROM $table LIMIT 1");
|
||||
$ref_sth->execute;
|
||||
if ($ref_sth->{mysql_is_auto_increment}->[0]) {
|
||||
if ($type eq 'MEDIUMINT') {
|
||||
$type = 'MEDIUMSERIAL';
|
||||
}
|
||||
elsif ($type eq 'SMALLINT') {
|
||||
$type = 'SMALLSERIAL';
|
||||
}
|
||||
else {
|
||||
$type = 'INTSERIAL';
|
||||
}
|
||||
}
|
||||
$ref_sth->finish;
|
||||
|
||||
}
|
||||
|
||||
# For all other db-specific types, check if they exist in
|
||||
# REVERSE_MAPPING and use the type found there.
|
||||
if (exists REVERSE_MAPPING->{$type}) {
|
||||
$type = REVERSE_MAPPING->{$type};
|
||||
}
|
||||
|
||||
$column->{TYPE} = $type;
|
||||
|
||||
#print "$table.$col_name: " . Data::Dumper->Dump([$column]) . "\n";
|
||||
|
||||
return $column;
|
||||
}
|
||||
|
||||
sub get_rename_column_ddl {
|
||||
my ($self, $table, $old_name, $new_name) = @_;
|
||||
my $def = $self->get_type_ddl($self->get_column($table, $old_name));
|
||||
# MySQL doesn't like having the PRIMARY KEY statement in a rename.
|
||||
$def =~ s/PRIMARY KEY//i;
|
||||
return ("ALTER TABLE $table CHANGE COLUMN $old_name $new_name $def");
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -1,143 +0,0 @@
|
||||
# -*- 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): Andrew Dunstan <andrew@dunslane.net>,
|
||||
# Edward J. Sabol <edwardjsabol@iname.com>
|
||||
# Max Kanat-Alexander <mkanat@bugzilla.org>
|
||||
|
||||
package Bugzilla::DB::Schema::Pg;
|
||||
|
||||
###############################################################################
|
||||
#
|
||||
# DB::Schema implementation for PostgreSQL
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
use strict;
|
||||
|
||||
use base qw(Bugzilla::DB::Schema);
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
sub _initialize {
|
||||
|
||||
my $self = shift;
|
||||
|
||||
$self = $self->SUPER::_initialize(@_);
|
||||
|
||||
# Remove FULLTEXT index types from the schemas.
|
||||
foreach my $table (keys %{ $self->{schema} }) {
|
||||
if ($self->{schema}{$table}{INDEXES}) {
|
||||
foreach my $index (@{ $self->{schema}{$table}{INDEXES} }) {
|
||||
if (ref($index) eq 'HASH') {
|
||||
delete($index->{TYPE}) if (exists $index->{TYPE}
|
||||
&& $index->{TYPE} eq 'FULLTEXT');
|
||||
}
|
||||
}
|
||||
foreach my $index (@{ $self->{abstract_schema}{$table}{INDEXES} }) {
|
||||
if (ref($index) eq 'HASH') {
|
||||
delete($index->{TYPE}) if (exists $index->{TYPE}
|
||||
&& $index->{TYPE} eq 'FULLTEXT');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$self->{db_specific} = {
|
||||
|
||||
BOOLEAN => 'smallint',
|
||||
FALSE => '0',
|
||||
TRUE => '1',
|
||||
|
||||
INT1 => 'integer',
|
||||
INT2 => 'integer',
|
||||
INT3 => 'integer',
|
||||
INT4 => 'integer',
|
||||
|
||||
SMALLSERIAL => 'serial unique',
|
||||
MEDIUMSERIAL => 'serial unique',
|
||||
INTSERIAL => 'serial unique',
|
||||
|
||||
TINYTEXT => 'varchar(255)',
|
||||
MEDIUMTEXT => 'text',
|
||||
TEXT => 'text',
|
||||
|
||||
LONGBLOB => 'bytea',
|
||||
|
||||
DATETIME => 'timestamp(0) without time zone',
|
||||
|
||||
};
|
||||
|
||||
$self->_adjust_schema;
|
||||
|
||||
return $self;
|
||||
|
||||
} #eosub--_initialize
|
||||
#--------------------------------------------------------------------
|
||||
|
||||
# Overridden because Pg has such weird ALTER TABLE problems.
|
||||
sub get_add_column_ddl {
|
||||
my ($self, $table, $column, $definition, $init_value) = @_;
|
||||
|
||||
my @statements;
|
||||
my $specific = $self->{db_specific};
|
||||
|
||||
my $type = $definition->{TYPE};
|
||||
$type = $specific->{$type} if exists $specific->{$type};
|
||||
push(@statements, "ALTER TABLE $table ADD COLUMN $column $type");
|
||||
|
||||
my $default = $definition->{DEFAULT};
|
||||
if (defined $default) {
|
||||
# Replace any abstract default value (such as 'TRUE' or 'FALSE')
|
||||
# with its database-specific implementation.
|
||||
$default = $specific->{$default} if exists $specific->{$default};
|
||||
push(@statements, "ALTER TABLE $table ALTER COLUMN $column "
|
||||
. " SET DEFAULT $default");
|
||||
}
|
||||
|
||||
if (defined $init_value) {
|
||||
push(@statements, "UPDATE $table SET $column = $init_value");
|
||||
}
|
||||
|
||||
if ($definition->{NOTNULL}) {
|
||||
# Handle rows that were NULL when we added the column.
|
||||
# We *must* have a DEFAULT. This check is usually handled
|
||||
# at a higher level than this code, but I figure it can't
|
||||
# hurt to have it here.
|
||||
die "NOT NULL columns must have a DEFAULT or an init_value."
|
||||
unless (exists $definition->{DEFAULT} || defined $init_value);
|
||||
push(@statements, "UPDATE $table SET $column = $default");
|
||||
push(@statements, "ALTER TABLE $table ALTER COLUMN $column "
|
||||
. " SET NOT NULL");
|
||||
}
|
||||
|
||||
if ($definition->{PRIMARYKEY}) {
|
||||
push(@statements, "ALTER TABLE $table ALTER COLUMN "
|
||||
. " ADD PRIMARY KEY ($column)");
|
||||
}
|
||||
|
||||
return @statements;
|
||||
}
|
||||
|
||||
sub get_rename_column_ddl {
|
||||
my ($self, $table, $old_name, $new_name) = @_;
|
||||
|
||||
return ("ALTER TABLE $table RENAME COLUMN $old_name TO $new_name");
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -1,203 +0,0 @@
|
||||
# -*- 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): Bradley Baetz <bbaetz@acm.org>
|
||||
|
||||
package Bugzilla::Error;
|
||||
|
||||
use strict;
|
||||
use base qw(Exporter);
|
||||
|
||||
@Bugzilla::Error::EXPORT = qw(ThrowCodeError ThrowTemplateError ThrowUserError);
|
||||
|
||||
use Bugzilla::Config qw($datadir);
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::Util;
|
||||
use Date::Format;
|
||||
|
||||
sub _throw_error {
|
||||
my ($name, $error, $vars) = @_;
|
||||
|
||||
$vars ||= {};
|
||||
|
||||
$vars->{error} = $error;
|
||||
|
||||
# Make sure any locked tables are unlocked
|
||||
# and the transaction is rolled back (if supported)
|
||||
Bugzilla->dbh->bz_unlock_tables(UNLOCK_ABORT);
|
||||
|
||||
# If a writable $datadir/errorlog exists, log error details there.
|
||||
if (-w "$datadir/errorlog") {
|
||||
require Data::Dumper;
|
||||
my $mesg = "";
|
||||
for (1..75) { $mesg .= "-"; };
|
||||
$mesg .= "\n[$$] " . time2str("%D %H:%M:%S ", time());
|
||||
$mesg .= "$name $error ";
|
||||
$mesg .= "$ENV{REMOTE_ADDR} " if $ENV{REMOTE_ADDR};
|
||||
$mesg .= Bugzilla->user->login;
|
||||
$mesg .= (' actually ' . Bugzilla->sudoer->login) if Bugzilla->sudoer;
|
||||
$mesg .= "\n";
|
||||
my %params = Bugzilla->cgi->Vars;
|
||||
$Data::Dumper::Useqq = 1;
|
||||
for my $param (sort keys %params) {
|
||||
my $val = $params{$param};
|
||||
# obscure passwords
|
||||
$val = "*****" if $param =~ /password/i;
|
||||
# limit line length
|
||||
$val =~ s/^(.{512}).*$/$1\[CHOP\]/;
|
||||
$mesg .= "[$$] " . Data::Dumper->Dump([$val],["param($param)"]);
|
||||
}
|
||||
for my $var (sort keys %ENV) {
|
||||
my $val = $ENV{$var};
|
||||
$val = "*****" if $val =~ /password|http_pass/i;
|
||||
$mesg .= "[$$] " . Data::Dumper->Dump([$val],["env($var)"]);
|
||||
}
|
||||
open(ERRORLOGFID, ">>$datadir/errorlog");
|
||||
print ERRORLOGFID "$mesg\n";
|
||||
close ERRORLOGFID;
|
||||
}
|
||||
|
||||
my $template = Bugzilla->template;
|
||||
if (Bugzilla->batch) {
|
||||
my $message;
|
||||
$template->process($name, $vars, \$message)
|
||||
|| ThrowTemplateError($template->error());
|
||||
die("$message");
|
||||
} else {
|
||||
print Bugzilla->cgi->header();
|
||||
$template->process($name, $vars)
|
||||
|| ThrowTemplateError($template->error());
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
sub ThrowUserError {
|
||||
_throw_error("global/user-error.html.tmpl", @_);
|
||||
}
|
||||
|
||||
sub ThrowCodeError {
|
||||
_throw_error("global/code-error.html.tmpl", @_);
|
||||
}
|
||||
|
||||
sub ThrowTemplateError {
|
||||
my ($template_err) = @_;
|
||||
|
||||
# Make sure any locked tables are unlocked
|
||||
# and the transaction is rolled back (if supported)
|
||||
Bugzilla->dbh->bz_unlock_tables(UNLOCK_ABORT);
|
||||
|
||||
my $vars = {};
|
||||
if (Bugzilla->batch) {
|
||||
die("error: template error: $template_err");
|
||||
}
|
||||
|
||||
$vars->{'template_error_msg'} = $template_err;
|
||||
$vars->{'error'} = "template_error";
|
||||
|
||||
my $template = Bugzilla->template;
|
||||
|
||||
# Try a template first; but if this one fails too, fall back
|
||||
# on plain old print statements.
|
||||
if (!$template->process("global/code-error.html.tmpl", $vars)) {
|
||||
my $maintainer = Bugzilla::Config::Param('maintainer');
|
||||
my $error = Bugzilla::Util::html_quote($vars->{'template_error_msg'});
|
||||
my $error2 = Bugzilla::Util::html_quote($template->error());
|
||||
print <<END;
|
||||
<tt>
|
||||
<p>
|
||||
Bugzilla has suffered an internal error. Please save this page and
|
||||
send it to $maintainer with details of what you were doing at the
|
||||
time this message appeared.
|
||||
</p>
|
||||
<script type="text/javascript"> <!--
|
||||
document.write("<p>URL: " +
|
||||
document.location.href.replace(/&/g,"&")
|
||||
.replace(/</g,"<")
|
||||
.replace(/>/g,">") + "</p>");
|
||||
// -->
|
||||
</script>
|
||||
<p>Template->process() failed twice.<br>
|
||||
First error: $error<br>
|
||||
Second error: $error2</p>
|
||||
</tt>
|
||||
END
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::Error - Error handling utilities for Bugzilla
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
use Bugzilla::Error;
|
||||
|
||||
ThrowUserError("error_tag",
|
||||
{ foo => 'bar' });
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
Various places throughout the Bugzilla codebase need to report errors to the
|
||||
user. The C<Throw*Error> family of functions allow this to be done in a
|
||||
generic and localizable manner.
|
||||
|
||||
These functions automatically unlock the database tables, if there were any
|
||||
locked. They will also roll back the transaction, if it is supported by
|
||||
the underlying DB.
|
||||
|
||||
=head1 FUNCTIONS
|
||||
|
||||
=over 4
|
||||
|
||||
=item C<ThrowUserError>
|
||||
|
||||
This function takes an error tag as the first argument, and an optional hashref
|
||||
of variables as a second argument. These are used by the
|
||||
I<global/user-error.html.tmpl> template to format the error, using the passed
|
||||
in variables as required.
|
||||
|
||||
=item C<ThrowCodeError>
|
||||
|
||||
This function is used when an internal check detects an error of some sort.
|
||||
This usually indicates a bug in Bugzilla, although it can occur if the user
|
||||
manually constructs urls without correct parameters.
|
||||
|
||||
This function's behaviour is similar to C<ThrowUserError>, except that the
|
||||
template used to display errors is I<global/code-error.html.tmpl>. In addition
|
||||
if the hashref used as the optional second argument contains a key I<variables>
|
||||
then the contents of the hashref (which is expected to be another hashref) will
|
||||
be displayed after the error message, as a debugging aid.
|
||||
|
||||
=item C<ThrowTemplateError>
|
||||
|
||||
This function should only be called if a C<template-<gt>process()> fails.
|
||||
It tries another template first, because often one template being
|
||||
broken or missing doesn't mean that they all are. But it falls back to
|
||||
a print statement as a last-ditch error.
|
||||
|
||||
=back
|
||||
|
||||
=head1 SEE ALSO
|
||||
|
||||
L<Bugzilla|Bugzilla>
|
||||
@@ -1,131 +0,0 @@
|
||||
# -*- 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>
|
||||
|
||||
package Bugzilla::Field;
|
||||
|
||||
use strict;
|
||||
|
||||
use base qw(Exporter);
|
||||
@Bugzilla::Field::EXPORT = qw(check_form_field check_form_field_defined
|
||||
get_field_id);
|
||||
|
||||
use Bugzilla::Util;
|
||||
use Bugzilla::Error;
|
||||
|
||||
|
||||
sub check_form_field {
|
||||
my ($cgi, $fieldname, $legalsRef) = @_;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
if (!defined $cgi->param($fieldname)
|
||||
|| trim($cgi->param($fieldname)) eq ""
|
||||
|| (defined($legalsRef)
|
||||
&& lsearch($legalsRef, $cgi->param($fieldname)) < 0))
|
||||
{
|
||||
trick_taint($fieldname);
|
||||
my ($result) = $dbh->selectrow_array("SELECT description FROM fielddefs
|
||||
WHERE name = ?", undef, $fieldname);
|
||||
|
||||
my $field = $result || $fieldname;
|
||||
ThrowCodeError("illegal_field", { field => $field });
|
||||
}
|
||||
}
|
||||
|
||||
sub check_form_field_defined {
|
||||
my ($cgi, $fieldname) = @_;
|
||||
|
||||
if (!defined $cgi->param($fieldname)) {
|
||||
ThrowCodeError("undefined_field", { field => $fieldname });
|
||||
}
|
||||
}
|
||||
|
||||
sub get_field_id {
|
||||
my ($name) = @_;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
trick_taint($name);
|
||||
my $id = $dbh->selectrow_array('SELECT fieldid FROM fielddefs
|
||||
WHERE name = ?', undef, $name);
|
||||
|
||||
ThrowCodeError('invalid_field_name', {field => $name}) unless $id;
|
||||
return $id
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::Field - Useful routines for fields manipulation
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
use Bugzilla::Field;
|
||||
|
||||
# Validation Routines
|
||||
check_form_field($cgi, $fieldname, \@legal_values);
|
||||
check_form_field_defined($cgi, $fieldname);
|
||||
$fieldid = get_field_id($fieldname);
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
This package provides functions for dealing with CGI form fields.
|
||||
|
||||
=head1 FUNCTIONS
|
||||
|
||||
This package provides several types of routines:
|
||||
|
||||
=head2 Validation
|
||||
|
||||
=over
|
||||
|
||||
=item C<check_form_field($cgi, $fieldname, \@legal_values)>
|
||||
|
||||
Description: Makes sure the field $fieldname is defined and its value
|
||||
is non empty. If @legal_values is defined, this routine
|
||||
also checks whether its value is one of the legal values
|
||||
associated with this field. If the test fails, an error
|
||||
is thrown.
|
||||
|
||||
Params: $cgi - a CGI object
|
||||
$fieldname - the field name to check
|
||||
@legal_values - (optional) ref to a list of legal values
|
||||
|
||||
Returns: nothing
|
||||
|
||||
=item C<check_form_field_defined($cgi, $fieldname)>
|
||||
|
||||
Description: Makes sure the field $fieldname is defined and its value
|
||||
is non empty. Else an error is thrown.
|
||||
|
||||
Params: $cgi - a CGI object
|
||||
$fieldname - the field name to check
|
||||
|
||||
Returns: nothing
|
||||
|
||||
=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: $fieldname - a field name
|
||||
|
||||
Returns: the corresponding field ID or an error if the field name
|
||||
does not exist.
|
||||
|
||||
=back
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,621 +0,0 @@
|
||||
# -*- 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): Myk Melez <myk@mozilla.org>
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::FlagType - A module to deal with Bugzilla flag types.
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
FlagType.pm provides an interface to flag types as stored in Bugzilla.
|
||||
See below for more information.
|
||||
|
||||
=head1 NOTES
|
||||
|
||||
=over
|
||||
|
||||
=item *
|
||||
|
||||
Prior to calling routines in this module, it's assumed that you have
|
||||
already done a C<require globals.pl>.
|
||||
|
||||
=item *
|
||||
|
||||
Use of private functions/variables outside this module may lead to
|
||||
unexpected results after an upgrade. Please avoid using private
|
||||
functions in other files/modules. Private functions are functions
|
||||
whose names start with _ or are specifically noted as being private.
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
######################################################################
|
||||
# Module Initialization
|
||||
######################################################################
|
||||
|
||||
# Make it harder for us to do dangerous things in Perl.
|
||||
use strict;
|
||||
|
||||
# This module implements flag types for the flag tracker.
|
||||
package Bugzilla::FlagType;
|
||||
|
||||
# Use Bugzilla's User module which contains utilities for handling users.
|
||||
use Bugzilla::User;
|
||||
|
||||
use Bugzilla::Error;
|
||||
use Bugzilla::Util;
|
||||
use Bugzilla::Config;
|
||||
|
||||
######################################################################
|
||||
# Global Variables
|
||||
######################################################################
|
||||
|
||||
=begin private
|
||||
|
||||
=head1 PRIVATE VARIABLES/CONSTANTS
|
||||
|
||||
=over
|
||||
|
||||
=item C<@base_columns>
|
||||
|
||||
basic sets of columns and tables for getting flag types from the
|
||||
database. B<Used by get, match, sqlify_criteria and perlify_record>
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
my @base_columns =
|
||||
("1", "flagtypes.id", "flagtypes.name", "flagtypes.description",
|
||||
"flagtypes.cc_list", "flagtypes.target_type", "flagtypes.sortkey",
|
||||
"flagtypes.is_active", "flagtypes.is_requestable",
|
||||
"flagtypes.is_requesteeble", "flagtypes.is_multiplicable",
|
||||
"flagtypes.grant_group_id", "flagtypes.request_group_id");
|
||||
|
||||
=pod
|
||||
|
||||
=over
|
||||
|
||||
=item C<@base_tables>
|
||||
|
||||
Which database(s) is the data coming from?
|
||||
|
||||
Note: when adding tables to @base_tables, make sure to include the separator
|
||||
(i.e. words like "LEFT OUTER JOIN") before the table name, since tables take
|
||||
multiple separators based on the join type, and therefore it is not possible
|
||||
to join them later using a single known separator.
|
||||
B<Used by get, match, sqlify_criteria and perlify_record>
|
||||
|
||||
=back
|
||||
|
||||
=end private
|
||||
|
||||
=cut
|
||||
|
||||
my @base_tables = ("flagtypes");
|
||||
|
||||
######################################################################
|
||||
# Public Functions
|
||||
######################################################################
|
||||
|
||||
=head1 PUBLIC FUNCTIONS/METHODS
|
||||
|
||||
=over
|
||||
|
||||
=item C<get($id)>
|
||||
|
||||
Returns a hash of information about a flag type.
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
sub get {
|
||||
my ($id) = @_;
|
||||
|
||||
my $select_clause = "SELECT " . join(", ", @base_columns);
|
||||
my $from_clause = "FROM " . join(" ", @base_tables);
|
||||
|
||||
&::PushGlobalSQLState();
|
||||
&::SendSQL("$select_clause $from_clause WHERE flagtypes.id = $id");
|
||||
my @data = &::FetchSQLData();
|
||||
my $type = perlify_record(@data);
|
||||
&::PopGlobalSQLState();
|
||||
|
||||
return $type;
|
||||
}
|
||||
|
||||
=pod
|
||||
|
||||
=over
|
||||
|
||||
=item C<get_inclusions($id)>
|
||||
|
||||
Someone please document this
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
sub get_inclusions {
|
||||
my ($id) = @_;
|
||||
return get_clusions($id, "in");
|
||||
}
|
||||
|
||||
=pod
|
||||
|
||||
=over
|
||||
|
||||
=item C<get_exclusions($id)>
|
||||
|
||||
Someone please document this
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
sub get_exclusions {
|
||||
my ($id) = @_;
|
||||
return get_clusions($id, "ex");
|
||||
}
|
||||
|
||||
=pod
|
||||
|
||||
=over
|
||||
|
||||
=item C<get_clusions($id, $type)>
|
||||
|
||||
Return a hash of product/component IDs and names
|
||||
associated with the flagtype:
|
||||
$clusions{'product_name:component_name'} = "product_ID:component_ID"
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
sub get_clusions {
|
||||
my ($id, $type) = @_;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
my $list =
|
||||
$dbh->selectall_arrayref("SELECT products.id, products.name, " .
|
||||
" components.id, components.name " .
|
||||
"FROM flagtypes, flag${type}clusions " .
|
||||
"LEFT OUTER JOIN products " .
|
||||
" ON flag${type}clusions.product_id = products.id " .
|
||||
"LEFT OUTER JOIN components " .
|
||||
" ON flag${type}clusions.component_id = components.id " .
|
||||
"WHERE flagtypes.id = ? " .
|
||||
" AND flag${type}clusions.type_id = flagtypes.id",
|
||||
undef, $id);
|
||||
my %clusions;
|
||||
foreach my $data (@$list) {
|
||||
my ($product_id, $product_name, $component_id, $component_name) = @$data;
|
||||
$product_id ||= 0;
|
||||
$product_name ||= "__Any__";
|
||||
$component_id ||= 0;
|
||||
$component_name ||= "__Any__";
|
||||
$clusions{"$product_name:$component_name"} = "$product_id:$component_id";
|
||||
}
|
||||
return \%clusions;
|
||||
}
|
||||
|
||||
=pod
|
||||
|
||||
=over
|
||||
|
||||
=item C<match($criteria, $include_count)>
|
||||
|
||||
Queries the database for flag types matching the given criteria
|
||||
and returns the set of matching types.
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
sub match {
|
||||
my ($criteria, $include_count) = @_;
|
||||
|
||||
my @tables = @base_tables;
|
||||
my @columns = @base_columns;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
# Include a count of the number of flags per type if requested.
|
||||
if ($include_count) {
|
||||
push(@columns, "COUNT(flags.id)");
|
||||
push(@tables, "LEFT OUTER JOIN flags ON flagtypes.id = flags.type_id");
|
||||
}
|
||||
|
||||
# Generate the SQL WHERE criteria.
|
||||
my @criteria = sqlify_criteria($criteria, \@tables);
|
||||
|
||||
# Build the query, grouping the types if we are counting flags.
|
||||
# DISTINCT is used in order to count flag types only once when
|
||||
# they appear several times in the flaginclusions table.
|
||||
my $select_clause = "SELECT DISTINCT " . join(", ", @columns);
|
||||
my $from_clause = "FROM " . join(" ", @tables);
|
||||
my $where_clause = "WHERE " . join(" AND ", @criteria);
|
||||
|
||||
my $query = "$select_clause $from_clause $where_clause";
|
||||
$query .= " " . $dbh->sql_group_by('flagtypes.id',
|
||||
join(', ', @base_columns[2..$#base_columns]))
|
||||
if $include_count;
|
||||
$query .= " ORDER BY flagtypes.sortkey, flagtypes.name";
|
||||
|
||||
# Execute the query and retrieve the results.
|
||||
&::PushGlobalSQLState();
|
||||
&::SendSQL($query);
|
||||
my @types;
|
||||
while (&::MoreSQLData()) {
|
||||
my @data = &::FetchSQLData();
|
||||
my $type = perlify_record(@data);
|
||||
push(@types, $type);
|
||||
}
|
||||
&::PopGlobalSQLState();
|
||||
|
||||
return \@types;
|
||||
}
|
||||
|
||||
=pod
|
||||
|
||||
=over
|
||||
|
||||
=item C<count($criteria)>
|
||||
|
||||
Returns the total number of flag types matching the given criteria.
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
sub count {
|
||||
my ($criteria) = @_;
|
||||
|
||||
# Generate query components.
|
||||
my @tables = @base_tables;
|
||||
my @criteria = sqlify_criteria($criteria, \@tables);
|
||||
|
||||
# Build the query.
|
||||
my $select_clause = "SELECT COUNT(flagtypes.id)";
|
||||
my $from_clause = "FROM " . join(" ", @tables);
|
||||
my $where_clause = "WHERE " . join(" AND ", @criteria);
|
||||
my $query = "$select_clause $from_clause $where_clause";
|
||||
|
||||
# Execute the query and get the results.
|
||||
&::PushGlobalSQLState();
|
||||
&::SendSQL($query);
|
||||
my $count = &::FetchOneColumn();
|
||||
&::PopGlobalSQLState();
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
=pod
|
||||
|
||||
=over
|
||||
|
||||
=item C<validate($cgi, $bug_id, $attach_id)>
|
||||
|
||||
Get a list of flag types to validate. Uses the "map" function
|
||||
to extract flag type IDs from form field names by matching columns
|
||||
whose name looks like "flag_type-nnn", where "nnn" is the ID,
|
||||
and returning just the ID portion of matching field names.
|
||||
|
||||
If the attachment is new, it has no ID yet and $attach_id is set
|
||||
to -1 to force its check anyway.
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
sub validate {
|
||||
my ($cgi, $bug_id, $attach_id) = @_;
|
||||
|
||||
my $user = Bugzilla->user;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
my @ids = map(/^flag_type-(\d+)$/ ? $1 : (), $cgi->param());
|
||||
|
||||
return unless scalar(@ids);
|
||||
|
||||
# No flag reference should exist when changing several bugs at once.
|
||||
ThrowCodeError("flags_not_available", { type => 'b' }) unless $bug_id;
|
||||
|
||||
# We don't check that these flag types are valid for
|
||||
# this bug/attachment. This check will be done later when
|
||||
# processing new flags, see Flag::FormToNewFlags().
|
||||
|
||||
foreach my $id (@ids) {
|
||||
my $status = $cgi->param("flag_type-$id");
|
||||
my @requestees = $cgi->param("requestee_type-$id");
|
||||
|
||||
# Don't bother validating types the user didn't touch.
|
||||
next if $status eq "X";
|
||||
|
||||
# Make sure the flag type exists.
|
||||
my $flag_type = get($id);
|
||||
$flag_type
|
||||
|| ThrowCodeError("flag_type_nonexistent", { id => $id });
|
||||
|
||||
# Make sure the flag type is active.
|
||||
$flag_type->{'is_active'}
|
||||
|| ThrowCodeError('flag_type_inactive', {'type' => $flag_type->{'name'}});
|
||||
|
||||
# Make sure the value of the field is a valid status.
|
||||
grep($status eq $_, qw(X + - ?))
|
||||
|| ThrowCodeError("flag_status_invalid",
|
||||
{ id => $id , status => $status });
|
||||
|
||||
# Make sure the user didn't request the flag unless it's requestable.
|
||||
if ($status eq '?' && !$flag_type->{is_requestable}) {
|
||||
ThrowCodeError("flag_status_invalid",
|
||||
{ id => $id , status => $status });
|
||||
}
|
||||
|
||||
# Make sure the user didn't specify a requestee unless the flag
|
||||
# is specifically requestable.
|
||||
if ($status eq '?'
|
||||
&& !$flag_type->{is_requesteeble}
|
||||
&& scalar(@requestees) > 0)
|
||||
{
|
||||
ThrowCodeError("flag_requestee_disabled", { type => $flag_type });
|
||||
}
|
||||
|
||||
# Make sure the user didn't enter multiple requestees for a flag
|
||||
# that can't be requested from more than one person at a time.
|
||||
if ($status eq '?'
|
||||
&& !$flag_type->{is_multiplicable}
|
||||
&& scalar(@requestees) > 1)
|
||||
{
|
||||
ThrowUserError("flag_not_multiplicable", { type => $flag_type });
|
||||
}
|
||||
|
||||
# Make sure the requestees are authorized to access the bug
|
||||
# (and attachment, if this installation is using the "insider group"
|
||||
# feature and the attachment is marked private).
|
||||
if ($status eq '?' && $flag_type->{is_requesteeble}) {
|
||||
foreach my $login (@requestees) {
|
||||
# We know the requestee exists because we ran
|
||||
# Bugzilla::User::match_field before getting here.
|
||||
my $requestee = Bugzilla::User->new_from_login($login);
|
||||
|
||||
# Throw an error if the user can't see the bug.
|
||||
if (!$requestee->can_see_bug($bug_id)) {
|
||||
ThrowUserError("flag_requestee_unauthorized",
|
||||
{ flag_type => $flag_type,
|
||||
requestee => $requestee,
|
||||
bug_id => $bug_id,
|
||||
attach_id => $attach_id });
|
||||
}
|
||||
|
||||
# Throw an error if the target is a private attachment and
|
||||
# the requestee isn't in the group of insiders who can see it.
|
||||
if ($attach_id
|
||||
&& Param("insidergroup")
|
||||
&& $cgi->param('isprivate')
|
||||
&& !$requestee->in_group(Param("insidergroup")))
|
||||
{
|
||||
ThrowUserError("flag_requestee_unauthorized_attachment",
|
||||
{ flag_type => $flag_type,
|
||||
requestee => $requestee,
|
||||
bug_id => $bug_id,
|
||||
attach_id => $attach_id });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Make sure the user is authorized to modify flags, see bug 180879
|
||||
# - User in the $grant_gid group can set flags, including "+" and "-"
|
||||
next if (!$flag_type->{grant_gid}
|
||||
|| $user->in_group(&::GroupIdToName($flag_type->{grant_gid})));
|
||||
|
||||
# - User in the $request_gid group can request flags
|
||||
next if ($status eq '?'
|
||||
&& (!$flag_type->{request_gid}
|
||||
|| $user->in_group(&::GroupIdToName($flag_type->{request_gid}))));
|
||||
|
||||
# - Any other flag modification is denied
|
||||
ThrowUserError("flag_update_denied",
|
||||
{ name => $flag_type->{name},
|
||||
status => $status,
|
||||
old_status => "X" });
|
||||
}
|
||||
}
|
||||
|
||||
=pod
|
||||
|
||||
=over
|
||||
|
||||
=item C<normalize(@ids)>
|
||||
|
||||
Given a list of flag types, checks its flags to make sure they should
|
||||
still exist after a change to the inclusions/exclusions lists.
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
sub normalize {
|
||||
# A list of IDs of flag types to normalize.
|
||||
my (@ids) = @_;
|
||||
|
||||
my $ids = join(", ", @ids);
|
||||
|
||||
# Check for flags whose product/component is no longer included.
|
||||
&::SendSQL("
|
||||
SELECT flags.id
|
||||
FROM (flags INNER JOIN bugs ON flags.bug_id = bugs.bug_id)
|
||||
LEFT OUTER JOIN flaginclusions AS i
|
||||
ON (flags.type_id = i.type_id
|
||||
AND (bugs.product_id = i.product_id OR i.product_id IS NULL)
|
||||
AND (bugs.component_id = i.component_id OR i.component_id IS NULL))
|
||||
WHERE flags.type_id IN ($ids)
|
||||
AND flags.is_active = 1
|
||||
AND i.type_id IS NULL
|
||||
");
|
||||
Bugzilla::Flag::clear(&::FetchOneColumn()) while &::MoreSQLData();
|
||||
|
||||
&::SendSQL("
|
||||
SELECT flags.id
|
||||
FROM flags, bugs, flagexclusions AS e
|
||||
WHERE flags.type_id IN ($ids)
|
||||
AND flags.bug_id = bugs.bug_id
|
||||
AND flags.type_id = e.type_id
|
||||
AND flags.is_active = 1
|
||||
AND (bugs.product_id = e.product_id OR e.product_id IS NULL)
|
||||
AND (bugs.component_id = e.component_id OR e.component_id IS NULL)
|
||||
");
|
||||
Bugzilla::Flag::clear(&::FetchOneColumn()) while &::MoreSQLData();
|
||||
}
|
||||
|
||||
######################################################################
|
||||
# Private Functions
|
||||
######################################################################
|
||||
|
||||
=begin private
|
||||
|
||||
=head1 PRIVATE FUNCTIONS
|
||||
|
||||
=over
|
||||
|
||||
=item C<sqlify_criteria($criteria, $tables)>
|
||||
|
||||
Converts a hash of criteria into a list of SQL criteria.
|
||||
$criteria is a reference to the criteria (field => value),
|
||||
$tables is a reference to an array of tables being accessed
|
||||
by the query.
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
sub sqlify_criteria {
|
||||
my ($criteria, $tables) = @_;
|
||||
|
||||
# the generated list of SQL criteria; "1=1" is a clever way of making sure
|
||||
# there's something in the list so calling code doesn't have to check list
|
||||
# size before building a WHERE clause out of it
|
||||
my @criteria = ("1=1");
|
||||
|
||||
if ($criteria->{name}) {
|
||||
push(@criteria, "flagtypes.name = " . &::SqlQuote($criteria->{name}));
|
||||
}
|
||||
if ($criteria->{target_type}) {
|
||||
# The target type is stored in the database as a one-character string
|
||||
# ("a" for attachment and "b" for bug), but this function takes complete
|
||||
# names ("attachment" and "bug") for clarity, so we must convert them.
|
||||
my $target_type = &::SqlQuote(substr($criteria->{target_type}, 0, 1));
|
||||
push(@criteria, "flagtypes.target_type = $target_type");
|
||||
}
|
||||
if (exists($criteria->{is_active})) {
|
||||
my $is_active = $criteria->{is_active} ? "1" : "0";
|
||||
push(@criteria, "flagtypes.is_active = $is_active");
|
||||
}
|
||||
if ($criteria->{product_id} && $criteria->{'component_id'}) {
|
||||
my $product_id = $criteria->{product_id};
|
||||
my $component_id = $criteria->{component_id};
|
||||
|
||||
# Add inclusions to the query, which simply involves joining the table
|
||||
# by flag type ID and target product/component.
|
||||
push(@$tables, "INNER JOIN flaginclusions AS i ON flagtypes.id = i.type_id");
|
||||
push(@criteria, "(i.product_id = $product_id OR i.product_id IS NULL)");
|
||||
push(@criteria, "(i.component_id = $component_id OR i.component_id IS NULL)");
|
||||
|
||||
# Add exclusions to the query, which is more complicated. First of all,
|
||||
# we do a LEFT JOIN so we don't miss flag types with no exclusions.
|
||||
# Then, as with inclusions, we join on flag type ID and target product/
|
||||
# component. However, since we want flag types that *aren't* on the
|
||||
# exclusions list, we add a WHERE criteria to use only records with
|
||||
# NULL exclusion type, i.e. without any exclusions.
|
||||
my $join_clause = "flagtypes.id = e.type_id " .
|
||||
"AND (e.product_id = $product_id OR e.product_id IS NULL) " .
|
||||
"AND (e.component_id = $component_id OR e.component_id IS NULL)";
|
||||
push(@$tables, "LEFT JOIN flagexclusions AS e ON ($join_clause)");
|
||||
push(@criteria, "e.type_id IS NULL");
|
||||
}
|
||||
if ($criteria->{group}) {
|
||||
my $gid = $criteria->{group};
|
||||
detaint_natural($gid);
|
||||
push(@criteria, "(flagtypes.grant_group_id = $gid " .
|
||||
" OR flagtypes.request_group_id = $gid)");
|
||||
}
|
||||
|
||||
return @criteria;
|
||||
}
|
||||
|
||||
=pod
|
||||
|
||||
=over
|
||||
|
||||
=item C<perlify_record()>
|
||||
|
||||
Converts data retrieved from the database into a Perl record. Depends on the
|
||||
formatting as described in @base_columns.
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
sub perlify_record {
|
||||
my $type = {};
|
||||
|
||||
$type->{'exists'} = $_[0];
|
||||
$type->{'id'} = $_[1];
|
||||
$type->{'name'} = $_[2];
|
||||
$type->{'description'} = $_[3];
|
||||
$type->{'cc_list'} = $_[4];
|
||||
$type->{'target_type'} = $_[5] eq "b" ? "bug" : "attachment";
|
||||
$type->{'sortkey'} = $_[6];
|
||||
$type->{'is_active'} = $_[7];
|
||||
$type->{'is_requestable'} = $_[8];
|
||||
$type->{'is_requesteeble'} = $_[9];
|
||||
$type->{'is_multiplicable'} = $_[10];
|
||||
$type->{'grant_gid'} = $_[11];
|
||||
$type->{'request_gid'} = $_[12];
|
||||
$type->{'flag_count'} = $_[13];
|
||||
|
||||
return $type;
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
=end private
|
||||
|
||||
=head1 SEE ALSO
|
||||
|
||||
=over
|
||||
|
||||
=item B<Bugzilla::Flags>
|
||||
|
||||
=back
|
||||
|
||||
=head1 CONTRIBUTORS
|
||||
|
||||
=over
|
||||
|
||||
=item Myk Melez <myk@mozilla.org>
|
||||
|
||||
=item Kevin Benton <kevin.benton@amd.com>
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
@@ -1,228 +0,0 @@
|
||||
# -*- 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): Joel Peshkin <bugreport@peshkin.net>
|
||||
# Erik Stambaugh <erik@dasbistro.com>
|
||||
# Tiago R. Mello <timello@async.com.br>
|
||||
|
||||
use strict;
|
||||
|
||||
package Bugzilla::Group;
|
||||
|
||||
use Bugzilla::Config;
|
||||
use Bugzilla::Util;
|
||||
use Bugzilla::Error;
|
||||
|
||||
###############################
|
||||
##### Module Initialization ###
|
||||
###############################
|
||||
|
||||
use constant DB_COLUMNS => qw(
|
||||
groups.id
|
||||
groups.name
|
||||
groups.description
|
||||
groups.isbuggroup
|
||||
groups.last_changed
|
||||
groups.userregexp
|
||||
groups.isactive
|
||||
);
|
||||
|
||||
our $columns = join(", ", DB_COLUMNS);
|
||||
|
||||
sub new {
|
||||
my $invocant = shift;
|
||||
my $class = ref($invocant) || $invocant;
|
||||
my $self = {};
|
||||
bless($self, $class);
|
||||
return $self->_init(@_);
|
||||
}
|
||||
|
||||
sub _init {
|
||||
my $self = shift;
|
||||
my ($param) = (@_);
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
my $id = $param unless (ref $param eq 'HASH');
|
||||
my $group;
|
||||
|
||||
if (defined $id) {
|
||||
detaint_natural($id)
|
||||
|| ThrowCodeError('param_must_be_numeric',
|
||||
{function => 'Bugzilla::Group::_init'});
|
||||
|
||||
$group = $dbh->selectrow_hashref(qq{
|
||||
SELECT $columns FROM groups
|
||||
WHERE id = ?}, undef, $id);
|
||||
|
||||
} elsif (defined $param->{'name'}) {
|
||||
|
||||
trick_taint($param->{'name'});
|
||||
|
||||
$group = $dbh->selectrow_hashref(qq{
|
||||
SELECT $columns FROM groups
|
||||
WHERE name = ?}, undef, $param->{'name'});
|
||||
|
||||
} else {
|
||||
ThrowCodeError('bad_arg',
|
||||
{argument => 'param',
|
||||
function => 'Bugzilla::Group::_init'});
|
||||
}
|
||||
|
||||
return undef unless (defined $group);
|
||||
|
||||
foreach my $field (keys %$group) {
|
||||
$self->{$field} = $group->{$field};
|
||||
}
|
||||
return $self;
|
||||
}
|
||||
|
||||
###############################
|
||||
#### Accessors ######
|
||||
###############################
|
||||
|
||||
sub id { return $_[0]->{'id'}; }
|
||||
sub name { return $_[0]->{'name'}; }
|
||||
sub description { return $_[0]->{'description'}; }
|
||||
sub is_bug_group { return $_[0]->{'isbuggroup'}; }
|
||||
sub last_changed { return $_[0]->{'last_changed'}; }
|
||||
sub user_regexp { return $_[0]->{'userregexp'}; }
|
||||
sub is_active { return $_[0]->{'isactive'}; }
|
||||
|
||||
###############################
|
||||
#### Methods ####
|
||||
###############################
|
||||
|
||||
sub is_active_bug_group {
|
||||
my $self = shift;
|
||||
return $self->is_active && $self->is_bug_group;
|
||||
}
|
||||
|
||||
################################
|
||||
##### Module Subroutines ###
|
||||
################################
|
||||
|
||||
sub ValidateGroupName {
|
||||
my ($name, @users) = (@_);
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $query = "SELECT id FROM groups " .
|
||||
"WHERE name = ?";
|
||||
if (Param('usevisibilitygroups')) {
|
||||
my @visible = (-1);
|
||||
foreach my $user (@users) {
|
||||
$user && push @visible, @{$user->visible_groups_direct};
|
||||
}
|
||||
my $visible = join(', ', @visible);
|
||||
$query .= " AND id IN($visible)";
|
||||
}
|
||||
my $sth = $dbh->prepare($query);
|
||||
$sth->execute($name);
|
||||
my ($ret) = $sth->fetchrow_array();
|
||||
return $ret;
|
||||
}
|
||||
|
||||
sub get_all_groups {
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
my $group_ids = $dbh->selectcol_arrayref('SELECT id FROM groups
|
||||
ORDER BY isbuggroup, name');
|
||||
|
||||
my @groups;
|
||||
foreach my $gid (@$group_ids) {
|
||||
push @groups, new Bugzilla::Group($gid);
|
||||
}
|
||||
return @groups;
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::Group - Bugzilla group class.
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
use Bugzilla::Group;
|
||||
|
||||
my $group = new Bugzilla::Group(1);
|
||||
my $group = new Bugzilla::Group({name => 'AcmeGroup'});
|
||||
|
||||
my $id = $group->id;
|
||||
my $name = $group->name;
|
||||
my $description = $group->description;
|
||||
my $last_changed = $group->last_changed;
|
||||
my $user_reg_exp = $group->user_reg_exp;
|
||||
my $is_active = $group->is_active;
|
||||
my $is_active_bug_group = $group->is_active_bug_group;
|
||||
|
||||
my $group_id = Bugzilla::Group::ValidateGroupName('admin', @users);
|
||||
my @groups = Bugzilla::get_all_groups();
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
Group.pm represents a Bugzilla Group object.
|
||||
|
||||
=head1 METHODS
|
||||
|
||||
=over
|
||||
|
||||
=item C<new($param)>
|
||||
|
||||
Description: The constructor is used to load an existing group
|
||||
by passing a group id or a hash with the group name.
|
||||
|
||||
Params: $param - If you pass an integer, the integer is the
|
||||
group id from the database that we want to
|
||||
read in. If you pass in a hash with 'name'
|
||||
key, then the value of the name key is the
|
||||
name of a product from the DB.
|
||||
|
||||
Returns: A Bugzilla::Group object.
|
||||
|
||||
=back
|
||||
|
||||
=head1 SUBROUTINES
|
||||
|
||||
=over
|
||||
|
||||
=item C<ValidateGroupName($name, @users)>
|
||||
|
||||
Description: ValidateGroupName checks to see if ANY of the users
|
||||
in the provided list of user objects can see the
|
||||
named group.
|
||||
|
||||
Params: $name - String with the group name.
|
||||
@users - An array with Bugzilla::User objects.
|
||||
|
||||
Returns: It returns the group id if successful
|
||||
and undef otherwise.
|
||||
|
||||
=item C<get_all_groups()>
|
||||
|
||||
Description: Returns all groups available, including both
|
||||
system groups and bug groups.
|
||||
|
||||
Params: none
|
||||
|
||||
Returns: An array of group objects.
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
@@ -1,195 +0,0 @@
|
||||
# -*- 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): Tiago R. Mello <timello@async.com.br>
|
||||
|
||||
use strict;
|
||||
|
||||
package Bugzilla::Milestone;
|
||||
|
||||
use Bugzilla::Util;
|
||||
use Bugzilla::Error;
|
||||
|
||||
################################
|
||||
##### Initialization #####
|
||||
################################
|
||||
|
||||
use constant DEFAULT_SORTKEY => 0;
|
||||
|
||||
use constant DB_COLUMNS => qw(
|
||||
milestones.value
|
||||
milestones.product_id
|
||||
milestones.sortkey
|
||||
);
|
||||
|
||||
my $columns = join(", ", DB_COLUMNS);
|
||||
|
||||
sub new {
|
||||
my $invocant = shift;
|
||||
my $class = ref($invocant) || $invocant;
|
||||
my $self = {};
|
||||
bless($self, $class);
|
||||
return $self->_init(@_);
|
||||
}
|
||||
|
||||
sub _init {
|
||||
my $self = shift;
|
||||
my ($product_id, $value) = (@_);
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
my $milestone;
|
||||
|
||||
if (defined $product_id
|
||||
&& detaint_natural($product_id)
|
||||
&& defined $value) {
|
||||
|
||||
trick_taint($value);
|
||||
$milestone = $dbh->selectrow_hashref(qq{
|
||||
SELECT $columns FROM milestones
|
||||
WHERE value = ?
|
||||
AND product_id = ?}, undef, ($value, $product_id));
|
||||
} else {
|
||||
ThrowCodeError('bad_arg',
|
||||
{argument => 'product_id/value',
|
||||
function => 'Bugzilla::Milestone::_init'});
|
||||
}
|
||||
|
||||
return undef unless (defined $milestone);
|
||||
|
||||
foreach my $field (keys %$milestone) {
|
||||
$self->{$field} = $milestone->{$field};
|
||||
}
|
||||
return $self;
|
||||
}
|
||||
|
||||
sub bug_count {
|
||||
my $self = shift;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
if (!defined $self->{'bug_count'}) {
|
||||
$self->{'bug_count'} = $dbh->selectrow_array(q{
|
||||
SELECT COUNT(*) FROM bugs
|
||||
WHERE product_id = ? AND target_milestone = ?},
|
||||
undef, $self->product_id, $self->name) || 0;
|
||||
}
|
||||
return $self->{'bug_count'};
|
||||
}
|
||||
|
||||
################################
|
||||
##### Accessors ######
|
||||
################################
|
||||
|
||||
sub name { return $_[0]->{'value'}; }
|
||||
sub product_id { return $_[0]->{'product_id'}; }
|
||||
sub sortkey { return $_[0]->{'sortkey'}; }
|
||||
|
||||
################################
|
||||
##### Subroutines #####
|
||||
################################
|
||||
|
||||
sub check_milestone {
|
||||
my ($product, $milestone_name) = @_;
|
||||
|
||||
unless ($milestone_name) {
|
||||
ThrowUserError('milestone_not_specified');
|
||||
}
|
||||
|
||||
my $milestone = new Bugzilla::Milestone($product->id,
|
||||
$milestone_name);
|
||||
unless ($milestone) {
|
||||
ThrowUserError('milestone_not_valid',
|
||||
{'product' => $product->name,
|
||||
'milestone' => $milestone_name});
|
||||
}
|
||||
return $milestone;
|
||||
}
|
||||
|
||||
sub check_sort_key {
|
||||
my ($milestone_name, $sortkey) = @_;
|
||||
# Keep a copy in case detaint_signed() clears the sortkey
|
||||
my $stored_sortkey = $sortkey;
|
||||
|
||||
if (!detaint_signed($sortkey) || $sortkey < -32768
|
||||
|| $sortkey > 32767) {
|
||||
ThrowUserError('milestone_sortkey_invalid',
|
||||
{'name' => $milestone_name,
|
||||
'sortkey' => $stored_sortkey});
|
||||
}
|
||||
return $sortkey;
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::Milestone - Bugzilla product milestone class.
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
use Bugzilla::Milestone;
|
||||
|
||||
my $milestone = new Bugzilla::Milestone(1, 'milestone_value');
|
||||
|
||||
my $product_id = $milestone->product_id;
|
||||
my $value = $milestone->value;
|
||||
|
||||
my $milestone = $hash_ref->{'milestone_value'};
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
Milestone.pm represents a Product Milestone object.
|
||||
|
||||
=head1 METHODS
|
||||
|
||||
=over
|
||||
|
||||
=item C<new($product_id, $value)>
|
||||
|
||||
Description: The constructor is used to load an existing milestone
|
||||
by passing a product id and a milestone value.
|
||||
|
||||
Params: $product_id - Integer with a Bugzilla product id.
|
||||
$value - String with a milestone value.
|
||||
|
||||
Returns: A Bugzilla::Milestone object.
|
||||
|
||||
=item C<bug_count()>
|
||||
|
||||
Description: Returns the total of bugs that belong to the milestone.
|
||||
|
||||
Params: none.
|
||||
|
||||
Returns: Integer with the number of bugs.
|
||||
|
||||
=back
|
||||
|
||||
=head1 SUBROUTINES
|
||||
|
||||
=over
|
||||
|
||||
=item C<check_milestone($product, $milestone_name)>
|
||||
|
||||
Description: Checks if a milestone name was passed in
|
||||
and if it is a valid milestone.
|
||||
|
||||
Params: $product - Bugzilla::Product object.
|
||||
$milestone_name - String with a milestone name.
|
||||
|
||||
Returns: Bugzilla::Milestone object.
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
@@ -1,387 +0,0 @@
|
||||
# -*- 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): Tiago R. Mello <timello@async.com.br>
|
||||
|
||||
use strict;
|
||||
|
||||
package Bugzilla::Product;
|
||||
|
||||
use Bugzilla::Component;
|
||||
use Bugzilla::Version;
|
||||
use Bugzilla::Milestone;
|
||||
|
||||
use Bugzilla::Util;
|
||||
use Bugzilla::Group;
|
||||
use Bugzilla::Error;
|
||||
|
||||
use constant DEFAULT_CLASSIFICATION_ID => 1;
|
||||
|
||||
###############################
|
||||
#### Initialization ####
|
||||
###############################
|
||||
|
||||
use constant DB_COLUMNS => qw(
|
||||
products.id
|
||||
products.name
|
||||
products.classification_id
|
||||
products.description
|
||||
products.milestoneurl
|
||||
products.disallownew
|
||||
products.votesperuser
|
||||
products.maxvotesperbug
|
||||
products.votestoconfirm
|
||||
products.defaultmilestone
|
||||
);
|
||||
|
||||
my $columns = join(", ", DB_COLUMNS);
|
||||
|
||||
sub new {
|
||||
my $invocant = shift;
|
||||
my $class = ref($invocant) || $invocant;
|
||||
my $self = {};
|
||||
bless($self, $class);
|
||||
return $self->_init(@_);
|
||||
}
|
||||
|
||||
sub _init {
|
||||
my $self = shift;
|
||||
my ($param) = @_;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
my $id = $param unless (ref $param eq 'HASH');
|
||||
my $product;
|
||||
|
||||
if (defined $id) {
|
||||
detaint_natural($id)
|
||||
|| ThrowCodeError('param_must_be_numeric',
|
||||
{function => 'Bugzilla::Product::_init'});
|
||||
|
||||
$product = $dbh->selectrow_hashref(qq{
|
||||
SELECT $columns FROM products
|
||||
WHERE id = ?}, undef, $id);
|
||||
|
||||
} elsif (defined $param->{'name'}) {
|
||||
|
||||
trick_taint($param->{'name'});
|
||||
$product = $dbh->selectrow_hashref(qq{
|
||||
SELECT $columns FROM products
|
||||
WHERE name = ?}, undef, $param->{'name'});
|
||||
} else {
|
||||
ThrowCodeError('bad_arg',
|
||||
{argument => 'param',
|
||||
function => 'Bugzilla::Product::_init'});
|
||||
}
|
||||
|
||||
return undef unless (defined $product);
|
||||
|
||||
foreach my $field (keys %$product) {
|
||||
$self->{$field} = $product->{$field};
|
||||
}
|
||||
return $self;
|
||||
}
|
||||
|
||||
###############################
|
||||
#### Methods ####
|
||||
###############################
|
||||
|
||||
sub components {
|
||||
my $self = shift;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
if (!defined $self->{components}) {
|
||||
my $ids = $dbh->selectcol_arrayref(q{
|
||||
SELECT id FROM components
|
||||
WHERE product_id = ?
|
||||
ORDER BY name}, undef, $self->id);
|
||||
|
||||
my @components;
|
||||
foreach my $id (@$ids) {
|
||||
push @components, new Bugzilla::Component($id);
|
||||
}
|
||||
$self->{components} = \@components;
|
||||
}
|
||||
return $self->{components};
|
||||
}
|
||||
|
||||
sub group_controls {
|
||||
my $self = shift;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
if (!defined $self->{group_controls}) {
|
||||
my $query = qq{SELECT
|
||||
groups.id,
|
||||
group_control_map.entry,
|
||||
group_control_map.membercontrol,
|
||||
group_control_map.othercontrol,
|
||||
group_control_map.canedit
|
||||
FROM groups
|
||||
LEFT JOIN group_control_map
|
||||
ON groups.id = group_control_map.group_id
|
||||
WHERE group_control_map.product_id = ?
|
||||
AND groups.isbuggroup != 0
|
||||
ORDER BY groups.name};
|
||||
$self->{group_controls} =
|
||||
$dbh->selectall_hashref($query, 'id', undef, $self->id);
|
||||
foreach my $group (keys(%{$self->{group_controls}})) {
|
||||
$self->{group_controls}->{$group}->{'group'} =
|
||||
new Bugzilla::Group($group);
|
||||
}
|
||||
}
|
||||
return $self->{group_controls};
|
||||
}
|
||||
|
||||
sub versions {
|
||||
my $self = shift;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
if (!defined $self->{versions}) {
|
||||
my $values = $dbh->selectcol_arrayref(q{
|
||||
SELECT value FROM versions
|
||||
WHERE product_id = ?
|
||||
ORDER BY value}, undef, $self->id);
|
||||
|
||||
my @versions;
|
||||
foreach my $value (@$values) {
|
||||
push @versions, new Bugzilla::Version($self->id, $value);
|
||||
}
|
||||
$self->{versions} = \@versions;
|
||||
}
|
||||
return $self->{versions};
|
||||
}
|
||||
|
||||
sub milestones {
|
||||
my $self = shift;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
if (!defined $self->{milestones}) {
|
||||
my $values = $dbh->selectcol_arrayref(q{
|
||||
SELECT value FROM milestones
|
||||
WHERE product_id = ?
|
||||
ORDER BY sortkey}, undef, $self->id);
|
||||
|
||||
my @milestones;
|
||||
foreach my $value (@$values) {
|
||||
push @milestones, new Bugzilla::Milestone($self->id, $value);
|
||||
}
|
||||
$self->{milestones} = \@milestones;
|
||||
}
|
||||
return $self->{milestones};
|
||||
}
|
||||
|
||||
sub bug_count {
|
||||
my $self = shift;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
if (!defined $self->{'bug_count'}) {
|
||||
$self->{'bug_count'} = $dbh->selectrow_array(qq{
|
||||
SELECT COUNT(bug_id) FROM bugs
|
||||
WHERE product_id = ?}, undef, $self->id);
|
||||
|
||||
}
|
||||
return $self->{'bug_count'};
|
||||
}
|
||||
|
||||
sub bug_ids {
|
||||
my $self = shift;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
if (!defined $self->{'bug_ids'}) {
|
||||
$self->{'bug_ids'} =
|
||||
$dbh->selectcol_arrayref(q{SELECT bug_id FROM bugs
|
||||
WHERE product_id = ?},
|
||||
undef, $self->id);
|
||||
}
|
||||
return $self->{'bug_ids'};
|
||||
}
|
||||
|
||||
|
||||
###############################
|
||||
#### Accessors ######
|
||||
###############################
|
||||
|
||||
sub id { return $_[0]->{'id'}; }
|
||||
sub name { return $_[0]->{'name'}; }
|
||||
sub description { return $_[0]->{'description'}; }
|
||||
sub milestone_url { return $_[0]->{'milestoneurl'}; }
|
||||
sub disallow_new { return $_[0]->{'disallownew'}; }
|
||||
sub votes_per_user { return $_[0]->{'votesperuser'}; }
|
||||
sub max_votes_per_bug { return $_[0]->{'maxvotesperbug'}; }
|
||||
sub votes_to_confirm { return $_[0]->{'votestoconfirm'}; }
|
||||
sub default_milestone { return $_[0]->{'defaultmilestone'}; }
|
||||
sub classification_id { return $_[0]->{'classification_id'}; }
|
||||
|
||||
###############################
|
||||
#### Subroutines ######
|
||||
###############################
|
||||
|
||||
sub get_all_products {
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
my $ids = $dbh->selectcol_arrayref(q{
|
||||
SELECT id FROM products ORDER BY name});
|
||||
|
||||
my @products;
|
||||
foreach my $id (@$ids) {
|
||||
push @products, new Bugzilla::Product($id);
|
||||
}
|
||||
return @products;
|
||||
}
|
||||
|
||||
sub check_product {
|
||||
my ($product_name) = @_;
|
||||
|
||||
unless ($product_name) {
|
||||
ThrowUserError('product_not_specified');
|
||||
}
|
||||
my $product = new Bugzilla::Product({name => $product_name});
|
||||
unless ($product) {
|
||||
ThrowUserError('product_doesnt_exist',
|
||||
{'product' => $product_name});
|
||||
}
|
||||
return $product;
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::Product - Bugzilla product class.
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
use Bugzilla::Product;
|
||||
|
||||
my $product = new Bugzilla::Product(1);
|
||||
my $product = new Bugzilla::Product('AcmeProduct');
|
||||
|
||||
my @components = $product->components();
|
||||
my $groups_controls = $product->group_controls();
|
||||
my @milestones = $product->milestones();
|
||||
my @versions = $product->versions();
|
||||
my $bugcount = $product->bug_count();
|
||||
my $bug_ids = $product->bug_ids();
|
||||
|
||||
my $id = $product->id;
|
||||
my $name = $product->name;
|
||||
my $description = $product->description;
|
||||
my $milestoneurl = $product->milestone_url;
|
||||
my disallownew = $product->disallow_new;
|
||||
my votesperuser = $product->votes_per_user;
|
||||
my maxvotesperbug = $product->max_votes_per_bug;
|
||||
my votestoconfirm = $product->votes_to_confirm;
|
||||
my $defaultmilestone = $product->default_milestone;
|
||||
my $classificationid = $product->classification_id;
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
Product.pm represents a product object.
|
||||
|
||||
=head1 METHODS
|
||||
|
||||
=over
|
||||
|
||||
=item C<new($param)>
|
||||
|
||||
Description: The constructor is used to load an existing product
|
||||
by passing a product id or a hash.
|
||||
|
||||
Params: $param - If you pass an integer, the integer is the
|
||||
product id from the database that we want to
|
||||
read in. If you pass in a hash with 'name' key,
|
||||
then the value of the name key is the name of a
|
||||
product from the DB.
|
||||
|
||||
Returns: A Bugzilla::Product object.
|
||||
|
||||
=item C<components()>
|
||||
|
||||
Description: Returns an array of component objects belonging to
|
||||
the product.
|
||||
|
||||
Params: none.
|
||||
|
||||
Returns: An array of Bugzilla::Component object.
|
||||
|
||||
=item C<group_controls()>
|
||||
|
||||
Description: Returns a hash (group id as key) with all product
|
||||
group controls.
|
||||
|
||||
Params: none.
|
||||
|
||||
Returns: A hash with group id as key and hash containing
|
||||
a Bugzilla::Group object and the properties of group
|
||||
relative to the product.
|
||||
|
||||
=item C<versions()>
|
||||
|
||||
Description: Returns all valid versions for that product.
|
||||
|
||||
Params: none.
|
||||
|
||||
Returns: An array of Bugzilla::Version objects.
|
||||
|
||||
=item C<milestones()>
|
||||
|
||||
Description: Returns all valid milestones for that product.
|
||||
|
||||
Params: none.
|
||||
|
||||
Returns: An array of Bugzilla::Milestone objects.
|
||||
|
||||
=item C<bug_count()>
|
||||
|
||||
Description: Returns the total of bugs that belong to the product.
|
||||
|
||||
Params: none.
|
||||
|
||||
Returns: Integer with the number of bugs.
|
||||
|
||||
=item C<bug_ids()>
|
||||
|
||||
Description: Returns the IDs of bugs that belong to the product.
|
||||
|
||||
Params: none.
|
||||
|
||||
Returns: An array of integer.
|
||||
|
||||
=back
|
||||
|
||||
=head1 SUBROUTINES
|
||||
|
||||
=over
|
||||
|
||||
=item C<get_all_products()>
|
||||
|
||||
Description: Returns all products from the database.
|
||||
|
||||
Params: none.
|
||||
|
||||
Returns: Bugzilla::Product object list.
|
||||
|
||||
=item C<check_product($product_name)>
|
||||
|
||||
Description: Checks if the product name was passed in and if is a valid
|
||||
product.
|
||||
|
||||
Params: $product_name - String with a product name.
|
||||
|
||||
Returns: Bugzilla::Product object.
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,498 +0,0 @@
|
||||
# -*- 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): C. Begle
|
||||
# Jesse Ruderman
|
||||
# Andreas Franke <afranke@mathweb.org>
|
||||
# Stephen Lee <slee@uk.bnsmc.com>
|
||||
# Marc Schumann <wurblzap@gmail.com>
|
||||
|
||||
package Bugzilla::Search::Quicksearch;
|
||||
|
||||
# Make it harder for us to do dangerous things in Perl.
|
||||
use strict;
|
||||
|
||||
use Bugzilla::Config;
|
||||
use Bugzilla::Error;
|
||||
use Bugzilla::Util;
|
||||
|
||||
use base qw(Exporter);
|
||||
@Bugzilla::Search::Quicksearch::EXPORT = qw(quicksearch);
|
||||
|
||||
|
||||
# Word renamings
|
||||
my %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",
|
||||
# Attachments
|
||||
"attachment" => "attachments.description",
|
||||
"attachmentdesc" => "attachments.description",
|
||||
"attachdesc" => "attachments.description",
|
||||
"attachmentdata" => "attach_data.thedata",
|
||||
"attachdata" => "attach_data.thedata",
|
||||
"attachmentmimetype" => "attachments.mimetype",
|
||||
"attachmimetype" => "attachments.mimetype");
|
||||
|
||||
# We might want to put this into localconfig or somewhere
|
||||
my @platforms = ('pc', 'sun', 'macintosh', 'mac');
|
||||
my @productExceptions = ('row' # [Browser]
|
||||
# ^^^
|
||||
,'new' # [MailNews]
|
||||
# ^^^
|
||||
);
|
||||
my @componentExceptions = ('hang' # [Bugzilla: Component/Keyword Changes]
|
||||
# ^^^^
|
||||
);
|
||||
|
||||
# Quicksearch-wide globals for boolean charts.
|
||||
my $chart = 0;
|
||||
my $and = 0;
|
||||
my $or = 0;
|
||||
|
||||
sub quicksearch {
|
||||
my ($searchstring) = (@_);
|
||||
my $urlbase = correct_urlbase();
|
||||
my $cgi = Bugzilla->cgi;
|
||||
|
||||
# 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');
|
||||
}
|
||||
}
|
||||
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.
|
||||
|
||||
&::GetVersionTable();
|
||||
|
||||
# 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 < Param('quicksearch_comment_cutoff');
|
||||
my @openStates = &::OpenStates();
|
||||
my @closedStates;
|
||||
my (%states, %resolutions);
|
||||
|
||||
foreach (@::legal_bug_status) {
|
||||
push(@closedStates, $_) unless &::IsOpenedState($_);
|
||||
}
|
||||
foreach (@openStates) { $states{$_} = 1 }
|
||||
if ($words[0] eq 'ALL') {
|
||||
foreach (@::legal_bug_status) { $states{$_} = 1 }
|
||||
shift @words;
|
||||
}
|
||||
elsif ($words[0] eq 'OPEN') {
|
||||
shift @words;
|
||||
}
|
||||
elsif ($words[0] =~ /^\+[A-Z]+(,[A-Z]+)*$/) {
|
||||
# e.g. +DUP,FIX
|
||||
if (matchPrefixes(\%states,
|
||||
\%resolutions,
|
||||
[split(/,/, substr($words[0], 1))],
|
||||
\@closedStates,
|
||||
\@::legal_resolution)) {
|
||||
shift @words;
|
||||
# Allowing additional resolutions means we need to keep
|
||||
# the "no resolution" resolution.
|
||||
$resolutions{'---'} = 1;
|
||||
}
|
||||
else {
|
||||
# Carry on if no match found.
|
||||
}
|
||||
}
|
||||
elsif ($words[0] =~ /^[A-Z]+(,[A-Z]+)*$/) {
|
||||
# e.g. NEW,ASSI,REOP,FIX
|
||||
undef %states;
|
||||
if (matchPrefixes(\%states,
|
||||
\%resolutions,
|
||||
[split(/,/, $words[0])],
|
||||
\@::legal_bug_status,
|
||||
\@::legal_resolution)) {
|
||||
shift @words;
|
||||
}
|
||||
else {
|
||||
# Carry on if no match found
|
||||
foreach (@openStates) { $states{$_} = 1 }
|
||||
}
|
||||
}
|
||||
else {
|
||||
# Default: search for unresolved bugs only.
|
||||
# Put custom code here if you would like to change this behaviour.
|
||||
}
|
||||
|
||||
# If we have wanted resolutions, allow closed states
|
||||
if (keys(%resolutions)) {
|
||||
foreach (@closedStates) { $states{$_} = 1 }
|
||||
}
|
||||
|
||||
$cgi->param('bug_status', keys(%states));
|
||||
$cgi->param('resolution', keys(%resolutions));
|
||||
|
||||
# Loop over all main-level QuickSearch words.
|
||||
foreach my $qsword (@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 '+') {
|
||||
foreach (@subWords) {
|
||||
addChart('short_desc', 'substring', $qsword, $negate);
|
||||
}
|
||||
}
|
||||
elsif ($firstChar eq '#') {
|
||||
addChart('short_desc', 'anywords', $baseWord, $negate);
|
||||
if ($searchComments) {
|
||||
addChart('longdesc', 'anywords', $baseWord, $negate);
|
||||
}
|
||||
}
|
||||
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 '[') {
|
||||
addChart('short_desc', 'substring', $baseWord, $negate);
|
||||
addChart('status_whiteboard', 'substring', $baseWord, $negate);
|
||||
}
|
||||
elsif ($firstChar eq '!') {
|
||||
addChart('keywords', 'anywords', $baseWord, $negate);
|
||||
|
||||
}
|
||||
else { # No special first char
|
||||
|
||||
# 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, $negate);
|
||||
}
|
||||
elsif ($or_operand =~ /^([^:]+):([^:]+)$/) {
|
||||
# generic field1,field2,field3:value1,value2 notation
|
||||
my @fields = split(/,/, $1);
|
||||
my @values = split(/,/, $2);
|
||||
foreach my $field (@fields) {
|
||||
# Be tolerant about unknown fields
|
||||
next unless defined($mappings{$field});
|
||||
$field = $mappings{$field};
|
||||
foreach (@values) {
|
||||
addChart($field, 'substring', $_, $negate);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
|
||||
# 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
|
||||
if (grep({lc($word) eq $_} @platforms)) {
|
||||
addChart('rep_platform', 'substring',
|
||||
$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)}
|
||||
@::legal_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
|
||||
|
||||
if (!grep({lc($word) eq $_}
|
||||
@productExceptions) &&
|
||||
length($word)>2
|
||||
) {
|
||||
addChart('product', 'substring',
|
||||
$word, $negate);
|
||||
}
|
||||
if (!grep({lc($word) eq $_}
|
||||
@componentExceptions) &&
|
||||
length($word)>2
|
||||
) {
|
||||
addChart('component', 'substring',
|
||||
$word, $negate);
|
||||
}
|
||||
if (grep({lc($word) eq $_}
|
||||
@::legal_keywords)) {
|
||||
addChart('keywords', 'substring',
|
||||
$word, $negate);
|
||||
if (length($word)>2) {
|
||||
addChart('short_desc', 'substring',
|
||||
$word, $negate);
|
||||
addChart('status_whiteboard',
|
||||
'substring',
|
||||
$word, $negate);
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
|
||||
addChart('short_desc', 'substring',
|
||||
$word, $negate);
|
||||
addChart('status_whiteboard', 'substring',
|
||||
$word, $negate);
|
||||
}
|
||||
if ($searchComments) {
|
||||
addChart('longdesc', 'substring',
|
||||
$word, $negate);
|
||||
}
|
||||
}
|
||||
# URL field (for IP addrs, host.names,
|
||||
# scheme://urls)
|
||||
if ($word =~ m/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/
|
||||
|| $word =~ /^[A-Za-z]+(\.[A-Za-z]+)+/
|
||||
|| $word =~ /:[\\\/][\\\/]/
|
||||
|| $word =~ /localhost/
|
||||
|| $word =~ /mailto[:]?/
|
||||
# || $word =~ /[A-Za-z]+[:][0-9]+/ #host:port
|
||||
) {
|
||||
addChart('bug_file_loc', 'substring',
|
||||
$word, $negate);
|
||||
}
|
||||
} # foreach my $word (split(/,/, $qsword))
|
||||
} # votes and generic field detection
|
||||
} # foreach (split(/\|/, $_))
|
||||
} # "switch" $firstChar
|
||||
$chart++;
|
||||
$and = 0;
|
||||
$or = 0;
|
||||
} # foreach (@words)
|
||||
|
||||
# We've been very tolerant about invalid queries, so all that's left
|
||||
# may be an empty query.
|
||||
scalar($cgi->param())>0 || ThrowUserError("buglist_parameters_required");
|
||||
}
|
||||
|
||||
# List of quicksearch-specific CGI parameters to get rid of.
|
||||
my @params_to_strip = ('quicksearch', 'load', 'run');
|
||||
my $modified_query_string = $cgi->canonicalise_query(@params_to_strip);
|
||||
|
||||
if ($cgi->param('load')) {
|
||||
# Param 'load' asks us to display the query in the advanced search form.
|
||||
print $cgi->redirect(-uri => "${urlbase}query.cgi?format=advanced&" .
|
||||
$modified_query_string);
|
||||
}
|
||||
|
||||
# Otherwise, pass the modified query string to the caller.
|
||||
# We modified $cgi->params, so the caller can choose to look at that, too,
|
||||
# and disregard the return value.
|
||||
$cgi->delete(@params_to_strip);
|
||||
return $modified_query_string;
|
||||
}
|
||||
|
||||
###########################################################################
|
||||
# Helpers
|
||||
###########################################################################
|
||||
|
||||
# Split string on whitespace, retaining quoted strings as one
|
||||
sub splitString {
|
||||
my $string = shift;
|
||||
my @quoteparts;
|
||||
my @parts;
|
||||
my $i = 0;
|
||||
|
||||
# Escape backslashes
|
||||
$string =~ s/\\/\\\//g;
|
||||
|
||||
# Now split on quote sign; be tolerant about unclosed quotes
|
||||
@quoteparts = split(/"/, $string);
|
||||
foreach (@quoteparts) {
|
||||
# After every odd quote, escape whitespace
|
||||
s/(\s)/\\$1/g if $i++ % 2;
|
||||
}
|
||||
# Join again
|
||||
$string = join('"', @quoteparts);
|
||||
|
||||
# Now split on unescaped whitespace
|
||||
@parts = split(/(?<!\\)\s+/, $string);
|
||||
foreach (@parts) {
|
||||
# Restore whitespace
|
||||
s/\\(\s)/$1/g;
|
||||
# Restore backslashes
|
||||
s/\\\//\\/g;
|
||||
# Remove quotes
|
||||
s/"//g;
|
||||
}
|
||||
return @parts;
|
||||
}
|
||||
|
||||
# Expand found prefixes to states or resolutions
|
||||
sub matchPrefixes {
|
||||
my $hr_states = shift;
|
||||
my $hr_resolutions = shift;
|
||||
my $ar_prefixes = shift;
|
||||
my $ar_check_states = shift;
|
||||
my $ar_check_resolutions = shift;
|
||||
my $foundMatch = 0;
|
||||
|
||||
foreach my $prefix (@$ar_prefixes) {
|
||||
foreach (@$ar_check_states) {
|
||||
if (/^$prefix/) {
|
||||
$$hr_states{$_} = 1;
|
||||
$foundMatch = 1;
|
||||
}
|
||||
}
|
||||
foreach (@$ar_check_resolutions) {
|
||||
if (/^$prefix/) {
|
||||
$$hr_resolutions{$_} = 1;
|
||||
$foundMatch = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $foundMatch;
|
||||
}
|
||||
|
||||
# Negate comparison type
|
||||
sub negateComparisonType {
|
||||
my $comparisonType = shift;
|
||||
|
||||
if ($comparisonType eq 'substring') {
|
||||
return 'notsubstring';
|
||||
}
|
||||
elsif ($comparisonType eq 'anywords') {
|
||||
return 'nowords';
|
||||
}
|
||||
elsif ($comparisonType eq 'regexp') {
|
||||
return 'notregexp';
|
||||
}
|
||||
else {
|
||||
# Don't know how to negate that
|
||||
ThrowCodeError('unknown_comparison_type');
|
||||
}
|
||||
}
|
||||
|
||||
# Add a boolean chart
|
||||
sub addChart {
|
||||
my ($field, $comparisonType, $value, $negate) = @_;
|
||||
|
||||
$negate && ($comparisonType = negateComparisonType($comparisonType));
|
||||
makeChart("$chart-$and-$or", $field, $comparisonType, $value);
|
||||
if ($negate) {
|
||||
$and++;
|
||||
$or = 0;
|
||||
}
|
||||
else {
|
||||
$or++;
|
||||
}
|
||||
}
|
||||
|
||||
# Create the CGI parameters for a boolean chart
|
||||
sub makeChart {
|
||||
my ($expr, $field, $type, $value) = @_;
|
||||
my $cgi = Bugzilla->cgi;
|
||||
|
||||
$cgi->param("field$expr", $field);
|
||||
$cgi->param("type$expr", $type);
|
||||
$cgi->param("value$expr", $value);
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -1,260 +0,0 @@
|
||||
# -*- 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): Gervase Markham <gerv@gerv.net>
|
||||
# Lance Larsh <lance.larsh@oracle.com>
|
||||
|
||||
use strict;
|
||||
use lib ".";
|
||||
|
||||
# This module implements a series - a set of data to be plotted on a chart.
|
||||
#
|
||||
# This Series is in the database if and only if self->{'series_id'} is defined.
|
||||
# Note that the series being in the database does not mean that the fields of
|
||||
# this object are the same as the DB entries, as the object may have been
|
||||
# altered.
|
||||
|
||||
package Bugzilla::Series;
|
||||
|
||||
use Bugzilla::Util;
|
||||
use Bugzilla::User;
|
||||
|
||||
use constant PUBLIC_USER_ID => 0;
|
||||
|
||||
sub new {
|
||||
my $invocant = shift;
|
||||
my $class = ref($invocant) || $invocant;
|
||||
|
||||
# Create a ref to an empty hash and bless it
|
||||
my $self = {};
|
||||
bless($self, $class);
|
||||
|
||||
my $arg_count = scalar(@_);
|
||||
|
||||
# new() can return undef if you pass in a series_id and the user doesn't
|
||||
# have sufficient permissions. If you create a new series in this way,
|
||||
# you need to check for an undef return, and act appropriately.
|
||||
my $retval = $self;
|
||||
|
||||
# There are three ways of creating Series objects. Two (CGI and Parameters)
|
||||
# are for use when creating a new series. One (Database) is for retrieving
|
||||
# information on existing series.
|
||||
if ($arg_count == 1) {
|
||||
if (ref($_[0])) {
|
||||
# We've been given a CGI object to create a new Series from.
|
||||
# This series may already exist - external code needs to check
|
||||
# before it calls writeToDatabase().
|
||||
$self->initFromCGI($_[0]);
|
||||
}
|
||||
else {
|
||||
# We've been given a series_id, which should represent an existing
|
||||
# Series.
|
||||
$retval = $self->initFromDatabase($_[0]);
|
||||
}
|
||||
}
|
||||
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.
|
||||
$self->initFromParameters(@_);
|
||||
}
|
||||
else {
|
||||
die("Bad parameters passed in - invalid number of args: $arg_count");
|
||||
}
|
||||
|
||||
return $retval;
|
||||
}
|
||||
|
||||
sub initFromDatabase {
|
||||
my $self = shift;
|
||||
my $series_id = shift;
|
||||
|
||||
&::detaint_natural($series_id)
|
||||
|| &::ThrowCodeError("invalid_series_id", { 'series_id' => $series_id });
|
||||
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my @series = $dbh->selectrow_array("SELECT series.series_id, cc1.name, " .
|
||||
"cc2.name, series.name, series.creator, series.frequency, " .
|
||||
"series.query, series.is_public " .
|
||||
"FROM series " .
|
||||
"LEFT JOIN series_categories AS cc1 " .
|
||||
" ON series.category = cc1.id " .
|
||||
"LEFT JOIN series_categories AS cc2 " .
|
||||
" ON series.subcategory = cc2.id " .
|
||||
"LEFT JOIN category_group_map AS cgm " .
|
||||
" ON series.category = cgm.category_id " .
|
||||
"LEFT JOIN user_group_map AS ugm " .
|
||||
" ON cgm.group_id = ugm.group_id " .
|
||||
" AND ugm.user_id = " . Bugzilla->user->id .
|
||||
" AND isbless = 0 " .
|
||||
"WHERE series.series_id = $series_id AND " .
|
||||
"(is_public = 1 OR creator = " . Bugzilla->user->id . " OR " .
|
||||
"(ugm.group_id IS NOT NULL)) " .
|
||||
$dbh->sql_group_by('series.series_id', 'cc1.name, cc2.name, ' .
|
||||
'series.name, series.creator, series.frequency, ' .
|
||||
'series.query, series.is_public'));
|
||||
|
||||
if (@series) {
|
||||
$self->initFromParameters(@series);
|
||||
return $self;
|
||||
}
|
||||
else {
|
||||
return undef;
|
||||
}
|
||||
}
|
||||
|
||||
sub initFromParameters {
|
||||
# Pass undef as the first parameter if you are creating a new series.
|
||||
my $self = shift;
|
||||
|
||||
($self->{'series_id'}, $self->{'category'}, $self->{'subcategory'},
|
||||
$self->{'name'}, $self->{'creator'}, $self->{'frequency'},
|
||||
$self->{'query'}, $self->{'public'}) = @_;
|
||||
}
|
||||
|
||||
sub initFromCGI {
|
||||
my $self = shift;
|
||||
my $cgi = shift;
|
||||
|
||||
$self->{'series_id'} = $cgi->param('series_id') || undef;
|
||||
if (defined($self->{'series_id'})) {
|
||||
detaint_natural($self->{'series_id'})
|
||||
|| &::ThrowCodeError("invalid_series_id",
|
||||
{ 'series_id' => $self->{'series_id'} });
|
||||
}
|
||||
|
||||
$self->{'category'} = $cgi->param('category')
|
||||
|| $cgi->param('newcategory')
|
||||
|| &::ThrowUserError("missing_category");
|
||||
|
||||
$self->{'subcategory'} = $cgi->param('subcategory')
|
||||
|| $cgi->param('newsubcategory')
|
||||
|| &::ThrowUserError("missing_subcategory");
|
||||
|
||||
$self->{'name'} = $cgi->param('name')
|
||||
|| &::ThrowUserError("missing_name");
|
||||
|
||||
$self->{'creator'} = Bugzilla->user->id;
|
||||
|
||||
$self->{'frequency'} = $cgi->param('frequency');
|
||||
detaint_natural($self->{'frequency'})
|
||||
|| &::ThrowUserError("missing_frequency");
|
||||
|
||||
$self->{'query'} = $cgi->canonicalise_query("format", "ctype", "action",
|
||||
"category", "subcategory", "name",
|
||||
"frequency", "public", "query_format");
|
||||
trick_taint($self->{'query'});
|
||||
|
||||
$self->{'public'} = $cgi->param('public') ? 1 : 0;
|
||||
|
||||
# Change 'admin' here and in series.html.tmpl, or remove the check
|
||||
# completely, if you want to change who can make series public.
|
||||
$self->{'public'} = 0 unless UserInGroup('admin');
|
||||
}
|
||||
|
||||
sub writeToDatabase {
|
||||
my $self = shift;
|
||||
|
||||
my $dbh = Bugzilla->dbh;
|
||||
$dbh->bz_lock_tables('series_categories WRITE', 'series WRITE');
|
||||
|
||||
my $category_id = getCategoryID($self->{'category'});
|
||||
my $subcategory_id = getCategoryID($self->{'subcategory'});
|
||||
|
||||
my $exists;
|
||||
if ($self->{'series_id'}) {
|
||||
$exists =
|
||||
$dbh->selectrow_array("SELECT series_id FROM series
|
||||
WHERE series_id = $self->{'series_id'}");
|
||||
}
|
||||
|
||||
# Is this already in the database?
|
||||
if ($exists) {
|
||||
# Update existing series
|
||||
my $dbh = Bugzilla->dbh;
|
||||
$dbh->do("UPDATE series SET " .
|
||||
"category = ?, subcategory = ?," .
|
||||
"name = ?, frequency = ?, is_public = ? " .
|
||||
"WHERE series_id = ?", undef,
|
||||
$category_id, $subcategory_id, $self->{'name'},
|
||||
$self->{'frequency'}, $self->{'public'},
|
||||
$self->{'series_id'});
|
||||
}
|
||||
else {
|
||||
# Insert the new series into the series table
|
||||
$dbh->do("INSERT INTO series (creator, category, subcategory, " .
|
||||
"name, frequency, query, is_public) VALUES " .
|
||||
"($self->{'creator'}, " .
|
||||
"$category_id, $subcategory_id, " .
|
||||
$dbh->quote($self->{'name'}) . ", $self->{'frequency'}," .
|
||||
$dbh->quote($self->{'query'}) . ", $self->{'public'})");
|
||||
|
||||
# Retrieve series_id
|
||||
$self->{'series_id'} = $dbh->selectrow_array("SELECT MAX(series_id) " .
|
||||
"FROM series");
|
||||
$self->{'series_id'}
|
||||
|| &::ThrowCodeError("missing_series_id", { 'series' => $self });
|
||||
}
|
||||
|
||||
$dbh->bz_unlock_tables();
|
||||
}
|
||||
|
||||
# Check whether a series with this name, category and subcategory exists in
|
||||
# the DB and, if so, returns its series_id.
|
||||
sub existsInDatabase {
|
||||
my $self = shift;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
my $category_id = getCategoryID($self->{'category'});
|
||||
my $subcategory_id = getCategoryID($self->{'subcategory'});
|
||||
|
||||
trick_taint($self->{'name'});
|
||||
my $series_id = $dbh->selectrow_array("SELECT series_id " .
|
||||
"FROM series WHERE category = $category_id " .
|
||||
"AND subcategory = $subcategory_id AND name = " .
|
||||
$dbh->quote($self->{'name'}));
|
||||
|
||||
return($series_id);
|
||||
}
|
||||
|
||||
# Get a category or subcategory IDs, creating the category if it doesn't exist.
|
||||
sub getCategoryID {
|
||||
my ($category) = @_;
|
||||
my $category_id;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
# This seems for the best idiom for "Do A. Then maybe do B and A again."
|
||||
while (1) {
|
||||
# We are quoting this to put it in the DB, so we can remove taint
|
||||
trick_taint($category);
|
||||
|
||||
$category_id = $dbh->selectrow_array("SELECT id " .
|
||||
"from series_categories " .
|
||||
"WHERE name =" . $dbh->quote($category));
|
||||
|
||||
last if defined($category_id);
|
||||
|
||||
$dbh->do("INSERT INTO series_categories (name) " .
|
||||
"VALUES (" . $dbh->quote($category) . ")");
|
||||
}
|
||||
|
||||
return $category_id;
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -1,643 +0,0 @@
|
||||
# -*- 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>
|
||||
# Dan Mosedale <dmose@mozilla.org>
|
||||
# Jacob Steenhagen <jake@bugzilla.org>
|
||||
# Bradley Baetz <bbaetz@student.usyd.edu.au>
|
||||
# Christopher Aillon <christopher@aillon.com>
|
||||
# Tobias Burnus <burnus@net-b.de>
|
||||
# Myk Melez <myk@mozilla.org>
|
||||
# Max Kanat-Alexander <mkanat@bugzilla.org>
|
||||
# Frédéric Buclin <LpSolit@gmail.com>
|
||||
# Greg Hendricks <ghendricks@novell.com>
|
||||
|
||||
|
||||
package Bugzilla::Template;
|
||||
|
||||
use strict;
|
||||
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::Config qw(:DEFAULT $templatedir $datadir $project);
|
||||
use Bugzilla::Util;
|
||||
use Bugzilla::User;
|
||||
use Bugzilla::Error;
|
||||
use MIME::Base64;
|
||||
|
||||
# for time2str - replace by TT Date plugin??
|
||||
use Date::Format ();
|
||||
|
||||
use base qw(Template);
|
||||
|
||||
# Convert the constants in the Bugzilla::Constants module into a hash we can
|
||||
# pass to the template object for reflection into its "constants" namespace
|
||||
# (which is like its "variables" namespace, but for constants). To do so, we
|
||||
# traverse the arrays of exported and exportable symbols, pulling out functions
|
||||
# (which is how Perl implements constants) and ignoring the rest (which, if
|
||||
# Constants.pm exports only constants, as it should, will be nothing else).
|
||||
use Bugzilla::Constants ();
|
||||
my %constants;
|
||||
foreach my $constant (@Bugzilla::Constants::EXPORT,
|
||||
@Bugzilla::Constants::EXPORT_OK)
|
||||
{
|
||||
if (defined Bugzilla::Constants->$constant) {
|
||||
# Constants can be lists, and we can't know whether we're getting
|
||||
# a scalar or a list in advance, since they come to us as the return
|
||||
# value of a function call, so we have to retrieve them all in list
|
||||
# context into anonymous arrays, then extract the scalar ones (i.e.
|
||||
# the ones whose arrays contain a single element) from their arrays.
|
||||
$constants{$constant} = [&{$Bugzilla::Constants::{$constant}}];
|
||||
if (scalar(@{$constants{$constant}}) == 1) {
|
||||
$constants{$constant} = @{$constants{$constant}}[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# XXX - mod_perl
|
||||
my $template_include_path;
|
||||
|
||||
# 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.
|
||||
sub sortAcceptLanguage {
|
||||
sub sortQvalue { $b->{'qvalue'} <=> $a->{'qvalue'} }
|
||||
my $accept_language = $_[0];
|
||||
|
||||
# clean up string.
|
||||
$accept_language =~ s/[^A-Za-z;q=0-9\.\-,]//g;
|
||||
my @qlanguages;
|
||||
my @languages;
|
||||
foreach(split /,/, $accept_language) {
|
||||
if (m/([A-Za-z\-]+)(?:;q=(\d(?:\.\d+)))?/) {
|
||||
my $lang = $1;
|
||||
my $qvalue = $2;
|
||||
$qvalue = 1 if not defined $qvalue;
|
||||
next if $qvalue == 0;
|
||||
$qvalue = 1 if $qvalue > 1;
|
||||
push(@qlanguages, {'qvalue' => $qvalue, 'language' => $lang});
|
||||
}
|
||||
}
|
||||
|
||||
return map($_->{'language'}, (sort sortQvalue @qlanguages));
|
||||
}
|
||||
|
||||
# Returns the path to the templates based on the Accept-Language
|
||||
# settings of the user and of the available languages
|
||||
# If no Accept-Language is present it uses the defined default
|
||||
sub getTemplateIncludePath {
|
||||
# Return cached value if available
|
||||
|
||||
# XXXX - mod_perl!
|
||||
if ($template_include_path) {
|
||||
return $template_include_path;
|
||||
}
|
||||
my $languages = trim(Param('languages'));
|
||||
if (not ($languages =~ /,/)) {
|
||||
if ($project) {
|
||||
$template_include_path = [
|
||||
"$templatedir/$languages/$project",
|
||||
"$templatedir/$languages/custom",
|
||||
"$templatedir/$languages/extension",
|
||||
"$templatedir/$languages/default"
|
||||
];
|
||||
} else {
|
||||
$template_include_path = [
|
||||
"$templatedir/$languages/custom",
|
||||
"$templatedir/$languages/extension",
|
||||
"$templatedir/$languages/default"
|
||||
];
|
||||
}
|
||||
return $template_include_path;
|
||||
}
|
||||
my @languages = sortAcceptLanguage($languages);
|
||||
my @accept_language = sortAcceptLanguage($ENV{'HTTP_ACCEPT_LANGUAGE'} || "" );
|
||||
my @usedlanguages;
|
||||
foreach my $lang (@accept_language) {
|
||||
# Per RFC 1766 and RFC 2616 any language tag matches also its
|
||||
# primary tag. That is 'en' (accept language) matches 'en-us',
|
||||
# 'en-uk' etc. but not the otherway round. (This is unfortunately
|
||||
# not very clearly stated in those RFC; see comment just over 14.5
|
||||
# in http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4)
|
||||
if(my @found = grep /^\Q$lang\E(-.+)?$/i, @languages) {
|
||||
push (@usedlanguages, @found);
|
||||
}
|
||||
}
|
||||
push(@usedlanguages, Param('defaultlanguage'));
|
||||
if ($project) {
|
||||
$template_include_path = [
|
||||
map((
|
||||
"$templatedir/$_/$project",
|
||||
"$templatedir/$_/custom",
|
||||
"$templatedir/$_/extension",
|
||||
"$templatedir/$_/default"
|
||||
), @usedlanguages
|
||||
)
|
||||
];
|
||||
} else {
|
||||
$template_include_path = [
|
||||
map((
|
||||
"$templatedir/$_/custom",
|
||||
"$templatedir/$_/extension",
|
||||
"$templatedir/$_/default"
|
||||
), @usedlanguages
|
||||
)
|
||||
];
|
||||
}
|
||||
return $template_include_path;
|
||||
}
|
||||
|
||||
sub put_header {
|
||||
my $self = shift;
|
||||
my $vars = {};
|
||||
($vars->{'title'}, $vars->{'h1'}, $vars->{'h2'}) = (@_);
|
||||
|
||||
$self->process("global/header.html.tmpl", $vars)
|
||||
|| ThrowTemplateError($self->error());
|
||||
$vars->{'header_done'} = 1;
|
||||
}
|
||||
|
||||
sub put_footer {
|
||||
my $self = shift;
|
||||
$self->process("global/footer.html.tmpl")
|
||||
|| ThrowTemplateError($self->error());
|
||||
}
|
||||
|
||||
sub get_format {
|
||||
my $self = shift;
|
||||
my ($template, $format, $ctype) = @_;
|
||||
|
||||
$ctype ||= 'html';
|
||||
$format ||= '';
|
||||
|
||||
# Security - allow letters and a hyphen only
|
||||
$ctype =~ s/[^a-zA-Z\-]//g;
|
||||
$format =~ s/[^a-zA-Z\-]//g;
|
||||
trick_taint($ctype);
|
||||
trick_taint($format);
|
||||
|
||||
$template .= ($format ? "-$format" : "");
|
||||
$template .= ".$ctype.tmpl";
|
||||
|
||||
# Now check that the template actually exists. We only want to check
|
||||
# if the template exists; any other errors (eg parse errors) will
|
||||
# end up being detected later.
|
||||
eval {
|
||||
$self->context->template($template);
|
||||
};
|
||||
# This parsing may seem fragile, but its OK:
|
||||
# http://lists.template-toolkit.org/pipermail/templates/2003-March/004370.html
|
||||
# Even if it is wrong, any sort of error is going to cause a failure
|
||||
# eventually, so the only issue would be an incorrect error message
|
||||
if ($@ && $@->info =~ /: not found$/) {
|
||||
ThrowUserError('format_not_found', {'format' => $format,
|
||||
'ctype' => $ctype});
|
||||
}
|
||||
|
||||
# Else, just return the info
|
||||
return
|
||||
{
|
||||
'template' => $template,
|
||||
'extension' => $ctype,
|
||||
'ctype' => Bugzilla::Constants::contenttypes->{$ctype}
|
||||
};
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# Templatization Code
|
||||
|
||||
# The Template Toolkit throws an error if a loop iterates >1000 times.
|
||||
# We want to raise that limit.
|
||||
# NOTE: If you change this number, you MUST RE-RUN checksetup.pl!!!
|
||||
# If you do not re-run checksetup.pl, the change you make will not apply
|
||||
$Template::Directive::WHILE_MAX = 1000000;
|
||||
|
||||
# Use the Toolkit Template's Stash module to add utility pseudo-methods
|
||||
# to template variables.
|
||||
use Template::Stash;
|
||||
|
||||
# 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.
|
||||
$Template::Stash::LIST_OPS->{ contains } =
|
||||
sub {
|
||||
my ($list, $item) = @_;
|
||||
return grep($_ eq $item, @$list);
|
||||
};
|
||||
|
||||
$Template::Stash::LIST_OPS->{ containsany } =
|
||||
sub {
|
||||
my ($list, $items) = @_;
|
||||
foreach my $item (@$items) {
|
||||
return 1 if grep($_ eq $item, @$list);
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
# Allow us to still get the scalar if we use the list operation ".0" on it,
|
||||
# as we often do for defaults in query.cgi and other places.
|
||||
$Template::Stash::SCALAR_OPS->{ 0 } =
|
||||
sub {
|
||||
return $_[0];
|
||||
};
|
||||
|
||||
# Add a "substr" method to the Template Toolkit's "scalar" object
|
||||
# that returns a substring of a string.
|
||||
$Template::Stash::SCALAR_OPS->{ substr } =
|
||||
sub {
|
||||
my ($scalar, $offset, $length) = @_;
|
||||
return substr($scalar, $offset, $length);
|
||||
};
|
||||
|
||||
# Add a "truncate" method to the Template Toolkit's "scalar" object
|
||||
# that truncates a string to a certain length.
|
||||
$Template::Stash::SCALAR_OPS->{ truncate } =
|
||||
sub {
|
||||
my ($string, $length, $ellipsis) = @_;
|
||||
$ellipsis ||= "";
|
||||
|
||||
return $string if !$length || length($string) <= $length;
|
||||
|
||||
my $strlen = $length - length($ellipsis);
|
||||
my $newstr = substr($string, 0, $strlen) . $ellipsis;
|
||||
return $newstr;
|
||||
};
|
||||
|
||||
# Create the template object that processes templates and specify
|
||||
# configuration parameters that apply to all templates.
|
||||
|
||||
###############################################################################
|
||||
|
||||
# Construct the Template object
|
||||
|
||||
# Note that all of the failure cases here can't use templateable errors,
|
||||
# since we won't have a template to use...
|
||||
|
||||
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'}) {
|
||||
$template_include_path = undef;
|
||||
}
|
||||
|
||||
# IMPORTANT - If you make any configuration changes here, make sure to
|
||||
# make them in t/004.template.t and checksetup.pl.
|
||||
|
||||
return $class->new({
|
||||
# Colon-separated list of directories containing templates.
|
||||
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
|
||||
# looking, more compact content. Use the plus sign at the beginning
|
||||
# of directives to maintain white space (i.e. [%+ DIRECTIVE %]).
|
||||
PRE_CHOMP => 1,
|
||||
TRIM => 1,
|
||||
|
||||
COMPILE_DIR => "$datadir/template",
|
||||
|
||||
# Initialize templates (f.e. by loading plugins like Hook).
|
||||
PRE_PROCESS => "global/initialize.none.tmpl",
|
||||
|
||||
# Functions for processing text within templates in various ways.
|
||||
# IMPORTANT! When adding a filter here that does not override a
|
||||
# built-in filter, please also add a stub filter to t/004template.t.
|
||||
FILTERS => {
|
||||
|
||||
# Render text in required style.
|
||||
|
||||
inactive => [
|
||||
sub {
|
||||
my($context, $isinactive) = @_;
|
||||
return sub {
|
||||
return $isinactive ? '<span class="bz_inactive">'.$_[0].'</span>' : $_[0];
|
||||
}
|
||||
}, 1
|
||||
],
|
||||
|
||||
closed => [
|
||||
sub {
|
||||
my($context, $isclosed) = @_;
|
||||
return sub {
|
||||
return $isclosed ? '<span class="bz_closed">'.$_[0].'</span>' : $_[0];
|
||||
}
|
||||
}, 1
|
||||
],
|
||||
|
||||
obsolete => [
|
||||
sub {
|
||||
my($context, $isobsolete) = @_;
|
||||
return sub {
|
||||
return $isobsolete ? '<span class="bz_obsolete">'.$_[0].'</span>' : $_[0];
|
||||
}
|
||||
}, 1
|
||||
],
|
||||
|
||||
# Returns the text with backslashes, single/double quotes,
|
||||
# and newlines/carriage returns escaped for use in JS strings.
|
||||
js => sub {
|
||||
my ($var) = @_;
|
||||
$var =~ s/([\\\'\"\/])/\\$1/g;
|
||||
$var =~ s/\n/\\n/g;
|
||||
$var =~ s/\r/\\r/g;
|
||||
$var =~ s/\@/\\x40/g; # anti-spam for email addresses
|
||||
return $var;
|
||||
},
|
||||
|
||||
# Converts data to base64
|
||||
base64 => sub {
|
||||
my ($data) = @_;
|
||||
return encode_base64($data);
|
||||
},
|
||||
|
||||
# HTML collapses newlines in element attributes to a single space,
|
||||
# so form elements which may have whitespace (ie comments) need
|
||||
# to be encoded using 
|
||||
# See bugs 4928, 22983 and 32000 for more details
|
||||
html_linebreak => sub {
|
||||
my ($var) = @_;
|
||||
$var =~ s/\r\n/\
/g;
|
||||
$var =~ s/\n\r/\
/g;
|
||||
$var =~ s/\r/\
/g;
|
||||
$var =~ s/\n/\
/g;
|
||||
return $var;
|
||||
},
|
||||
|
||||
# Prevents line break on hyphens and whitespaces.
|
||||
no_break => sub {
|
||||
my ($var) = @_;
|
||||
$var =~ s/ /\ /g;
|
||||
$var =~ s/-/\‑/g;
|
||||
return $var;
|
||||
},
|
||||
|
||||
xml => \&Bugzilla::Util::xml_quote ,
|
||||
|
||||
# This filter escapes characters in a variable or value string for
|
||||
# use in a query string. It escapes all characters NOT in the
|
||||
# regex set: [a-zA-Z0-9_\-.]. The 'uri' filter should be used for
|
||||
# a full URL that may have characters that need encoding.
|
||||
url_quote => \&Bugzilla::Util::url_quote ,
|
||||
|
||||
# This filter is similar to url_quote but used a \ instead of a %
|
||||
# as prefix. In addition it replaces a ' ' by a '_'.
|
||||
css_class_quote => \&Bugzilla::Util::css_class_quote ,
|
||||
|
||||
quoteUrls => [ sub {
|
||||
my ($context, $bug) = @_;
|
||||
return sub {
|
||||
my $text = shift;
|
||||
return &::quoteUrls($text, $bug);
|
||||
};
|
||||
},
|
||||
1
|
||||
],
|
||||
|
||||
bug_link => [ sub {
|
||||
my ($context, $bug) = @_;
|
||||
return sub {
|
||||
my $text = shift;
|
||||
return &::GetBugLink($bug, $text);
|
||||
};
|
||||
},
|
||||
1
|
||||
],
|
||||
|
||||
# In CSV, quotes are doubled, and any value containing a quote or a
|
||||
# comma is enclosed in quotes.
|
||||
csv => sub
|
||||
{
|
||||
my ($var) = @_;
|
||||
$var =~ s/\"/\"\"/g;
|
||||
if ($var !~ /^-?(\d+\.)?\d*$/) {
|
||||
$var = "\"$var\"";
|
||||
}
|
||||
return $var;
|
||||
} ,
|
||||
|
||||
# Format a filesize in bytes to a human readable value
|
||||
unitconvert => sub
|
||||
{
|
||||
my ($data) = @_;
|
||||
my $retval = "";
|
||||
my %units = (
|
||||
'KB' => 1024,
|
||||
'MB' => 1024 * 1024,
|
||||
'GB' => 1024 * 1024 * 1024,
|
||||
);
|
||||
|
||||
if ($data < 1024) {
|
||||
return "$data bytes";
|
||||
}
|
||||
else {
|
||||
my $u;
|
||||
foreach $u ('GB', 'MB', 'KB') {
|
||||
if ($data >= $units{$u}) {
|
||||
return sprintf("%.2f %s", $data/$units{$u}, $u);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
# Format a time for display (more info in Bugzilla::Util)
|
||||
time => \&Bugzilla::Util::format_time,
|
||||
|
||||
# Bug 120030: Override html filter to obscure the '@' in user
|
||||
# visible strings.
|
||||
# Bug 319331: Handle BiDi disruptions.
|
||||
html => sub {
|
||||
my ($var) = Template::Filters::html_filter(@_);
|
||||
# Obscure '@'.
|
||||
$var =~ s/\@/\@/g;
|
||||
if (Param('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 |
|
||||
# --------------------------------------------------------
|
||||
#
|
||||
# Do the replacing in a loop so that we don't get tricked
|
||||
# by stuff like 0xe2 0xe2 0x80 0xae 0x80 0xae.
|
||||
while ($var =~ s/\xe2\x80(\xaa|\xab|\xac|\xad|\xae)//g) {
|
||||
}
|
||||
}
|
||||
return $var;
|
||||
},
|
||||
|
||||
html_light => \&Bugzilla::Util::html_light_quote,
|
||||
|
||||
# iCalendar contentline filter
|
||||
ics => [ sub {
|
||||
my ($context, @args) = @_;
|
||||
return sub {
|
||||
my ($var) = shift;
|
||||
my ($par) = shift @args;
|
||||
my ($output) = "";
|
||||
|
||||
$var =~ s/[\r\n]/ /g;
|
||||
$var =~ s/([;\\\",])/\\$1/g;
|
||||
|
||||
if ($par) {
|
||||
$output = sprintf("%s:%s", $par, $var);
|
||||
} else {
|
||||
$output = $var;
|
||||
}
|
||||
|
||||
$output =~ s/(.{75,75})/$1\n /g;
|
||||
|
||||
return $output;
|
||||
};
|
||||
},
|
||||
1
|
||||
],
|
||||
|
||||
# Wrap a displayed comment to the appropriate length
|
||||
wrap_comment => \&Bugzilla::Util::wrap_comment,
|
||||
|
||||
# We force filtering of every variable in key security-critical
|
||||
# places; we have a none filter for people to use when they
|
||||
# really, really don't want a variable to be changed.
|
||||
none => sub { return $_[0]; } ,
|
||||
},
|
||||
|
||||
PLUGIN_BASE => 'Bugzilla::Template::Plugin',
|
||||
|
||||
CONSTANTS => \%constants,
|
||||
|
||||
# Default variables for all templates
|
||||
VARIABLES => {
|
||||
# Function for retrieving global parameters.
|
||||
'Param' => \&Bugzilla::Config::Param,
|
||||
|
||||
# Function to create date strings
|
||||
'time2str' => \&Date::Format::time2str,
|
||||
|
||||
# Generic linear search function
|
||||
'lsearch' => \&Bugzilla::Util::lsearch,
|
||||
|
||||
# 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; },
|
||||
|
||||
# If an sudo session is in progress, this is the user who
|
||||
# started the session.
|
||||
'sudoer' => sub { return Bugzilla->sudoer; },
|
||||
|
||||
# UserInGroup. Deprecated - use the user.* functions instead
|
||||
'UserInGroup' => \&Bugzilla::User::UserInGroup,
|
||||
|
||||
# SendBugMail - sends mail about a bug, using Bugzilla::BugMail.pm
|
||||
'SendBugMail' => sub {
|
||||
my ($id, $mailrecipients) = (@_);
|
||||
require Bugzilla::BugMail;
|
||||
Bugzilla::BugMail::Send($id, $mailrecipients);
|
||||
},
|
||||
|
||||
# Bugzilla version
|
||||
# This could be made a ref, or even a CONSTANT with TT2.08
|
||||
'VERSION' => $Bugzilla::Config::VERSION ,
|
||||
},
|
||||
|
||||
}) || die("Template creation failed: " . $class->error());
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::Template - Wrapper around the Template Toolkit C<Template> object
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
my $template = Bugzilla::Template->create;
|
||||
|
||||
$template->put_header($title, $h1, $h2);
|
||||
$template->put_footer();
|
||||
|
||||
my $format = $template->get_format("foo/bar",
|
||||
scalar($cgi->param('format')),
|
||||
scalar($cgi->param('ctype')));
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
This is basically a wrapper so that the correct arguments get passed into
|
||||
the C<Template> constructor.
|
||||
|
||||
It should not be used directly by scripts or modules - instead, use
|
||||
C<Bugzilla-E<gt>instance-E<gt>template> to get an already created module.
|
||||
|
||||
=head1 METHODS
|
||||
|
||||
=over
|
||||
|
||||
=item C<put_header($title, $h1, $h2)>
|
||||
|
||||
Description: Display the header of the page for non yet templatized .cgi files.
|
||||
|
||||
Params: $title - Page title.
|
||||
$h1 - Main page header.
|
||||
$h2 - Page subheader.
|
||||
|
||||
Returns: nothing
|
||||
|
||||
=item C<put_footer()>
|
||||
|
||||
Description: Display the footer of the page for non yet templatized .cgi files.
|
||||
|
||||
Params: none
|
||||
|
||||
Returns: nothing
|
||||
|
||||
=item C<get_format($file, $format, $ctype)>
|
||||
|
||||
Description: Construct a format object from URL parameters.
|
||||
|
||||
Params: $file - Name of the template to display.
|
||||
$format - When the template exists under several formats
|
||||
(e.g. table or graph), specify the one to choose.
|
||||
$ctype - Content type, see Bugzilla::Constants::contenttypes.
|
||||
|
||||
Returns: A format object.
|
||||
|
||||
=back
|
||||
|
||||
=head1 SEE ALSO
|
||||
|
||||
L<Bugzilla>, L<Template>
|
||||
@@ -1,64 +0,0 @@
|
||||
# -*- 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): Bradley Baetz <bbaetz@student.usyd.edu.au>
|
||||
#
|
||||
|
||||
package Bugzilla::Template::Plugin::Bugzilla;
|
||||
|
||||
use strict;
|
||||
|
||||
use base qw(Template::Plugin);
|
||||
|
||||
use Bugzilla;
|
||||
|
||||
sub new {
|
||||
my ($class, $context) = @_;
|
||||
|
||||
return bless {}, $class;
|
||||
}
|
||||
|
||||
sub AUTOLOAD {
|
||||
my $class = shift;
|
||||
our $AUTOLOAD;
|
||||
|
||||
$AUTOLOAD =~ s/^.*:://;
|
||||
|
||||
return if $AUTOLOAD eq 'DESTROY';
|
||||
|
||||
return Bugzilla->$AUTOLOAD(@_);
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::Template::Plugin::Bugzilla
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
Template Toolkit plugin to allow access to the persistent C<Bugzilla>
|
||||
object.
|
||||
|
||||
=head1 SEE ALSO
|
||||
|
||||
L<Bugzilla>, L<Template::Plugin>
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
# -*- 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): Myk Melez <myk@mozilla.org>
|
||||
#
|
||||
|
||||
package Bugzilla::Template::Plugin::Hook;
|
||||
|
||||
use strict;
|
||||
|
||||
use base qw(Template::Plugin);
|
||||
|
||||
sub load {
|
||||
my ($class, $context) = @_;
|
||||
return $class;
|
||||
}
|
||||
|
||||
sub new {
|
||||
my ($class, $context) = @_;
|
||||
return bless { _CONTEXT => $context }, $class;
|
||||
}
|
||||
|
||||
sub process {
|
||||
my ($self, $hook_name) = @_;
|
||||
|
||||
my $paths = $self->{_CONTEXT}->{LOAD_TEMPLATES}->[0]->paths;
|
||||
my $template = $self->{_CONTEXT}->stash->{component}->{name};
|
||||
my @hooks = ();
|
||||
|
||||
foreach my $path (@$paths) {
|
||||
my @files = glob("$path/hook/$template/$hook_name/*.tmpl");
|
||||
|
||||
# Have to remove the templates path (INCLUDE_PATH) from the
|
||||
# file path since the template processor auto-adds it back.
|
||||
@files = map($_ =~ /^$path\/(.*)$/ ? $1 : {}, @files);
|
||||
|
||||
# Add found files to the list of hooks, but removing duplicates,
|
||||
# which can happen when there are identical hooks or duplicate
|
||||
# directories in the INCLUDE_PATH (the latter probably being a TT bug).
|
||||
foreach my $file (@files) {
|
||||
push(@hooks, $file) unless grep($file eq $_, @hooks);
|
||||
}
|
||||
}
|
||||
|
||||
my $output;
|
||||
foreach my $hook (@hooks) {
|
||||
$output .= $self->{_CONTEXT}->process($hook);
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::Template::Plugin::Hook
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
Template Toolkit plugin to process hooks added into templates by extensions.
|
||||
|
||||
=head1 SEE ALSO
|
||||
|
||||
L<Template::Plugin>,
|
||||
L<http:E<sol>E<sol>bugzilla.mozilla.orgE<sol>show_bug.cgi?id=229658>
|
||||
@@ -1,65 +0,0 @@
|
||||
# -*- 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): Bradley Baetz <bbaetz@student.usyd.edu.au>
|
||||
# Joel Peshkin <bugreport@peshkin.net>
|
||||
#
|
||||
|
||||
package Bugzilla::Template::Plugin::User;
|
||||
|
||||
use strict;
|
||||
|
||||
use base qw(Template::Plugin);
|
||||
|
||||
use Bugzilla::User;
|
||||
|
||||
sub new {
|
||||
my ($class, $context) = @_;
|
||||
|
||||
return bless {}, $class;
|
||||
}
|
||||
|
||||
sub AUTOLOAD {
|
||||
my $class = shift;
|
||||
our $AUTOLOAD;
|
||||
|
||||
$AUTOLOAD =~ s/^.*:://;
|
||||
|
||||
return if $AUTOLOAD eq 'DESTROY';
|
||||
|
||||
return Bugzilla::User->$AUTOLOAD(@_);
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::Template::Plugin::User
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
Template Toolkit plugin to allow access to the C<User>
|
||||
object.
|
||||
|
||||
=head1 SEE ALSO
|
||||
|
||||
L<Bugzilla::User>, L<Template::Plugin>
|
||||
|
||||
@@ -1,534 +0,0 @@
|
||||
# -*- 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): Myk Melez <myk@mozilla.org>
|
||||
# Frédéric Buclin <LpSolit@gmail.com>
|
||||
|
||||
################################################################################
|
||||
# Module Initialization
|
||||
################################################################################
|
||||
|
||||
# Make it harder for us to do dangerous things in Perl.
|
||||
use strict;
|
||||
|
||||
# Bundle the functions in this file together into the "Bugzilla::Token" package.
|
||||
package Bugzilla::Token;
|
||||
|
||||
use Bugzilla::Config;
|
||||
use Bugzilla::Error;
|
||||
use Bugzilla::BugMail;
|
||||
use Bugzilla::Util;
|
||||
|
||||
use Date::Format;
|
||||
use Date::Parse;
|
||||
use File::Basename;
|
||||
|
||||
use base qw(Exporter);
|
||||
|
||||
@Bugzilla::Token::EXPORT = qw(issue_session_token check_token_data delete_token);
|
||||
|
||||
# This module requires that its caller have said "require globals.pl" to import
|
||||
# relevant functions from that script.
|
||||
|
||||
################################################################################
|
||||
# Constants
|
||||
################################################################################
|
||||
|
||||
# The maximum number of days a token will remain valid.
|
||||
my $maxtokenage = 3;
|
||||
|
||||
################################################################################
|
||||
# Public Functions
|
||||
################################################################################
|
||||
|
||||
sub IssueEmailChangeToken {
|
||||
my ($userid, $old_email, $new_email) = @_;
|
||||
|
||||
my ($token, $token_ts) = _create_token($userid, 'emailold', $old_email . ":" . $new_email);
|
||||
|
||||
my $newtoken = _create_token($userid, 'emailnew', $old_email . ":" . $new_email);
|
||||
|
||||
# Mail the user the token along with instructions for using it.
|
||||
|
||||
my $template = Bugzilla->template;
|
||||
my $vars = {};
|
||||
|
||||
$vars->{'oldemailaddress'} = $old_email . Param('emailsuffix');
|
||||
$vars->{'newemailaddress'} = $new_email . Param('emailsuffix');
|
||||
|
||||
$vars->{'max_token_age'} = $maxtokenage;
|
||||
$vars->{'token_ts'} = $token_ts;
|
||||
|
||||
$vars->{'token'} = $token;
|
||||
$vars->{'emailaddress'} = $old_email . Param('emailsuffix');
|
||||
|
||||
my $message;
|
||||
$template->process("account/email/change-old.txt.tmpl", $vars, \$message)
|
||||
|| ThrowTemplateError($template->error());
|
||||
|
||||
Bugzilla::BugMail::MessageToMTA($message);
|
||||
|
||||
$vars->{'token'} = $newtoken;
|
||||
$vars->{'emailaddress'} = $new_email . Param('emailsuffix');
|
||||
|
||||
$message = "";
|
||||
$template->process("account/email/change-new.txt.tmpl", $vars, \$message)
|
||||
|| ThrowTemplateError($template->error());
|
||||
|
||||
Bugzilla::BugMail::MessageToMTA($message);
|
||||
}
|
||||
|
||||
sub IssuePasswordToken {
|
||||
# Generates a random token, adds it to the tokens table, and sends it
|
||||
# to the user with instructions for using it to change their password.
|
||||
|
||||
my ($loginname) = @_;
|
||||
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
# Retrieve the user's ID from the database.
|
||||
my $quotedloginname = &::SqlQuote($loginname);
|
||||
&::SendSQL("SELECT profiles.userid, tokens.issuedate FROM profiles
|
||||
LEFT JOIN tokens
|
||||
ON tokens.userid = profiles.userid
|
||||
AND tokens.tokentype = 'password'
|
||||
AND tokens.issuedate > NOW() - " .
|
||||
$dbh->sql_interval(10, 'MINUTE') . "
|
||||
WHERE " . $dbh->sql_istrcmp('login_name', $quotedloginname));
|
||||
my ($userid, $toosoon) = &::FetchSQLData();
|
||||
|
||||
if ($toosoon) {
|
||||
ThrowUserError('too_soon_for_new_token');
|
||||
};
|
||||
|
||||
my ($token, $token_ts) = _create_token($userid, 'password', $::ENV{'REMOTE_ADDR'});
|
||||
|
||||
# Mail the user the token along with instructions for using it.
|
||||
|
||||
my $template = Bugzilla->template;
|
||||
my $vars = {};
|
||||
|
||||
$vars->{'token'} = $token;
|
||||
$vars->{'emailaddress'} = $loginname . Param('emailsuffix');
|
||||
|
||||
$vars->{'max_token_age'} = $maxtokenage;
|
||||
$vars->{'token_ts'} = $token_ts;
|
||||
|
||||
my $message = "";
|
||||
$template->process("account/password/forgotten-password.txt.tmpl",
|
||||
$vars, \$message)
|
||||
|| ThrowTemplateError($template->error());
|
||||
|
||||
Bugzilla::BugMail::MessageToMTA($message);
|
||||
}
|
||||
|
||||
sub issue_session_token {
|
||||
# Generates a random token, adds it to the tokens table, and returns
|
||||
# the token to the caller.
|
||||
|
||||
my $data = shift;
|
||||
return _create_token(Bugzilla->user->id, 'session', $data);
|
||||
}
|
||||
|
||||
sub CleanTokenTable {
|
||||
my $dbh = Bugzilla->dbh;
|
||||
$dbh->bz_lock_tables('tokens WRITE');
|
||||
&::SendSQL("DELETE FROM tokens WHERE " .
|
||||
$dbh->sql_to_days('NOW()') . " - " .
|
||||
$dbh->sql_to_days('issuedate') . " >= " . $maxtokenage);
|
||||
$dbh->bz_unlock_tables();
|
||||
}
|
||||
|
||||
sub GenerateUniqueToken {
|
||||
# Generates a unique random token. Uses generate_random_password
|
||||
# for the tokens themselves and checks uniqueness by searching for
|
||||
# the token in the "tokens" table. Gives up if it can't come up
|
||||
# with a token after about one hundred tries.
|
||||
|
||||
my ($table, $column) = @_;
|
||||
|
||||
my $token;
|
||||
my $duplicate = 1;
|
||||
my $tries = 0;
|
||||
$table ||= "tokens";
|
||||
$column ||= "token";
|
||||
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $sth = $dbh->prepare("SELECT userid FROM $table WHERE $column = ?");
|
||||
|
||||
while ($duplicate) {
|
||||
++$tries;
|
||||
if ($tries > 100) {
|
||||
ThrowCodeError("token_generation_error");
|
||||
}
|
||||
$token = generate_random_password();
|
||||
$sth->execute($token);
|
||||
$duplicate = $sth->fetchrow_array;
|
||||
}
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
sub Cancel {
|
||||
# Cancels a previously issued token and notifies the system administrator.
|
||||
# This should only happen when the user accidentally makes a token request
|
||||
# or when a malicious hacker makes a token request on behalf of a user.
|
||||
|
||||
my ($token, $cancelaction, $vars) = @_;
|
||||
|
||||
my $dbh = Bugzilla->dbh;
|
||||
$vars ||= {};
|
||||
|
||||
# Quote the token for inclusion in SQL statements.
|
||||
my $quotedtoken = &::SqlQuote($token);
|
||||
|
||||
# Get information about the token being cancelled.
|
||||
&::SendSQL("SELECT " . $dbh->sql_date_format('issuedate') . ",
|
||||
tokentype , eventdata , login_name , realname
|
||||
FROM tokens, profiles
|
||||
WHERE tokens.userid = profiles.userid
|
||||
AND token = $quotedtoken");
|
||||
my ($issuedate, $tokentype, $eventdata, $loginname, $realname) = &::FetchSQLData();
|
||||
|
||||
# Get the email address of the Bugzilla maintainer.
|
||||
my $maintainer = Param('maintainer');
|
||||
|
||||
my $template = Bugzilla->template;
|
||||
|
||||
$vars->{'emailaddress'} = $loginname . Param('emailsuffix');
|
||||
$vars->{'maintainer'} = $maintainer;
|
||||
$vars->{'remoteaddress'} = $::ENV{'REMOTE_ADDR'};
|
||||
$vars->{'token'} = $token;
|
||||
$vars->{'tokentype'} = $tokentype;
|
||||
$vars->{'issuedate'} = $issuedate;
|
||||
$vars->{'eventdata'} = $eventdata;
|
||||
$vars->{'cancelaction'} = $cancelaction;
|
||||
|
||||
# Notify the user via email about the cancellation.
|
||||
|
||||
my $message;
|
||||
$template->process("account/cancel-token.txt.tmpl", $vars, \$message)
|
||||
|| ThrowTemplateError($template->error());
|
||||
|
||||
Bugzilla::BugMail::MessageToMTA($message);
|
||||
|
||||
# Delete the token from the database.
|
||||
delete_token($token);
|
||||
}
|
||||
|
||||
sub DeletePasswordTokens {
|
||||
my ($userid, $reason) = @_;
|
||||
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $sth = $dbh->prepare("SELECT token " .
|
||||
"FROM tokens " .
|
||||
"WHERE userid=? AND tokentype='password'");
|
||||
$sth->execute($userid);
|
||||
while (my $token = $sth->fetchrow_array) {
|
||||
Bugzilla::Token::Cancel($token, $reason);
|
||||
}
|
||||
}
|
||||
|
||||
sub HasEmailChangeToken {
|
||||
# Returns an email change token if the user has one.
|
||||
|
||||
my ($userid) = @_;
|
||||
|
||||
my $dbh = Bugzilla->dbh;
|
||||
&::SendSQL("SELECT token FROM tokens WHERE userid = $userid " .
|
||||
"AND (tokentype = 'emailnew' OR tokentype = 'emailold') " .
|
||||
$dbh->sql_limit(1));
|
||||
my ($token) = &::FetchSQLData();
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
sub GetTokenData {
|
||||
# Returns the userid, issuedate and eventdata for the specified token
|
||||
|
||||
my ($token) = @_;
|
||||
return unless defined $token;
|
||||
$token = clean_text($token);
|
||||
trick_taint($token);
|
||||
|
||||
my $dbh = Bugzilla->dbh;
|
||||
return $dbh->selectrow_array(
|
||||
"SELECT userid, " . $dbh->sql_date_format('issuedate') . ", eventdata
|
||||
FROM tokens
|
||||
WHERE token = ?", undef, $token);
|
||||
}
|
||||
|
||||
sub delete_token {
|
||||
# Deletes specified token
|
||||
|
||||
my ($token) = @_;
|
||||
return unless defined $token;
|
||||
trick_taint($token);
|
||||
|
||||
my $dbh = Bugzilla->dbh;
|
||||
$dbh->bz_lock_tables('tokens WRITE');
|
||||
$dbh->do("DELETE FROM tokens WHERE token = ?", undef, $token);
|
||||
$dbh->bz_unlock_tables();
|
||||
}
|
||||
|
||||
# Given a token, makes sure it comes from the currently logged in user
|
||||
# and match the expected event. Returns 1 on success, else displays a warning.
|
||||
# Note: this routine must not be called while tables are locked as it will try
|
||||
# to lock some tables itself, see CleanTokenTable().
|
||||
sub check_token_data {
|
||||
my ($token, $expected_action) = @_;
|
||||
my $user = Bugzilla->user;
|
||||
my $template = Bugzilla->template;
|
||||
my $cgi = Bugzilla->cgi;
|
||||
|
||||
my ($creator_id, $date, $token_action) = GetTokenData($token);
|
||||
unless ($creator_id
|
||||
&& $creator_id == $user->id
|
||||
&& $token_action eq $expected_action)
|
||||
{
|
||||
# Something is going wrong. Ask confirmation before processing.
|
||||
# It is possible that someone tried to trick an administrator.
|
||||
# In this case, we want to know his name!
|
||||
require Bugzilla::User;
|
||||
|
||||
my $vars = {};
|
||||
$vars->{'abuser'} = Bugzilla::User->new($creator_id)->identity;
|
||||
$vars->{'token_action'} = $token_action;
|
||||
$vars->{'expected_action'} = $expected_action;
|
||||
$vars->{'script_name'} = basename($0);
|
||||
|
||||
# Now is a good time to remove old tokens from the DB.
|
||||
CleanTokenTable();
|
||||
|
||||
# If no token was found, create a valid token for the given action.
|
||||
unless ($creator_id) {
|
||||
$token = issue_session_token($expected_action);
|
||||
$cgi->param('token', $token);
|
||||
}
|
||||
|
||||
print $cgi->header();
|
||||
|
||||
$template->process('admin/confirm-action.html.tmpl', $vars)
|
||||
|| ThrowTemplateError($template->error());
|
||||
exit;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# Internal Functions
|
||||
################################################################################
|
||||
|
||||
sub _create_token {
|
||||
# Generates a unique token and inserts it into the database
|
||||
# Returns the token and the token timestamp
|
||||
my ($userid, $tokentype, $eventdata) = @_;
|
||||
|
||||
detaint_natural($userid);
|
||||
trick_taint($tokentype);
|
||||
trick_taint($eventdata);
|
||||
|
||||
my $dbh = Bugzilla->dbh;
|
||||
$dbh->bz_lock_tables('tokens WRITE');
|
||||
|
||||
my $token = GenerateUniqueToken();
|
||||
|
||||
$dbh->do("INSERT INTO tokens (userid, issuedate, token, tokentype, eventdata)
|
||||
VALUES (?, NOW(), ?, ?, ?)", undef, ($userid, $token, $tokentype, $eventdata));
|
||||
|
||||
$dbh->bz_unlock_tables();
|
||||
|
||||
if (wantarray) {
|
||||
my (undef, $token_ts, undef) = GetTokenData($token);
|
||||
$token_ts = str2time($token_ts);
|
||||
return ($token, $token_ts);
|
||||
} else {
|
||||
return $token;
|
||||
}
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::Token - Provides different routines to manage tokens.
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
use Bugzilla::Token;
|
||||
|
||||
Bugzilla::Token::IssueEmailChangeToken($user_id, $old_email, $new_email);
|
||||
Bugzilla::Token::IssuePasswordToken($login_name);
|
||||
Bugzilla::Token::DeletePasswordTokens($user_id, $reason);
|
||||
Bugzilla::Token::Cancel($token, $cancelaction, $vars);
|
||||
|
||||
Bugzilla::Token::CleanTokenTable();
|
||||
|
||||
my $token = issue_session_token($event);
|
||||
check_token_data($token, $event)
|
||||
delete_token($token);
|
||||
|
||||
my $token = Bugzilla::Token::GenerateUniqueToken($table, $column);
|
||||
my $token = Bugzilla::Token::HasEmailChangeToken($user_id);
|
||||
my ($token, $date, $data) = Bugzilla::Token::GetTokenData($token);
|
||||
|
||||
=head1 SUBROUTINES
|
||||
|
||||
=over
|
||||
|
||||
=item C<sub IssueEmailChangeToken($user_id, $old_email, $new_email)>
|
||||
|
||||
Description: Sends two distinct tokens per email to the old and new email
|
||||
addresses to confirm the email address change for the given
|
||||
user ID. These tokens remain valid for the next MAX_TOKEN_AGE days.
|
||||
|
||||
Params: $user_id - The user ID of the user account requesting a new
|
||||
email address.
|
||||
$old_email - The current (old) email address of the user.
|
||||
$new_email - The new email address of the user.
|
||||
|
||||
Returns: Nothing.
|
||||
|
||||
=item C<IssuePasswordToken($login_name)>
|
||||
|
||||
Description: Sends a token per email to the given login name. This token
|
||||
can be used to change the password (e.g. in case the user
|
||||
cannot remember his password and wishes to enter a new one).
|
||||
|
||||
Params: $login_name - The login name of the user requesting a new password.
|
||||
|
||||
Returns: Nothing. It throws an error if the same user made the same
|
||||
request in the last few minutes.
|
||||
|
||||
=item C<CleanTokenTable()>
|
||||
|
||||
Description: Removes all tokens older than MAX_TOKEN_AGE days from the DB.
|
||||
This means that these tokens will now be considered as invalid.
|
||||
|
||||
Params: None.
|
||||
|
||||
Returns: Nothing.
|
||||
|
||||
=item C<GenerateUniqueToken($table, $column)>
|
||||
|
||||
Description: Generates and returns a unique token. This token is unique
|
||||
in the $column of the $table. This token is NOT stored in the DB.
|
||||
|
||||
Params: $table (optional): The table to look at (default: tokens).
|
||||
$column (optional): The column to look at for uniqueness (default: token).
|
||||
|
||||
Returns: A token which is unique in $column.
|
||||
|
||||
=item C<Cancel($token, $cancelaction, $vars)>
|
||||
|
||||
Description: Invalidates an existing token, generally when the token is used
|
||||
for an action which is not the one expected. An email is sent
|
||||
to the user who originally requested this token to inform him
|
||||
that this token has been invalidated (e.g. because an hacker
|
||||
tried to use this token for some malicious action).
|
||||
|
||||
Params: $token: The token to invalidate.
|
||||
$cancelaction: The reason why this token is invalidated.
|
||||
$vars: Some additional information about this action.
|
||||
|
||||
Returns: Nothing.
|
||||
|
||||
=item C<DeletePasswordTokens($user_id, $reason)>
|
||||
|
||||
Description: Cancels all password tokens for the given user. Emails are sent
|
||||
to the user to inform him about this action.
|
||||
|
||||
Params: $user_id: The user ID of the user account whose password tokens
|
||||
are cancelled.
|
||||
$reason: The reason why these tokens are cancelled.
|
||||
|
||||
Returns: Nothing.
|
||||
|
||||
=item C<HasEmailChangeToken($user_id)>
|
||||
|
||||
Description: Returns any existing token currently used for an email change
|
||||
for the given user.
|
||||
|
||||
Params: $user_id - A user ID.
|
||||
|
||||
Returns: A token if it exists, else undef.
|
||||
|
||||
=item C<GetTokenData($token)>
|
||||
|
||||
Description: Returns all stored data for the given token.
|
||||
|
||||
Params: $token - A valid token.
|
||||
|
||||
Returns: The user ID, the date and time when the token was created and
|
||||
the (event)data stored with that token.
|
||||
|
||||
=back
|
||||
|
||||
=head2 Security related routines
|
||||
|
||||
The following routines have been written to be used together as described below,
|
||||
although they can be used separately.
|
||||
|
||||
=over
|
||||
|
||||
=item C<issue_session_token($event)>
|
||||
|
||||
Description: Creates and returns a token used internally.
|
||||
|
||||
Params: $event - The event which needs to be stored in the DB for future
|
||||
reference/checks.
|
||||
|
||||
Returns: A unique token.
|
||||
|
||||
=item C<check_token_data($token, $event)>
|
||||
|
||||
Description: Makes sure the $token has been created by the currently logged in
|
||||
user and to be used for the given $event. If this token is used for
|
||||
an unexpected action (i.e. $event doesn't match the information stored
|
||||
with the token), a warning is displayed asking whether the user really
|
||||
wants to continue. On success, it returns 1.
|
||||
This is the routine to use for security checks, combined with
|
||||
issue_session_token() and delete_token() as follows:
|
||||
|
||||
1. First, create a token for some coming action.
|
||||
my $token = issue_session_token($action);
|
||||
2. Some time later, it's time to make sure that the expected action
|
||||
is going to be executed, and by the expected user.
|
||||
check_token_data($token, $action);
|
||||
3. The check has been done and we no longer need this token.
|
||||
delete_token($token);
|
||||
|
||||
Params: $token - The token used for security checks.
|
||||
$event - The expected event to be run.
|
||||
|
||||
Returns: 1 on success, else a warning is thrown.
|
||||
|
||||
=item C<delete_token($token)>
|
||||
|
||||
Description: Deletes the specified token. No notification is sent.
|
||||
|
||||
Params: $token - The token to delete.
|
||||
|
||||
Returns: Nothing.
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,396 +0,0 @@
|
||||
# -*- 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): Shane H. W. Travis <travis@sedsystems.ca>
|
||||
# Max Kanat-Alexander <mkanat@bugzilla.org>
|
||||
#
|
||||
|
||||
|
||||
package Bugzilla::User::Setting;
|
||||
|
||||
use strict;
|
||||
use base qw(Exporter);
|
||||
|
||||
# Module stuff
|
||||
@Bugzilla::User::Setting::EXPORT = qw(get_all_settings get_defaults
|
||||
add_setting);
|
||||
|
||||
use Bugzilla::Error;
|
||||
use Bugzilla::Util qw{trick_taint};
|
||||
|
||||
###############################
|
||||
### Module Initialization ###
|
||||
###############################
|
||||
|
||||
sub new {
|
||||
my $invocant = shift;
|
||||
my $setting_name = shift;
|
||||
my $user_id = shift;
|
||||
|
||||
my $class = ref($invocant) || $invocant;
|
||||
|
||||
# Create a ref to an empty hash and bless it
|
||||
my $self = {};
|
||||
bless($self, $class);
|
||||
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
# Confirm that the $setting_name is properly formed;
|
||||
# if not, throw a code error.
|
||||
#
|
||||
# NOTE: due to the way that setting names are used in templates,
|
||||
# they must conform to to the limitations set for HTML NAMEs and IDs.
|
||||
#
|
||||
if ( !($setting_name =~ /^[a-zA-Z][-.:\w]*$/) ) {
|
||||
ThrowCodeError("setting_name_invalid", { name => $setting_name });
|
||||
}
|
||||
|
||||
# If there were only two parameters passed in, then we need
|
||||
# to retrieve the information for this setting ourselves.
|
||||
if (scalar @_ == 0) {
|
||||
|
||||
my ($default, $is_enabled, $value) =
|
||||
$dbh->selectrow_array(
|
||||
q{SELECT default_value, is_enabled, setting_value
|
||||
FROM setting
|
||||
LEFT JOIN profile_setting
|
||||
ON setting.name = profile_setting.setting_name
|
||||
WHERE name = ?
|
||||
AND profile_setting.user_id = ?},
|
||||
undef,
|
||||
$setting_name, $user_id);
|
||||
|
||||
# if not defined, then grab the default value
|
||||
if (! defined $value) {
|
||||
($default, $is_enabled) =
|
||||
$dbh->selectrow_array(
|
||||
q{SELECT default_value, is_enabled
|
||||
FROM setting
|
||||
WHERE name = ?},
|
||||
undef,
|
||||
$setting_name);
|
||||
}
|
||||
|
||||
$self->{'is_enabled'} = $is_enabled;
|
||||
$self->{'default_value'} = $default;
|
||||
|
||||
# IF the setting is enabled, AND the user has chosen a setting
|
||||
# THEN return that value
|
||||
# ELSE return the site default, and note that it is the default.
|
||||
if ( ($is_enabled) && (defined $value) ) {
|
||||
$self->{'value'} = $value;
|
||||
} else {
|
||||
$self->{'value'} = $default;
|
||||
$self->{'isdefault'} = 1;
|
||||
}
|
||||
}
|
||||
else {
|
||||
# If the values were passed in, simply assign them and return.
|
||||
$self->{'is_enabled'} = shift;
|
||||
$self->{'default_value'} = shift;
|
||||
$self->{'value'} = shift;
|
||||
$self->{'is_default'} = shift;
|
||||
}
|
||||
|
||||
$self->{'_setting_name'} = $setting_name;
|
||||
$self->{'_user_id'} = $user_id;
|
||||
|
||||
return $self;
|
||||
}
|
||||
|
||||
###############################
|
||||
### Subroutine Definitions ###
|
||||
###############################
|
||||
|
||||
sub add_setting {
|
||||
my ($name, $values, $default_value) = @_;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
return if _setting_exists($name);
|
||||
|
||||
($name && $values && $default_value)
|
||||
|| ThrowCodeError("setting_info_invalid");
|
||||
|
||||
print "Adding a new user setting called '$name'\n";
|
||||
$dbh->do(q{INSERT INTO setting (name, default_value, is_enabled)
|
||||
VALUES (?, ?, 1)},
|
||||
undef, ($name, $default_value));
|
||||
|
||||
my $sth = $dbh->prepare(q{INSERT INTO setting_value (name, value, sortindex)
|
||||
VALUES (?, ?, ?)});
|
||||
|
||||
my @values_list = keys %{$values};
|
||||
foreach my $key (@values_list){
|
||||
$sth->execute($name, $key, $values->{$key});
|
||||
}
|
||||
}
|
||||
|
||||
sub get_all_settings {
|
||||
my ($user_id) = @_;
|
||||
my $settings = get_defaults($user_id); # first get the defaults
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
my $sth = $dbh->prepare(
|
||||
q{SELECT name, default_value, is_enabled, setting_value
|
||||
FROM setting
|
||||
LEFT JOIN profile_setting
|
||||
ON setting.name = profile_setting.setting_name
|
||||
WHERE profile_setting.user_id = ?
|
||||
ORDER BY name});
|
||||
|
||||
$sth->execute($user_id);
|
||||
while (my ($name, $default_value, $is_enabled, $value)
|
||||
= $sth->fetchrow_array()) {
|
||||
|
||||
my $is_default;
|
||||
|
||||
if ( ($is_enabled) && (defined $value) ) {
|
||||
$is_default = 0;
|
||||
} else {
|
||||
$value = $default_value;
|
||||
$is_default = 1;
|
||||
}
|
||||
|
||||
$settings->{$name} = new Bugzilla::User::Setting(
|
||||
$name, $user_id, $is_enabled,
|
||||
$default_value, $value, $is_default);
|
||||
}
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
sub get_defaults {
|
||||
my ($user_id) = @_;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $default_settings = {};
|
||||
|
||||
$user_id ||= 0;
|
||||
|
||||
my $sth = $dbh->prepare(q{SELECT name, default_value, is_enabled
|
||||
FROM setting
|
||||
ORDER BY name});
|
||||
$sth->execute();
|
||||
while (my ($name, $default_value, $is_enabled) = $sth->fetchrow_array()) {
|
||||
|
||||
$default_settings->{$name} = new Bugzilla::User::Setting(
|
||||
$name, $user_id, $is_enabled, $default_value, $default_value, 1);
|
||||
}
|
||||
|
||||
return $default_settings;
|
||||
}
|
||||
|
||||
sub set_default {
|
||||
my ($setting_name, $default_value, $is_enabled) = @_;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
my $sth = $dbh->prepare(q{UPDATE setting
|
||||
SET default_value = ?, is_enabled = ?
|
||||
WHERE name = ?});
|
||||
$sth->execute($default_value, $is_enabled, $setting_name);
|
||||
}
|
||||
|
||||
sub _setting_exists {
|
||||
my ($setting_name) = @_;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $sth = $dbh->prepare("SELECT name FROM setting WHERE name = ?");
|
||||
$sth->execute($setting_name);
|
||||
return ($sth->rows) ? 1 : 0;
|
||||
}
|
||||
|
||||
|
||||
sub legal_values {
|
||||
my ($self) = @_;
|
||||
|
||||
return $self->{'legal_values'} if defined $self->{'legal_values'};
|
||||
|
||||
my $dbh = Bugzilla->dbh;
|
||||
$self->{'legal_values'} = $dbh->selectcol_arrayref(
|
||||
q{SELECT value
|
||||
FROM setting_value
|
||||
WHERE name = ?
|
||||
ORDER BY sortindex},
|
||||
undef, $self->{'_setting_name'});
|
||||
|
||||
return $self->{'legal_values'};
|
||||
}
|
||||
|
||||
sub validate_value {
|
||||
my $self = shift;
|
||||
|
||||
if (grep(/^$_[0]$/, @{$self->legal_values()})) {
|
||||
trick_taint($_[0]);
|
||||
}
|
||||
else {
|
||||
ThrowCodeError('setting_value_invalid',
|
||||
{'name' => $self->{'_setting_name'},
|
||||
'value' => $_[0]});
|
||||
}
|
||||
}
|
||||
|
||||
sub reset_to_default {
|
||||
my ($self) = @_;
|
||||
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $sth = $dbh->do(q{ DELETE
|
||||
FROM profile_setting
|
||||
WHERE setting_name = ?
|
||||
AND user_id = ?},
|
||||
undef, $self->{'_setting_name'}, $self->{'_user_id'});
|
||||
$self->{'value'} = $self->{'default_value'};
|
||||
$self->{'is_default'} = 1;
|
||||
}
|
||||
|
||||
sub set {
|
||||
my ($self, $value) = @_;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $query;
|
||||
|
||||
if ($self->{'is_default'}) {
|
||||
$query = q{INSERT INTO profile_setting
|
||||
(setting_value, setting_name, user_id)
|
||||
VALUES (?,?,?)};
|
||||
} else {
|
||||
$query = q{UPDATE profile_setting
|
||||
SET setting_value = ?
|
||||
WHERE setting_name = ?
|
||||
AND user_id = ?};
|
||||
}
|
||||
$dbh->do($query, undef, $value, $self->{'_setting_name'}, $self->{'_user_id'});
|
||||
|
||||
$self->{'value'} = $value;
|
||||
$self->{'is_default'} = 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::User::Setting - Object for a user preference setting
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
Setting.pm creates a setting object, which is a hash containing the user
|
||||
preference information for a single preference for a single user. These
|
||||
are usually accessed through the "settings" object of a user, and not
|
||||
directly.
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
use Bugzilla::User::Setting;
|
||||
my $settings;
|
||||
|
||||
$settings->{$setting_name} = new Bugzilla::User::Setting(
|
||||
$setting_name, $user_id);
|
||||
|
||||
OR
|
||||
|
||||
$settings->{$setting_name} = new Bugzilla::User::Setting(
|
||||
$setting_name, $user_id, $is_enabled,
|
||||
$default_value, $value, $is_default);
|
||||
|
||||
=head1 CLASS FUNCTIONS
|
||||
|
||||
=over 4
|
||||
|
||||
=item C<add_setting($name, $values, $default_value)>
|
||||
|
||||
Description: Checks for the existence of a setting, and adds it
|
||||
to the database if it does not yet exist.
|
||||
Params: C<$name> - string - the name of the new setting
|
||||
C<$values> - hash - contains the new values (key) and
|
||||
sortindexes for the new setting
|
||||
C<$default_value> - string - the site default
|
||||
Returns: a pointer to a hash of settings
|
||||
#
|
||||
#
|
||||
=item C<get_all_settings($user_id)>
|
||||
|
||||
Description: Provides the user's choices for each setting in the
|
||||
system; if the user has made no choice, uses the site
|
||||
default instead.
|
||||
Params: C<$user_id> - integer - the user id.
|
||||
Returns: a pointer to a hash of settings
|
||||
|
||||
=item C<get_defaults($user_id)>
|
||||
|
||||
Description: When a user is not logged in, they must use the site
|
||||
defaults for every settings; this subroutine provides them.
|
||||
Params: C<$user_id> (optional) - integer - the user id. Note that
|
||||
this optional parameter is mainly for internal use only.
|
||||
Returns: A pointer to a hash of settings. If $user_id was passed, set
|
||||
the user_id value for each setting.
|
||||
|
||||
=item C<set_default($setting_name, $default_value, $is_enabled)>
|
||||
|
||||
Description: Sets the global default for a given setting. Also sets
|
||||
whether users are allowed to choose their own value for
|
||||
this setting, or if they must use the global default.
|
||||
Params: C<$setting_name> - string - the name of the setting
|
||||
C<$default_value> - string - the new default value for this setting
|
||||
C<$is_enabled> - boolean - if false, all users must use the global default
|
||||
Returns: nothing
|
||||
|
||||
=begin private
|
||||
|
||||
=item C<_setting_exists>
|
||||
|
||||
Description: Determines if a given setting exists in the database.
|
||||
Params: C<$setting_name> - string - the setting name
|
||||
Returns: boolean - true if the setting already exists in the DB.
|
||||
|
||||
=back
|
||||
|
||||
=end private
|
||||
|
||||
=head1 METHODS
|
||||
|
||||
=over 4
|
||||
|
||||
=item C<legal_values($setting_name)>
|
||||
|
||||
Description: Returns all legal values for this setting
|
||||
Params: none
|
||||
Returns: A reference to an array containing all legal values
|
||||
|
||||
=item C<validate_value>
|
||||
|
||||
Description: Determines whether a value is valid for the setting
|
||||
by checking against the list of legal values.
|
||||
Untaints the parameter if the value is indeed valid,
|
||||
and throws a setting_value_invalid code error if not.
|
||||
Params: An lvalue containing a candidate for a setting value
|
||||
Returns: nothing
|
||||
|
||||
=item C<reset_to_default>
|
||||
|
||||
Description: If a user chooses to use the global default for a given
|
||||
setting, their saved entry is removed from the database via
|
||||
this subroutine.
|
||||
Params: none
|
||||
Returns: nothing
|
||||
|
||||
=item C<set($value)>
|
||||
|
||||
Description: If a user chooses to use their own value rather than the
|
||||
global value for a given setting, OR changes their value for
|
||||
a given setting, this subroutine is called to insert or
|
||||
update the database as appropriate.
|
||||
Params: C<$value> - string - the new value for this setting for this user.
|
||||
Returns: nothing
|
||||
|
||||
=back
|
||||
@@ -1,849 +0,0 @@
|
||||
# -*- 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>
|
||||
# Dan Mosedale <dmose@mozilla.org>
|
||||
# Jacob Steenhagen <jake@bugzilla.org>
|
||||
# Bradley Baetz <bbaetz@student.usyd.edu.au>
|
||||
# Christopher Aillon <christopher@aillon.com>
|
||||
# Max Kanat-Alexander <mkanat@bugzilla.org>
|
||||
# Frédéric Buclin <LpSolit@gmail.com>
|
||||
|
||||
package Bugzilla::Util;
|
||||
|
||||
use strict;
|
||||
|
||||
use base qw(Exporter);
|
||||
@Bugzilla::Util::EXPORT = qw(is_tainted trick_taint detaint_natural
|
||||
detaint_signed
|
||||
html_quote url_quote value_quote xml_quote
|
||||
css_class_quote html_light_quote
|
||||
i_am_cgi correct_urlbase
|
||||
lsearch max min
|
||||
diff_arrays diff_strings
|
||||
trim wrap_comment find_wrap_point
|
||||
perform_substs
|
||||
format_time format_time_decimal validate_date
|
||||
file_mod_time is_7bit_clean
|
||||
bz_crypt generate_random_password
|
||||
validate_email_syntax clean_text);
|
||||
|
||||
use Bugzilla::Config;
|
||||
use Bugzilla::Constants;
|
||||
|
||||
use Date::Parse;
|
||||
use Date::Format;
|
||||
use Text::Wrap;
|
||||
|
||||
# This is from the perlsec page, slightly modified to remove a warning
|
||||
# From that page:
|
||||
# This function makes use of the fact that the presence of
|
||||
# tainted data anywhere within an expression renders the
|
||||
# entire expression tainted.
|
||||
# Don't ask me how it works...
|
||||
sub is_tainted {
|
||||
return not eval { my $foo = join('',@_), kill 0; 1; };
|
||||
}
|
||||
|
||||
sub trick_taint {
|
||||
require Carp;
|
||||
Carp::confess("Undef to trick_taint") unless defined $_[0];
|
||||
my ($match) = $_[0] =~ /^(.*)$/s;
|
||||
$_[0] = $match;
|
||||
return (defined($_[0]));
|
||||
}
|
||||
|
||||
sub detaint_natural {
|
||||
my ($match) = $_[0] =~ /^(\d+)$/;
|
||||
$_[0] = $match;
|
||||
return (defined($_[0]));
|
||||
}
|
||||
|
||||
sub detaint_signed {
|
||||
my ($match) = $_[0] =~ /^([-+]?\d+)$/;
|
||||
$_[0] = $match;
|
||||
# Remove any leading plus sign.
|
||||
if (defined($_[0]) && $_[0] =~ /^\+(\d+)$/) {
|
||||
$_[0] = $1;
|
||||
}
|
||||
return (defined($_[0]));
|
||||
}
|
||||
|
||||
sub html_quote {
|
||||
my ($var) = (@_);
|
||||
$var =~ s/\&/\&/g;
|
||||
$var =~ s/</\</g;
|
||||
$var =~ s/>/\>/g;
|
||||
$var =~ s/\"/\"/g;
|
||||
return $var;
|
||||
}
|
||||
|
||||
sub html_light_quote {
|
||||
my ($text) = @_;
|
||||
|
||||
# List of allowed HTML elements having no attributes.
|
||||
my @allow = qw(b strong em i u p br abbr acronym ins del cite code var
|
||||
dfn samp kbd big small sub sup tt dd dt dl ul li ol);
|
||||
|
||||
# Are HTML::Scrubber and HTML::Parser installed?
|
||||
eval { require HTML::Scrubber;
|
||||
require HTML::Parser;
|
||||
};
|
||||
|
||||
# We need utf8_mode() from HTML::Parser 3.40 if running Perl >= 5.8.
|
||||
if ($@ || ($] >= 5.008 && $HTML::Parser::VERSION < 3.40)) { # Package(s) not installed.
|
||||
my $safe = join('|', @allow);
|
||||
my $chr = chr(1);
|
||||
|
||||
# First, escape safe elements.
|
||||
$text =~ s#<($safe)>#$chr$1$chr#go;
|
||||
$text =~ s#</($safe)>#$chr/$1$chr#go;
|
||||
# Now filter < and >.
|
||||
$text =~ s#<#<#g;
|
||||
$text =~ s#>#>#g;
|
||||
# Restore safe elements.
|
||||
$text =~ s#$chr/($safe)$chr#</$1>#go;
|
||||
$text =~ s#$chr($safe)$chr#<$1>#go;
|
||||
return $text;
|
||||
}
|
||||
else { # Packages installed.
|
||||
# We can be less restrictive. We can accept elements with attributes.
|
||||
push(@allow, qw(a blockquote q span));
|
||||
|
||||
# Allowed protocols.
|
||||
my $safe_protocols = join('|', SAFE_PROTOCOLS);
|
||||
my $protocol_regexp = qr{(^(?:$safe_protocols):|^[^:]+$)}i;
|
||||
|
||||
# Deny all elements and attributes unless explicitly authorized.
|
||||
my @default = (0 => {
|
||||
id => 1,
|
||||
name => 1,
|
||||
class => 1,
|
||||
'*' => 0, # Reject all other attributes.
|
||||
}
|
||||
);
|
||||
|
||||
# Specific rules for allowed elements. If no specific rule is set
|
||||
# for a given element, then the default is used.
|
||||
my @rules = (a => {
|
||||
href => $protocol_regexp,
|
||||
title => 1,
|
||||
id => 1,
|
||||
name => 1,
|
||||
class => 1,
|
||||
'*' => 0, # Reject all other attributes.
|
||||
},
|
||||
blockquote => {
|
||||
cite => $protocol_regexp,
|
||||
id => 1,
|
||||
name => 1,
|
||||
class => 1,
|
||||
'*' => 0, # Reject all other attributes.
|
||||
},
|
||||
'q' => {
|
||||
cite => $protocol_regexp,
|
||||
id => 1,
|
||||
name => 1,
|
||||
class => 1,
|
||||
'*' => 0, # Reject all other attributes.
|
||||
},
|
||||
);
|
||||
|
||||
my $scrubber = HTML::Scrubber->new(default => \@default,
|
||||
allow => \@allow,
|
||||
rules => \@rules,
|
||||
comment => 0,
|
||||
process => 0);
|
||||
|
||||
# Avoid filling the web server error log with Perl 5.8.x.
|
||||
# In HTML::Scrubber 0.08, the HTML::Parser object is stored in
|
||||
# the "_p" key, but this may change in future versions.
|
||||
if ($] >= 5.008 && ref($scrubber->{_p}) eq 'HTML::Parser') {
|
||||
$scrubber->{_p}->utf8_mode(1);
|
||||
}
|
||||
return $scrubber->scrub($text);
|
||||
}
|
||||
}
|
||||
|
||||
# This originally came from CGI.pm, by Lincoln D. Stein
|
||||
sub url_quote {
|
||||
my ($toencode) = (@_);
|
||||
$toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("%%%02x",ord($1))/eg;
|
||||
return $toencode;
|
||||
}
|
||||
|
||||
sub css_class_quote {
|
||||
my ($toencode) = (@_);
|
||||
$toencode =~ s/ /_/g;
|
||||
$toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("&#x%x;",ord($1))/eg;
|
||||
return $toencode;
|
||||
}
|
||||
|
||||
sub value_quote {
|
||||
my ($var) = (@_);
|
||||
$var =~ s/\&/\&/g;
|
||||
$var =~ s/</\</g;
|
||||
$var =~ s/>/\>/g;
|
||||
$var =~ s/\"/\"/g;
|
||||
# See bug http://bugzilla.mozilla.org/show_bug.cgi?id=4928 for
|
||||
# explanation of why Bugzilla does this linebreak substitution.
|
||||
# This caused form submission problems in mozilla (bug 22983, 32000).
|
||||
$var =~ s/\r\n/\
/g;
|
||||
$var =~ s/\n\r/\
/g;
|
||||
$var =~ s/\r/\
/g;
|
||||
$var =~ s/\n/\
/g;
|
||||
return $var;
|
||||
}
|
||||
|
||||
sub xml_quote {
|
||||
my ($var) = (@_);
|
||||
$var =~ s/\&/\&/g;
|
||||
$var =~ s/</\</g;
|
||||
$var =~ s/>/\>/g;
|
||||
$var =~ s/\"/\"/g;
|
||||
$var =~ s/\'/\'/g;
|
||||
return $var;
|
||||
}
|
||||
|
||||
sub url_decode {
|
||||
my ($todecode) = (@_);
|
||||
$todecode =~ tr/+/ /; # pluses become spaces
|
||||
$todecode =~ s/%([0-9a-fA-F]{2})/pack("c",hex($1))/ge;
|
||||
return $todecode;
|
||||
}
|
||||
|
||||
sub i_am_cgi {
|
||||
# I use SERVER_SOFTWARE because it's required to be
|
||||
# defined for all requests in the CGI spec.
|
||||
return exists $ENV{'SERVER_SOFTWARE'} ? 1 : 0;
|
||||
}
|
||||
|
||||
sub correct_urlbase {
|
||||
return Param('urlbase') if Param('ssl') eq 'never';
|
||||
|
||||
if (Param('sslbase')) {
|
||||
return Param('sslbase') if Param('ssl') eq 'always';
|
||||
# Authenticated Sessions
|
||||
return Param('sslbase') if Bugzilla->user->id;
|
||||
}
|
||||
|
||||
# Set to "authenticated sessions" but nobody's logged in, or
|
||||
# sslbase isn't set.
|
||||
return Param('urlbase');
|
||||
}
|
||||
|
||||
sub lsearch {
|
||||
my ($list,$item) = (@_);
|
||||
my $count = 0;
|
||||
foreach my $i (@$list) {
|
||||
if ($i eq $item) {
|
||||
return $count;
|
||||
}
|
||||
$count++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
sub max {
|
||||
my $max = shift(@_);
|
||||
foreach my $val (@_) {
|
||||
$max = $val if $val > $max;
|
||||
}
|
||||
return $max;
|
||||
}
|
||||
|
||||
sub min {
|
||||
my $min = shift(@_);
|
||||
foreach my $val (@_) {
|
||||
$min = $val if $val < $min;
|
||||
}
|
||||
return $min;
|
||||
}
|
||||
|
||||
sub diff_arrays {
|
||||
my ($old_ref, $new_ref) = @_;
|
||||
|
||||
my @old = @$old_ref;
|
||||
my @new = @$new_ref;
|
||||
|
||||
# For each pair of (old, new) entries:
|
||||
# If they're equal, set them to empty. When done, @old contains entries
|
||||
# that were removed; @new contains ones that got added.
|
||||
foreach my $oldv (@old) {
|
||||
foreach my $newv (@new) {
|
||||
next if ($newv eq '');
|
||||
if ($oldv eq $newv) {
|
||||
$newv = $oldv = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
my @removed = grep { $_ ne '' } @old;
|
||||
my @added = grep { $_ ne '' } @new;
|
||||
return (\@removed, \@added);
|
||||
}
|
||||
|
||||
sub trim {
|
||||
my ($str) = @_;
|
||||
if ($str) {
|
||||
$str =~ s/^\s+//g;
|
||||
$str =~ s/\s+$//g;
|
||||
}
|
||||
return $str;
|
||||
}
|
||||
|
||||
sub diff_strings {
|
||||
my ($oldstr, $newstr) = @_;
|
||||
|
||||
# Split the old and new strings into arrays containing their values.
|
||||
$oldstr =~ s/[\s,]+/ /g;
|
||||
$newstr =~ s/[\s,]+/ /g;
|
||||
my @old = split(" ", $oldstr);
|
||||
my @new = split(" ", $newstr);
|
||||
|
||||
my ($rem, $add) = diff_arrays(\@old, \@new);
|
||||
|
||||
my $removed = join (", ", @$rem);
|
||||
my $added = join (", ", @$add);
|
||||
|
||||
return ($removed, $added);
|
||||
}
|
||||
|
||||
sub wrap_comment {
|
||||
my ($comment) = @_;
|
||||
my $wrappedcomment = "";
|
||||
|
||||
# Use 'local', as recommended by Text::Wrap's perldoc.
|
||||
local $Text::Wrap::columns = COMMENT_COLS;
|
||||
# Make words that are longer than COMMENT_COLS not wrap.
|
||||
local $Text::Wrap::huge = 'overflow';
|
||||
# Don't mess with tabs.
|
||||
local $Text::Wrap::unexpand = 0;
|
||||
|
||||
# If the line starts with ">", don't wrap it. Otherwise, wrap.
|
||||
foreach my $line (split(/\r\n|\r|\n/, $comment)) {
|
||||
if ($line =~ qr/^>/) {
|
||||
$wrappedcomment .= ($line . "\n");
|
||||
}
|
||||
else {
|
||||
$wrappedcomment .= (wrap('', '', $line) . "\n");
|
||||
}
|
||||
}
|
||||
|
||||
return $wrappedcomment;
|
||||
}
|
||||
|
||||
sub find_wrap_point {
|
||||
my ($string, $maxpos) = @_;
|
||||
if (!$string) { return 0 }
|
||||
if (length($string) < $maxpos) { return length($string) }
|
||||
my $wrappoint = rindex($string, ",", $maxpos); # look for comma
|
||||
if ($wrappoint < 0) { # can't find comma
|
||||
$wrappoint = rindex($string, " ", $maxpos); # look for space
|
||||
if ($wrappoint < 0) { # can't find space
|
||||
$wrappoint = rindex($string, "-", $maxpos); # look for hyphen
|
||||
if ($wrappoint < 0) { # can't find hyphen
|
||||
$wrappoint = $maxpos; # just truncate it
|
||||
} else {
|
||||
$wrappoint++; # leave hyphen on the left side
|
||||
}
|
||||
}
|
||||
}
|
||||
return $wrappoint;
|
||||
}
|
||||
|
||||
sub perform_substs {
|
||||
my ($str, $substs) = (@_);
|
||||
$str =~ s/%([a-z]*)%/(defined $substs->{$1} ? $substs->{$1} : Param($1))/eg;
|
||||
return $str;
|
||||
}
|
||||
|
||||
sub format_time {
|
||||
my ($date, $format) = @_;
|
||||
|
||||
# If $format is undefined, try to guess the correct date format.
|
||||
my $show_timezone;
|
||||
if (!defined($format)) {
|
||||
if ($date =~ m/^(\d{4})[-\.](\d{2})[-\.](\d{2}) (\d{2}):(\d{2})(:(\d{2}))?$/) {
|
||||
my $sec = $7;
|
||||
if (defined $sec) {
|
||||
$format = "%Y-%m-%d %T";
|
||||
} else {
|
||||
$format = "%Y-%m-%d %R";
|
||||
}
|
||||
} else {
|
||||
# Default date format. See Date::Format for other formats available.
|
||||
$format = "%Y-%m-%d %R";
|
||||
}
|
||||
# By default, we want the timezone to be displayed.
|
||||
$show_timezone = 1;
|
||||
}
|
||||
else {
|
||||
# Search for %Z or %z, meaning we want the timezone to be displayed.
|
||||
# Till bug 182238 gets fixed, we assume Param('timezone') is used.
|
||||
$show_timezone = ($format =~ s/\s?%Z$//i);
|
||||
}
|
||||
|
||||
# str2time($date) is undefined if $date has an invalid date format.
|
||||
my $time = str2time($date);
|
||||
|
||||
if (defined $time) {
|
||||
$date = time2str($format, $time);
|
||||
$date .= " " . &::Param('timezone') if $show_timezone;
|
||||
}
|
||||
else {
|
||||
# Don't let invalid (time) strings to be passed to templates!
|
||||
$date = '';
|
||||
}
|
||||
return trim($date);
|
||||
}
|
||||
|
||||
sub format_time_decimal {
|
||||
my ($time) = (@_);
|
||||
|
||||
my $newtime = sprintf("%.2f", $time);
|
||||
|
||||
if ($newtime =~ /0\Z/) {
|
||||
$newtime = sprintf("%.1f", $time);
|
||||
}
|
||||
|
||||
return $newtime;
|
||||
}
|
||||
|
||||
sub file_mod_time {
|
||||
my ($filename) = (@_);
|
||||
my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,
|
||||
$atime,$mtime,$ctime,$blksize,$blocks)
|
||||
= stat($filename);
|
||||
return $mtime;
|
||||
}
|
||||
|
||||
sub bz_crypt {
|
||||
my ($password) = @_;
|
||||
|
||||
# The list of characters that can appear in a salt. Salts and hashes
|
||||
# are both encoded as a sequence of characters from a set containing
|
||||
# 64 characters, each one of which represents 6 bits of the salt/hash.
|
||||
# The encoding is similar to BASE64, the difference being that the
|
||||
# BASE64 plus sign (+) is replaced with a forward slash (/).
|
||||
my @saltchars = (0..9, 'A'..'Z', 'a'..'z', '.', '/');
|
||||
|
||||
# Generate the salt. We use an 8 character (48 bit) salt for maximum
|
||||
# security on systems whose crypt uses MD5. Systems with older
|
||||
# versions of crypt will just use the first two characters of the salt.
|
||||
my $salt = '';
|
||||
for ( my $i=0 ; $i < 8 ; ++$i ) {
|
||||
$salt .= $saltchars[rand(64)];
|
||||
}
|
||||
|
||||
# Crypt the password.
|
||||
my $cryptedpassword = crypt($password, $salt);
|
||||
|
||||
# Return the crypted password.
|
||||
return $cryptedpassword;
|
||||
}
|
||||
|
||||
sub generate_random_password {
|
||||
my $size = shift || 10; # default to 10 chars if nothing specified
|
||||
return join("", map{ ('0'..'9','a'..'z','A'..'Z')[rand 62] } (1..$size));
|
||||
}
|
||||
|
||||
sub validate_email_syntax {
|
||||
my ($addr) = @_;
|
||||
my $match = Param('emailregexp');
|
||||
my $ret = ($addr =~ /$match/ && $addr !~ /[\\\(\)<>&,;:"\[\] \t\r\n]/);
|
||||
if ($ret) {
|
||||
# We assume these checks to suffice to consider the address untainted.
|
||||
trick_taint($_[0]);
|
||||
}
|
||||
return $ret ? 1 : 0;
|
||||
}
|
||||
|
||||
sub validate_date {
|
||||
my ($date) = @_;
|
||||
my $date2;
|
||||
|
||||
# $ts is undefined if the parser fails.
|
||||
my $ts = str2time($date);
|
||||
if ($ts) {
|
||||
$date2 = time2str("%Y-%m-%d", $ts);
|
||||
|
||||
$date =~ s/(\d+)-0*(\d+?)-0*(\d+?)/$1-$2-$3/;
|
||||
$date2 =~ s/(\d+)-0*(\d+?)-0*(\d+?)/$1-$2-$3/;
|
||||
}
|
||||
my $ret = ($ts && $date eq $date2);
|
||||
return $ret ? 1 : 0;
|
||||
}
|
||||
|
||||
sub is_7bit_clean {
|
||||
return $_[0] !~ /[^\x20-\x7E\x0A\x0D]/;
|
||||
}
|
||||
|
||||
sub clean_text {
|
||||
my ($dtext) = shift;
|
||||
$dtext =~ s/[\x00-\x1F\x7F]+/ /g; # change control characters into a space
|
||||
return trim($dtext);
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::Util - Generic utility functions for bugzilla
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
use Bugzilla::Util;
|
||||
|
||||
# Functions for dealing with variable tainting
|
||||
$rv = is_tainted($var);
|
||||
trick_taint($var);
|
||||
detaint_natural($var);
|
||||
detaint_signed($var);
|
||||
|
||||
# Functions for quoting
|
||||
html_quote($var);
|
||||
url_quote($var);
|
||||
value_quote($var);
|
||||
xml_quote($var);
|
||||
|
||||
# Functions for decoding
|
||||
$rv = url_decode($var);
|
||||
|
||||
# Functions that tell you about your environment
|
||||
my $is_cgi = i_am_cgi();
|
||||
my $urlbase = correct_urlbase();
|
||||
|
||||
# Functions for searching
|
||||
$loc = lsearch(\@arr, $val);
|
||||
$val = max($a, $b, $c);
|
||||
$val = min($a, $b, $c);
|
||||
|
||||
# Data manipulation
|
||||
($removed, $added) = diff_arrays(\@old, \@new);
|
||||
|
||||
# Functions for manipulating strings
|
||||
$val = trim(" abc ");
|
||||
($removed, $added) = diff_strings($old, $new);
|
||||
$wrapped = wrap_comment($comment);
|
||||
$msg = perform_substs($str, $substs);
|
||||
|
||||
# Functions for formatting time
|
||||
format_time($time);
|
||||
|
||||
# Functions for dealing with files
|
||||
$time = file_mod_time($filename);
|
||||
|
||||
# Cryptographic Functions
|
||||
$crypted_password = bz_crypt($password);
|
||||
$new_password = generate_random_password($password_length);
|
||||
|
||||
# Validation Functions
|
||||
validate_email_syntax($email);
|
||||
validate_date($date);
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
This package contains various utility functions which do not belong anywhere
|
||||
else.
|
||||
|
||||
B<It is not intended as a general dumping group for something which
|
||||
people feel might be useful somewhere, someday>. Do not add methods to this
|
||||
package unless it is intended to be used for a significant number of files,
|
||||
and it does not belong anywhere else.
|
||||
|
||||
=head1 FUNCTIONS
|
||||
|
||||
This package provides several types of routines:
|
||||
|
||||
=head2 Tainting
|
||||
|
||||
Several functions are available to deal with tainted variables. B<Use these
|
||||
with care> to avoid security holes.
|
||||
|
||||
=over 4
|
||||
|
||||
=item C<is_tainted>
|
||||
|
||||
Determines whether a particular variable is tainted
|
||||
|
||||
=item C<trick_taint($val)>
|
||||
|
||||
Tricks perl into untainting a particular variable.
|
||||
|
||||
Use trick_taint() when you know that there is no way that the data
|
||||
in a scalar can be tainted, but taint mode still bails on it.
|
||||
|
||||
B<WARNING!! Using this routine on data that really could be tainted defeats
|
||||
the purpose of taint mode. It should only be used on variables that have been
|
||||
sanity checked in some way and have been determined to be OK.>
|
||||
|
||||
=item C<detaint_natural($num)>
|
||||
|
||||
This routine detaints a natural number. It returns a true value if the
|
||||
value passed in was a valid natural number, else it returns false. You
|
||||
B<MUST> check the result of this routine to avoid security holes.
|
||||
|
||||
=item C<detaint_signed($num)>
|
||||
|
||||
This routine detaints a signed integer. It returns a true value if the
|
||||
value passed in was a valid signed integer, else it returns false. You
|
||||
B<MUST> check the result of this routine to avoid security holes.
|
||||
|
||||
=back
|
||||
|
||||
=head2 Quoting
|
||||
|
||||
Some values may need to be quoted from perl. However, this should in general
|
||||
be done in the template where possible.
|
||||
|
||||
=over 4
|
||||
|
||||
=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.
|
||||
|
||||
=item C<html_light_quote($val)>
|
||||
|
||||
Returns a string where only explicitly allowed HTML elements and attributes
|
||||
are kept. All HTML elements and attributes not being in the whitelist are either
|
||||
escaped (if HTML::Scrubber is not installed) or removed.
|
||||
|
||||
=item C<url_quote($val)>
|
||||
|
||||
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.
|
||||
|
||||
=item C<value_quote($val)>
|
||||
|
||||
As well as escaping html like C<html_quote>, this routine converts newlines
|
||||
into 
, suitable for use in html attributes.
|
||||
|
||||
=item C<xml_quote($val)>
|
||||
|
||||
This is similar to C<html_quote>, except that ' is escaped to '. This
|
||||
is kept separate from html_quote partly for compatibility with previous code
|
||||
(for ') and partly for future handling of non-ASCII characters.
|
||||
|
||||
=item C<url_decode($val)>
|
||||
|
||||
Converts the %xx encoding from the given URL back to its original form.
|
||||
|
||||
=item C<i_am_cgi()>
|
||||
|
||||
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<correct_urlbase()>
|
||||
|
||||
Returns either the C<sslbase> or C<urlbase> parameter, depending on the
|
||||
current setting for the C<ssl> parameter.
|
||||
|
||||
=back
|
||||
|
||||
=head2 Searching
|
||||
|
||||
Functions for searching within a set of values.
|
||||
|
||||
=over 4
|
||||
|
||||
=item C<lsearch($list, $item)>
|
||||
|
||||
Returns the position of C<$item> in C<$list>. C<$list> must be a list
|
||||
reference.
|
||||
|
||||
If the item is not in the list, returns -1.
|
||||
|
||||
=item C<max($a, $b, ...)>
|
||||
|
||||
Returns the maximum from a set of values.
|
||||
|
||||
=item C<min($a, $b, ...)>
|
||||
|
||||
Returns the minimum from a set of values.
|
||||
|
||||
=back
|
||||
|
||||
=head2 Data Manipulation
|
||||
|
||||
=over 4
|
||||
|
||||
=item C<diff_arrays(\@old, \@new)>
|
||||
|
||||
Description: Takes two arrayrefs, and will tell you what it takes to
|
||||
get from @old to @new.
|
||||
Params: @old = array that you are changing from
|
||||
@new = array that you are changing to
|
||||
Returns: A list of two arrayrefs. The first is a reference to an
|
||||
array containing items that were removed from @old. The
|
||||
second is a reference to an array containing items
|
||||
that were added to @old. If both returned arrays are
|
||||
empty, @old and @new contain the same values.
|
||||
|
||||
=back
|
||||
|
||||
=head2 String Manipulation
|
||||
|
||||
=over 4
|
||||
|
||||
=item C<trim($str)>
|
||||
|
||||
Removes any leading or trailing whitespace from a string. This routine does not
|
||||
modify the existing string.
|
||||
|
||||
=item C<diff_strings($oldstr, $newstr)>
|
||||
|
||||
Takes two strings containing a list of comma- or space-separated items
|
||||
and returns what items were removed from or added to the new one,
|
||||
compared to the old one. Returns a list, where the first entry is a scalar
|
||||
containing removed items, and the second entry is a scalar containing added
|
||||
items.
|
||||
|
||||
=item C<wrap_comment($comment)>
|
||||
|
||||
Takes a bug comment, and wraps it to the appropriate length. The length is
|
||||
currently specified in C<Bugzilla::Constants::COMMENT_COLS>. Lines beginning
|
||||
with ">" are assumed to be quotes, and they will not be wrapped.
|
||||
|
||||
The intended use of this function is to wrap comments that are about to be
|
||||
displayed or emailed. Generally, wrapped text should not be stored in the
|
||||
database.
|
||||
|
||||
=item C<find_wrap_point($string, $maxpos)>
|
||||
|
||||
Search for a comma, a whitespace or a hyphen to split $string, within the first
|
||||
$maxpos characters. If none of them is found, just split $string at $maxpos.
|
||||
The search starts at $maxpos and goes back to the beginning of the string.
|
||||
|
||||
=item C<perform_substs($str, $substs)>
|
||||
|
||||
Performs substitutions for sending out email with variables in it,
|
||||
or for inserting a parameter into some other string.
|
||||
|
||||
Takes a string and a reference to a hash containing substitution
|
||||
variables and their values.
|
||||
|
||||
If the hash is not specified, or if we need to substitute something
|
||||
that's not in the hash, then we will use parameters to do the
|
||||
substitution instead.
|
||||
|
||||
Substitutions are always enclosed with '%' symbols. So they look like:
|
||||
%some_variable_name%. If "some_variable_name" is a key in the hash, then
|
||||
its value will be placed into the string. If it's not a key in the hash,
|
||||
then the value of the parameter called "some_variable_name" will be placed
|
||||
into the string.
|
||||
|
||||
=item C<is_7bit_clean($str)>
|
||||
|
||||
Returns true is the string contains only 7-bit characters (ASCII 32 through 126,
|
||||
ASCII 10 (LineFeed) and ASCII 13 (Carrage Return).
|
||||
|
||||
=item C<clean_text($str)>
|
||||
Returns the parameter "cleaned" by exchanging non-printable characters with spaces.
|
||||
Specifically characters (ASCII 0 through 31) and (ASCII 127) will become ASCII 32 (Space).
|
||||
|
||||
=back
|
||||
|
||||
=head2 Formatting Time
|
||||
|
||||
=over 4
|
||||
|
||||
=item C<format_time($time)>
|
||||
|
||||
Takes a time, converts it to the desired format and appends the timezone
|
||||
as defined in editparams.cgi, if desired. This routine will be expanded
|
||||
in the future to adjust for user preferences regarding what timezone to
|
||||
display times in.
|
||||
|
||||
This routine is mainly called from templates to filter dates, see
|
||||
"FILTER time" in Templates.pm. In this case, $format is undefined and
|
||||
the routine has to "guess" the date format that was passed to $dbh->sql_date_format().
|
||||
|
||||
|
||||
=item C<format_time_decimal($time)>
|
||||
|
||||
Returns a number with 2 digit precision, unless the last digit is a 0. Then it
|
||||
returns only 1 digit precision.
|
||||
|
||||
=back
|
||||
|
||||
|
||||
=head2 Files
|
||||
|
||||
=over 4
|
||||
|
||||
=item C<file_mod_time($filename)>
|
||||
|
||||
Takes a filename and returns the modification time. It returns it in the format
|
||||
of the "mtime" parameter of the perl "stat" function.
|
||||
|
||||
=back
|
||||
|
||||
=head2 Cryptography
|
||||
|
||||
=over 4
|
||||
|
||||
=item C<bz_crypt($password)>
|
||||
|
||||
Takes a string and returns a C<crypt>ed value for it, using a random salt.
|
||||
|
||||
Please always use this function instead of the built-in perl "crypt"
|
||||
when initially encrypting a password.
|
||||
|
||||
=begin undocumented
|
||||
|
||||
Random salts are generated because the alternative is usually
|
||||
to use the first two characters of the password itself, and since
|
||||
the salt appears in plaintext at the beginning of the encrypted
|
||||
password string this has the effect of revealing the first two
|
||||
characters of the password to anyone who views the encrypted version.
|
||||
|
||||
=end undocumented
|
||||
|
||||
=item C<generate_random_password($password_length)>
|
||||
|
||||
Returns an alphanumeric string with the specified length
|
||||
(10 characters by default). Use this function to generate passwords
|
||||
and tokens.
|
||||
|
||||
=back
|
||||
|
||||
=head2 Validation
|
||||
|
||||
=over 4
|
||||
|
||||
=item C<validate_email_syntax($email)>
|
||||
|
||||
Do a syntax checking for a legal email address and returns 1 if
|
||||
the check is successful, else returns 0.
|
||||
|
||||
=item C<validate_date($date)>
|
||||
|
||||
Make sure the date has the correct format and returns 1 if
|
||||
the check is successful, else returns 0.
|
||||
|
||||
=back
|
||||
@@ -1,177 +0,0 @@
|
||||
# -*- 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): Tiago R. Mello <timello@async.com.br>
|
||||
|
||||
use strict;
|
||||
|
||||
package Bugzilla::Version;
|
||||
|
||||
use Bugzilla::Util;
|
||||
use Bugzilla::Error;
|
||||
|
||||
################################
|
||||
##### Initialization #####
|
||||
################################
|
||||
|
||||
use constant DEFAULT_VERSION => 'unspecified';
|
||||
|
||||
use constant DB_COLUMNS => qw(
|
||||
versions.value
|
||||
versions.product_id
|
||||
);
|
||||
|
||||
our $columns = join(", ", DB_COLUMNS);
|
||||
|
||||
sub new {
|
||||
my $invocant = shift;
|
||||
my $class = ref($invocant) || $invocant;
|
||||
my $self = {};
|
||||
bless($self, $class);
|
||||
return $self->_init(@_);
|
||||
}
|
||||
|
||||
sub _init {
|
||||
my $self = shift;
|
||||
my ($product_id, $value) = (@_);
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
my $version;
|
||||
|
||||
if (defined $product_id
|
||||
&& detaint_natural($product_id)
|
||||
&& defined $value) {
|
||||
|
||||
trick_taint($value);
|
||||
$version = $dbh->selectrow_hashref(qq{
|
||||
SELECT $columns FROM versions
|
||||
WHERE value = ?
|
||||
AND product_id = ?}, undef, ($value, $product_id));
|
||||
} else {
|
||||
ThrowCodeError('bad_arg',
|
||||
{argument => 'product_id/value',
|
||||
function => 'Bugzilla::Version::_init'});
|
||||
}
|
||||
|
||||
return undef unless (defined $version);
|
||||
|
||||
foreach my $field (keys %$version) {
|
||||
$self->{$field} = $version->{$field};
|
||||
}
|
||||
return $self;
|
||||
}
|
||||
|
||||
sub bug_count {
|
||||
my $self = shift;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
if (!defined $self->{'bug_count'}) {
|
||||
$self->{'bug_count'} = $dbh->selectrow_array(qq{
|
||||
SELECT COUNT(*) FROM bugs
|
||||
WHERE product_id = ? AND version = ?}, undef,
|
||||
($self->product_id, $self->name)) || 0;
|
||||
}
|
||||
return $self->{'bug_count'};
|
||||
}
|
||||
|
||||
###############################
|
||||
##### Accessors ####
|
||||
###############################
|
||||
|
||||
sub name { return $_[0]->{'value'}; }
|
||||
sub product_id { return $_[0]->{'product_id'}; }
|
||||
|
||||
###############################
|
||||
##### Subroutines ###
|
||||
###############################
|
||||
|
||||
sub check_version {
|
||||
my ($product, $version_name) = @_;
|
||||
|
||||
$version_name || ThrowUserError('version_not_specified');
|
||||
my $version = new Bugzilla::Version($product->id, $version_name);
|
||||
unless ($version) {
|
||||
ThrowUserError('version_not_valid',
|
||||
{'product' => $product->name,
|
||||
'version' => $version_name});
|
||||
}
|
||||
return $version;
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::Version - Bugzilla product version class.
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
use Bugzilla::Version;
|
||||
|
||||
my $version = new Bugzilla::Version(1, 'version_value');
|
||||
|
||||
my $product_id = $version->product_id;
|
||||
my $value = $version->value;
|
||||
|
||||
my $version = $hash_ref->{'version_value'};
|
||||
|
||||
my $version = Bugzilla::Version::check_version($product_obj,
|
||||
'acme_version');
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
Version.pm represents a Product Version object.
|
||||
|
||||
=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.
|
||||
|
||||
Params: none.
|
||||
|
||||
Returns: Integer with the number of bugs.
|
||||
|
||||
=back
|
||||
|
||||
=head1 SUBROUTINES
|
||||
|
||||
=over
|
||||
|
||||
=item C<check_version($product, $version_name)>
|
||||
|
||||
Description: Checks if the version name exists for the product name.
|
||||
|
||||
Params: $product - A Bugzilla::Product object.
|
||||
$version_name - String with a version name.
|
||||
|
||||
Returns: Bugzilla::Version object.
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
@@ -1,84 +0,0 @@
|
||||
Bugzilla Quick Start Guide
|
||||
==========================
|
||||
(or, how to get Bugzilla up and running in 10 steps)
|
||||
Christian Reis <kiko@async.com.br>
|
||||
|
||||
This express installation guide is for "normal" Bugzilla installations,
|
||||
which means a Linux or Unix system on which Apache, Perl, MySQL or PostgreSQL
|
||||
and a Sendmail compatible MTA are available. For other configurations, please
|
||||
see Section 4 of the Bugzilla Guide in the docs/ directory.
|
||||
|
||||
1. Decide from which URL and directory under your webserver root you
|
||||
will be serving the Bugzilla webpages.
|
||||
|
||||
2. Unpack the distribution into the chosen directory (there is no copying or
|
||||
installation involved).
|
||||
|
||||
3. Run ./checksetup.pl, look for unsolved requirements, and install them.
|
||||
You can run checksetup as many times as necessary to check if
|
||||
everything required has been installed.
|
||||
|
||||
These will usually include assorted Perl modules, MySQL or PostgreSQL,
|
||||
and a MTA.
|
||||
|
||||
After a successful dependency check, checksetup should complain that
|
||||
localconfig needs to be edited.
|
||||
|
||||
4. Edit the localconfig file, in particular the $webservergroup and
|
||||
$db_* variables. In particular, $db_name and $db_user will define
|
||||
your database setup in step 5.
|
||||
|
||||
5. Using the name you provided as $db_name above, create a MySQL database
|
||||
for Bugzilla. You should also create a user permission for the name
|
||||
supplied as $db_user with read/write access to that database.
|
||||
|
||||
If you are not familiar with MySQL permissions, it's a good idea to
|
||||
use the mysql_setpermission script that is installed with the MySQL
|
||||
distribution, and be sure to read Bugzilla Security - MySQL section
|
||||
in the Bugzilla Guide or PostgreSQL documentation.
|
||||
|
||||
6. Run checksetup.pl once more; if all goes well, it should set up the
|
||||
Bugzilla database for you. If not, return to step 5.
|
||||
|
||||
checksetup.pl should ask you, this time, for the administrator's
|
||||
email address and password. These will be used for the initial
|
||||
Bugzilla administrator account.
|
||||
|
||||
7. Configure Apache (or install and configure, if you don't have it up
|
||||
yet) to point to the Bugzilla directory. You should enable and
|
||||
activate mod_cgi, and add the configuration entries
|
||||
|
||||
Options +ExecCGI
|
||||
AllowOverride Limit
|
||||
DirectoryIndex index.cgi
|
||||
|
||||
to your Bugzilla <Directory> block. You may also need
|
||||
|
||||
AddHandler cgi-script .cgi
|
||||
|
||||
if you don't have that in your Apache configuration file yet.
|
||||
|
||||
8. Visit the URL you chose for Bugzilla. Your browser should display the
|
||||
default Bugzilla home page. You should then log in as the
|
||||
administrator by following the "Log in" link and supplying the
|
||||
account information you provided in step 6.
|
||||
|
||||
9. Scroll to the bottom of the page after logging in, and select
|
||||
"Parameters". Set up the relevant parameters for your local setup.
|
||||
|
||||
See section 4.2 of the Bugzilla Guide for a in-depth description of
|
||||
some of the configuration parameters available.
|
||||
|
||||
10. That's it. If anything unexpected comes up:
|
||||
|
||||
- read the error message carefully,
|
||||
- backtrack through the steps above,
|
||||
- check the official installation guide, which is section 4 in the
|
||||
Bugzilla Guide, included in the docs/ directory in various
|
||||
formats.
|
||||
|
||||
Support and installation questions should be directed to the
|
||||
mozilla-webtools@mozilla.org mailing list -- don't write to the
|
||||
developer mailing list: your post *will* be ignored if you do.
|
||||
|
||||
Further support information is at http://www.bugzilla.org/support/
|
||||
@@ -1,19 +0,0 @@
|
||||
* This README is no longer used to house installation instructions. Instead,
|
||||
it contains pointers to where you may find the information you need.
|
||||
|
||||
* A quick installation guide is provided in the QUICKSTART file.
|
||||
|
||||
* Complete installation instructions are found in docs/, with a
|
||||
variety of document types available. Please refer to these documents
|
||||
when installing, configuring, and maintaining your Bugzilla
|
||||
installation. A helpful starting point is docs/txt/Bugzilla-Guide.txt,
|
||||
or with a web browser at docs/html/index.html.
|
||||
|
||||
* Release notes for people upgrading to a new version of Bugzilla are
|
||||
available at docs/rel_notes.txt.
|
||||
|
||||
* If you wish to contribute to the documentation, please read docs/README.docs.
|
||||
|
||||
* The Bugzilla web site is at "http://www.bugzilla.org/". This site will
|
||||
contain the latest Bugzilla information, including how to report bugs and how
|
||||
to get help with Bugzilla.
|
||||
@@ -1,3 +0,0 @@
|
||||
Please consult The Bugzilla Guide for instructions on how to upgrade
|
||||
Bugzilla from an older version. The Guide can be found with this
|
||||
distribution, in docs/html, docs/txt, and docs/sgml.
|
||||
@@ -1,412 +0,0 @@
|
||||
This file contains only important changes made to Bugzilla before release
|
||||
2.8. If you are upgrading from version older than 2.8, please read this file.
|
||||
If you are upgrading from 2.8 or newer, please read the Installation and
|
||||
Upgrade instructions in The Bugzilla Guide, found with this distribution in
|
||||
docs/html, docs/txt, and docs/sgml.
|
||||
|
||||
Please note that the period in our version numbers is a place separator, not
|
||||
a decimal point. The 14 in version 2.14 is newer than the 8 in 2.8, for
|
||||
example. You should only be using this file if you have a single digit
|
||||
after the period in the version 2.x Bugzilla you are upgrading from.
|
||||
|
||||
For a complete list of what changes, use Bonsai
|
||||
(http://cvs-mirror.mozilla.org/webtools/bonsai/cvsqueryform.cgi) to
|
||||
query the CVS tree. For example,
|
||||
|
||||
http://cvs-mirror.mozilla.org/webtools/bonsai/cvsquery.cgi?module=all&branch=HEAD&branchtype=match&dir=mozilla%2Fwebtools%2Fbugzilla&file=&filetype=match&who=&whotype=match&sortby=Date&hours=2&date=week&mindate=&maxdate=&cvsroot=%2Fcvsroot
|
||||
|
||||
will tell you what has been changed in the last week.
|
||||
|
||||
|
||||
10/12/99 The CHANGES file is now obsolete! There is a new file called
|
||||
checksetup.pl. You should get in the habit of running that file every time
|
||||
you update your installation of Bugzilla. That file will be constantly
|
||||
updated to automatically update your installation to match any code changes.
|
||||
If you're curious as to what is going on, changes are commented in that file,
|
||||
at the end.
|
||||
|
||||
Many thanks to Holger Schurig <holgerschurig@nikocity.de> for writing this
|
||||
script!
|
||||
|
||||
|
||||
|
||||
10/11/99 Restructured voting database to add a cached value in each
|
||||
bug recording how many total votes that bug has. While I'm at it, I
|
||||
removed the unused "area" field from the bugs database. It is
|
||||
distressing to realize that the bugs table has reached the maximum
|
||||
number of indices allowed by MySQL (16), which may make future
|
||||
enhancements awkward.
|
||||
|
||||
You must feed the following to MySQL:
|
||||
|
||||
alter table bugs drop column area;
|
||||
alter table bugs add column votes mediumint not null, add index (votes);
|
||||
|
||||
You then *must* delete the data/versioncache file when you make this
|
||||
change, as it contains references to the "area" field. Deleting it is safe,
|
||||
bugzilla will correctly regenerate it.
|
||||
|
||||
If you have been using the voting feature at all, then you will then
|
||||
need to update the voting cache. You can do this by visiting the
|
||||
sanitycheck.cgi page, and taking it up on its offer to rebuild the
|
||||
votes stuff.
|
||||
|
||||
|
||||
10/7/99 Added voting ability. You must run the new script
|
||||
"makevotestable.sh". You must also feed the following to mysql:
|
||||
|
||||
alter table products add column votesperuser smallint not null;
|
||||
|
||||
|
||||
|
||||
9/15/99 Apparently, newer alphas of MySQL won't allow you to have
|
||||
"when" as a column name. So, I have had to rename a column in the
|
||||
bugs_activity table. You must feed the below to mysql or you won't
|
||||
work at all.
|
||||
|
||||
alter table bugs_activity change column when bug_when datetime not null;
|
||||
|
||||
|
||||
8/16/99 Added "OpenVMS" to the list of OS's. Feed this to mysql:
|
||||
|
||||
alter table bugs change column op_sys op_sys enum("All", "Windows 3.1", "Windows 95", "Windows 98", "Windows NT", "Mac System 7", "Mac System 7.5", "Mac System 7.6.1", "Mac System 8.0", "Mac System 8.5", "Mac System 8.6", "AIX", "BSDI", "HP-UX", "IRIX", "Linux", "FreeBSD", "OSF/1", "Solaris", "SunOS", "Neutrino", "OS/2", "BeOS", "OpenVMS", "other") not null;
|
||||
|
||||
6/22/99 Added an entry to the attachments table to record who the submitter
|
||||
was. Nothing uses this yet, but it still should be recorded.
|
||||
|
||||
alter table attachments add column submitter_id mediumint not null;
|
||||
|
||||
You should also run this script to populate the new field:
|
||||
|
||||
#!/usr/bin/perl -w
|
||||
use diagnostics;
|
||||
use strict;
|
||||
require "globals.pl";
|
||||
$|=1;
|
||||
ConnectToDatabase();
|
||||
SendSQL("select bug_id, attach_id from attachments order by bug_id");
|
||||
my @list;
|
||||
while (MoreSQLData()) {
|
||||
my @row = FetchSQLData();
|
||||
push(@list, \@row);
|
||||
}
|
||||
foreach my $ref (@list) {
|
||||
my ($bug, $attach) = (@$ref);
|
||||
SendSQL("select long_desc from bugs where bug_id = $bug");
|
||||
my $comment = FetchOneColumn() . "Created an attachment (id=$attach)";
|
||||
|
||||
if ($comment =~ m@-* Additional Comments From ([^ ]*)[- 0-9/:]*\nCreated an attachment \(id=$attach\)@) {
|
||||
print "Found $1\n";
|
||||
SendSQL("select userid from profiles where login_name=" .
|
||||
SqlQuote($1));
|
||||
my $userid = FetchOneColumn();
|
||||
if (defined $userid && $userid > 0) {
|
||||
SendSQL("update attachments set submitter_id=$userid where attach_id = $attach");
|
||||
}
|
||||
} else {
|
||||
print "Bug $bug can't find comment for attachment $attach\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
6/14/99 Added "BeOS" to the list of OS's. Feed this to mysql:
|
||||
|
||||
alter table bugs change column op_sys op_sys enum("All", "Windows 3.1", "Windows 95", "Windows 98", "Windows NT", "Mac System 7", "Mac System 7.5", "Mac System 7.6.1", "Mac System 8.0", "Mac System 8.5", "Mac System 8.6", "AIX", "BSDI", "HP-UX", "IRIX", "Linux", "FreeBSD", "OSF/1", "Solaris", "SunOS", "Neutrino", "OS/2", "BeOS", "other") not null;
|
||||
|
||||
|
||||
5/27/99 Added support for dependency information. You must run the new
|
||||
"makedependenciestable.sh" script. You can turn off dependencies with the new
|
||||
"usedependencies" param, but it defaults to being on. Also, read very
|
||||
carefully the description for the new "webdotbase" param; you will almost
|
||||
certainly need to tweak it.
|
||||
|
||||
|
||||
5/24/99 Added "Mac System 8.6" and "Neutrino" to the list of OS's.
|
||||
Feed this to mysql:
|
||||
|
||||
alter table bugs change column op_sys op_sys enum("All", "Windows 3.1", "Windows 95", "Windows 98", "Windows NT", "Mac System 7", "Mac System 7.5", "Mac System 7.6.1", "Mac System 8.0", "Mac System 8.5", "Mac System 8.6", "AIX", "BSDI", "HP-UX", "IRIX", "Linux", "FreeBSD", "OSF/1", "Solaris", "SunOS", "Neutrino", "OS/2", "other") not null;
|
||||
|
||||
|
||||
5/12/99 Added a pref to control how much email you get. This needs a new
|
||||
column in the profiles table, so feed the following to mysql:
|
||||
|
||||
alter table profiles add column emailnotification enum("ExcludeSelfChanges", "CConly", "All") not null default "ExcludeSelfChanges";
|
||||
|
||||
5/5/99 Added the ability to search by creation date. To make this perform
|
||||
well, you ought to do the following:
|
||||
|
||||
alter table bugs change column creation_ts creation_ts datetime not null, add index (creation_ts);
|
||||
|
||||
|
||||
4/30/99 Added a new severity, "blocker". To get this into your running
|
||||
Bugzilla, do the following:
|
||||
|
||||
alter table bugs change column bug_severity bug_severity enum("blocker", "critical", "major", "normal", "minor", "trivial", "enhancement") not null;
|
||||
|
||||
|
||||
4/22/99 There was a bug where the long descriptions of bugs had a variety of
|
||||
newline characters at the end, depending on the operating system of the browser
|
||||
that submitted the text. This bug has been fixed, so that no further changes
|
||||
like that will happen. But to fix problems that have already crept into your
|
||||
database, you can run the following perl script (which is slow and ugly, but
|
||||
does work:)
|
||||
#!/usr/bin/perl -w
|
||||
use diagnostics;
|
||||
use strict;
|
||||
require "globals.pl";
|
||||
$|=1;
|
||||
ConnectToDatabase();
|
||||
SendSQL("select bug_id from bugs order by bug_id");
|
||||
my @list;
|
||||
while (MoreSQLData()) {
|
||||
push(@list, FetchOneColumn());
|
||||
}
|
||||
foreach my $id (@list) {
|
||||
if ($id % 50 == 0) {
|
||||
print "\n$id ";
|
||||
}
|
||||
SendSQL("select long_desc from bugs where bug_id = $id");
|
||||
my $comment = FetchOneColumn();
|
||||
my $orig = $comment;
|
||||
$comment =~ s/\r\n/\n/g; # Get rid of windows-style line endings.
|
||||
$comment =~ s/\r/\n/g; # Get rid of mac-style line endings.
|
||||
if ($comment ne $orig) {
|
||||
SendSQL("update bugs set long_desc = " . SqlQuote($comment) .
|
||||
" where bug_id = $id");
|
||||
print ".";
|
||||
} else {
|
||||
print "-";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
4/8/99 Added ability to store patches with bugs. This requires a new table
|
||||
to store the data, so you will need to run the "makeattachmenttable.sh" script.
|
||||
|
||||
3/25/99 Unfortunately, the HTML::FromText CPAN module had too many bugs, and
|
||||
so I had to roll my own. We no longer use the HTML::FromText CPAN module.
|
||||
|
||||
3/24/99 (This entry has been removed. It used to say that we required the
|
||||
HTML::FromText CPAN module, but that's no longer true.)
|
||||
|
||||
3/22/99 Added the ability to query by fields which have changed within a date
|
||||
range. To make this perform a bit better, we need a new index:
|
||||
|
||||
alter table bugs_activity add index (field);
|
||||
|
||||
3/10/99 Added 'groups' stuff, where we have different group bits that we can
|
||||
put on a person or on a bug. Some of the group bits control access to bugzilla
|
||||
features. And a person can't access a bug unless he has every group bit set
|
||||
that is also set on the bug. See the comments in makegroupstable.sh for a bit
|
||||
more info.
|
||||
|
||||
The 'maintainer' param is now used only as an email address for people to send
|
||||
complaints to. The groups table is what is now used to determine permissions.
|
||||
|
||||
You will need to run the new script "makegroupstable.sh". And then you need to
|
||||
feed the following lines to MySQL (replace XXX with the login name of the
|
||||
maintainer, the person you wish to be all-powerful).
|
||||
|
||||
alter table bugs add column groupset bigint not null;
|
||||
alter table profiles add column groupset bigint not null;
|
||||
update profiles set groupset=0x7fffffffffffffff where login_name = XXX;
|
||||
|
||||
|
||||
|
||||
3/8/99 Added params to control how priorities are set in a new bug. You can
|
||||
now choose whether to let submitters of new bugs choose a priority, or whether
|
||||
they should just accept the default priority (which is now no longer hardcoded
|
||||
to "P2", but is instead a param.) The default value of the params will cause
|
||||
the same behavior as before.
|
||||
|
||||
3/3/99 Added a "disallownew" field to the products table. If non-zero, then
|
||||
don't let people file new bugs against this product. (This is for when a
|
||||
product is retired, but you want to keep the bug reports around for posterity.)
|
||||
Feed this to MySQL:
|
||||
|
||||
alter table products add column disallownew tinyint not null;
|
||||
|
||||
|
||||
2/8/99 Added FreeBSD to the list of OS's. Feed this to MySQL:
|
||||
|
||||
alter table bugs change column op_sys op_sys enum("All", "Windows 3.1", "Windows 95", "Windows 98", "Windows NT", "Mac System 7", "Mac System 7.5", "Mac System 7.6.1", "Mac System 8.0", "Mac System 8.5", "AIX", "BSDI", "HP-UX", "IRIX", "Linux", "FreeBSD", "OSF/1", "Solaris", "SunOS", "OS/2", "other") not null;
|
||||
|
||||
|
||||
2/4/99 Added a new column "description" to the components table, and added
|
||||
links to a new page which will use this to describe the components of a
|
||||
given product. Feed this to MySQL:
|
||||
|
||||
alter table components add column description mediumtext not null;
|
||||
|
||||
|
||||
2/3/99 Added a new column "initialqacontact" to the components table that gives
|
||||
an initial QA contact field. It may be empty if you wish the initial qa
|
||||
contact to be empty. If you're not using the QA contact field, you don't need
|
||||
to add this column, but you might as well be safe and add it anyway:
|
||||
|
||||
alter table components add column initialqacontact tinytext not null;
|
||||
|
||||
|
||||
2/2/99 Added a new column "milestoneurl" to the products table that gives a URL
|
||||
which is to describe the currently defined milestones for a product. If you
|
||||
don't use target milestone, you might be able to get away without adding this
|
||||
column, but you might as well be safe and add it anyway:
|
||||
|
||||
alter table products add column milestoneurl tinytext not null;
|
||||
|
||||
|
||||
1/29/99 Whoops; had a misspelled op_sys. It was "Mac System 7.1.6"; it should
|
||||
be "Mac System 7.6.1". It turns out I had no bugs with this value set, so I
|
||||
could just do the below simple command. If you have bugs with this value, you
|
||||
may need to do something more complicated.
|
||||
|
||||
alter table bugs change column op_sys op_sys enum("All", "Windows 3.1", "Windows 95", "Windows 98", "Windows NT", "Mac System 7", "Mac System 7.5", "Mac System 7.6.1", "Mac System 8.0", "Mac System 8.5", "AIX", "BSDI", "HP-UX", "IRIX", "Linux", "OSF/1", "Solaris", "SunOS", "OS/2", "other") not null;
|
||||
|
||||
|
||||
|
||||
1/20/99 Added new fields: Target Milestone, QA Contact, and Status Whiteboard.
|
||||
These fields are all optional in the UI; there are parameters to turn them on.
|
||||
However, whether or not you use them, the fields need to be in the DB. There
|
||||
is some code that needs them, even if you don't.
|
||||
|
||||
To update your DB to have these fields, send the following to MySQL:
|
||||
|
||||
alter table bugs add column target_milestone varchar(20) not null,
|
||||
add column qa_contact mediumint not null,
|
||||
add column status_whiteboard mediumtext not null,
|
||||
add index (target_milestone), add index (qa_contact);
|
||||
|
||||
|
||||
|
||||
1/18/99 You can now query by CC. To make this perform reasonably, the CC table
|
||||
needs some indices. The following MySQL does the necessary stuff:
|
||||
|
||||
alter table cc add index (bug_id), add index (who);
|
||||
|
||||
|
||||
1/15/99 The op_sys field can now be queried by (and more easily tweaked).
|
||||
To make this perform reasonably, it needs an index. The following MySQL
|
||||
command will create the necessary index:
|
||||
|
||||
alter table bugs add index (op_sys);
|
||||
|
||||
|
||||
12/2/98 The op_sys and rep_platform fields have been tweaked. op_sys
|
||||
is now an enum, rather than having the legal values all hard-coded in
|
||||
perl. rep_platform now no longer allows a value of "X-Windows".
|
||||
|
||||
Here's how I ported to the new world. This ought to work for you too.
|
||||
Actually, it's probably overkill. I had a lot of illegal values for op_sys
|
||||
in my tables, from importing bugs from strange places. If you haven't done
|
||||
anything funky, then much of the below will be a no-op.
|
||||
|
||||
First, send the following commands to MySQL to make sure all your values for
|
||||
rep_platform and op_sys are legal in the new world..
|
||||
|
||||
update bugs set rep_platform="Sun" where rep_platform="X-Windows" and op_sys like "Solaris%";
|
||||
update bugs set rep_platform="SGI" where rep_platform="X-Windows" and op_sys = "IRIX";
|
||||
update bugs set rep_platform="SGI" where rep_platform="X-Windows" and op_sys = "HP-UX";
|
||||
update bugs set rep_platform="DEC" where rep_platform="X-Windows" and op_sys = "OSF/1";
|
||||
update bugs set rep_platform="PC" where rep_platform="X-Windows" and op_sys = "Linux";
|
||||
update bugs set rep_platform="other" where rep_platform="X-Windows";
|
||||
update bugs set rep_platform="other" where rep_platform="";
|
||||
update bugs set op_sys="Mac System 7" where op_sys="System 7";
|
||||
update bugs set op_sys="Mac System 7.5" where op_sys="System 7.5";
|
||||
update bugs set op_sys="Mac System 8.0" where op_sys="8.0";
|
||||
update bugs set op_sys="OSF/1" where op_sys="Digital Unix 4.0";
|
||||
update bugs set op_sys="IRIX" where op_sys like "IRIX %";
|
||||
update bugs set op_sys="HP-UX" where op_sys like "HP-UX %";
|
||||
update bugs set op_sys="Windows NT" where op_sys like "NT %";
|
||||
update bugs set op_sys="OSF/1" where op_sys like "OSF/1 %";
|
||||
update bugs set op_sys="Solaris" where op_sys like "Solaris %";
|
||||
update bugs set op_sys="SunOS" where op_sys like "SunOS%";
|
||||
update bugs set op_sys="other" where op_sys = "Motif";
|
||||
update bugs set op_sys="other" where op_sys = "Other";
|
||||
|
||||
Next, send the following commands to make sure you now have only legal
|
||||
entries in your table. If either of the queries do not come up empty, then
|
||||
you have to do more stuff like the above.
|
||||
|
||||
select bug_id,op_sys,rep_platform from bugs where rep_platform not regexp "^(All|DEC|HP|Macintosh|PC|SGI|Sun|X-Windows|Other)$";
|
||||
select bug_id,op_sys,rep_platform from bugs where op_sys not regexp "^(All|Windows 3.1|Windows 95|Windows 98|Windows NT|Mac System 7|Mac System 7.5|Mac System 7.1.6|Mac System 8.0|AIX|BSDI|HP-UX|IRIX|Linux|OSF/1|Solaris|SunOS|other)$";
|
||||
|
||||
Finally, once that's all clear, alter the table to make enforce the new legal
|
||||
entries:
|
||||
|
||||
alter table bugs change column op_sys op_sys enum("All", "Windows 3.1", "Windows 95", "Windows 98", "Windows NT", "Mac System 7", "Mac System 7.5", "Mac System 7.1.6", "Mac System 8.0", "AIX", "BSDI", "HP-UX", "IRIX", "Linux", "OSF/1", "Solaris", "SunOS", "other") not null, change column rep_platform rep_platform enum("All", "DEC", "HP", "Macintosh", "PC", "SGI", "Sun", "Other");
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
11/20/98 Added searching of CC field. To better support this, added
|
||||
some indexes to the CC table. You probably want to execute the following
|
||||
mysql commands:
|
||||
|
||||
alter table cc add index (bug_id);
|
||||
alter table cc add index (who);
|
||||
|
||||
|
||||
10/27/98 security check for legal products in place. bug charts are not
|
||||
available as an option if collectstats.pl has never been run. all products
|
||||
get daily stats collected now. README updated: Chart::Base is listed as
|
||||
a requirement, instructions for using collectstats.pl included as
|
||||
an optional step. also got silly and added optional quips to bug
|
||||
reports.
|
||||
|
||||
10/17/98 modified README installation instructions slightly.
|
||||
|
||||
10/7/98 Added a new table called "products". Right now, this is used
|
||||
only to have a description for each product, and that description is
|
||||
only used when initially adding a new bug. Anyway, you *must* create
|
||||
the new table (which you can do by running the new makeproducttable.sh
|
||||
script). If you just leave it empty, things will work much as they
|
||||
did before, or you can add descriptions for some or all of your
|
||||
products.
|
||||
|
||||
|
||||
9/15/98 Everything has been ported to Perl. NO MORE TCL. This
|
||||
transition should be relatively painless, except for the "params"
|
||||
file. This is the file that contains parameters you've set up on the
|
||||
editparams.cgi page. Before changing to Perl, this was a tcl-syntax
|
||||
file, stored in the same directory as the code; after the change to
|
||||
Perl, it becomes a perl-syntax file, stored in a subdirectory named
|
||||
"data". See the README file for more details on what version of Perl
|
||||
you need.
|
||||
|
||||
So, if updating from an older version of Bugzilla, you will need to
|
||||
edit data/param, change the email address listed for
|
||||
$::param{'maintainer'}, and then go revisit the editparams.cgi page
|
||||
and reset all the parameters to your taste. Fortunately, your old
|
||||
params file will still be around, and so you ought to be able to
|
||||
cut&paste important bits from there.
|
||||
|
||||
Also, note that the "whineatnews" script has changed name (it now has
|
||||
an extension of .pl instead of .tcl), so you'll need to change your
|
||||
cron job.
|
||||
|
||||
And the "comments" file has been moved to the data directory. Just do
|
||||
"cat comments >> data/comments" to restore any old comments that may
|
||||
have been lost.
|
||||
|
||||
|
||||
|
||||
9/2/98 Changed the way password validation works. We now keep a
|
||||
crypt'd version of the password in the database, and check against
|
||||
that. (This is silly, because we're also keeping the plaintext
|
||||
version there, but I have plans...) Stop passing the plaintext
|
||||
password around as a cookie; instead, we have a cookie that references
|
||||
a record in a new database table, logincookies.
|
||||
|
||||
IMPORTANT: if updating from an older version of Bugzilla, you must run
|
||||
the following commands to keep things working:
|
||||
|
||||
./makelogincookiestable.sh
|
||||
echo "alter table profiles add column cryptpassword varchar(64);" | mysql bugs
|
||||
echo "update profiles set cryptpassword = encrypt(password,substring(rand(),3, 4));" | mysql bugs
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,78 +0,0 @@
|
||||
<!ELEMENT bugzilla (bug+)>
|
||||
<!ATTLIST bugzilla
|
||||
version CDATA #REQUIRED
|
||||
urlbase CDATA #REQUIRED
|
||||
maintainer CDATA #REQUIRED
|
||||
exporter CDATA #IMPLIED
|
||||
>
|
||||
<!ELEMENT bug (bug_id, (alias?, creation_ts, short_desc, delta_ts, reporter_accessible, cclist_accessible, classification_id, classification, product, component, version, rep_platform, op_sys, bug_status, resolution?, bug_file_loc?, status_whiteboard?, keywords*, priority, bug_severity, target_milestone?, dependson*, blocked*, votes?, everconfirmed, reporter, assigned_to, qa_contact?, cc*, (estimated_time, remaining_time, actual_time, deadline)?, group*, flag*, long_desc*, attachment*)?)>
|
||||
<!ATTLIST bug
|
||||
error (NotFound | NotPermitted | InvalidBugId) #IMPLIED
|
||||
>
|
||||
<!ELEMENT bug_id (#PCDATA)>
|
||||
<!ELEMENT alias (#PCDATA)>
|
||||
<!ELEMENT reporter_accessible (#PCDATA)>
|
||||
<!ELEMENT cclist_accessible (#PCDATA)>
|
||||
<!ELEMENT exporter (#PCDATA)>
|
||||
<!ELEMENT urlbase (#PCDATA)>
|
||||
<!ELEMENT bug_status (#PCDATA)>
|
||||
<!ELEMENT classification_id (#PCDATA)>
|
||||
<!ELEMENT classification (#PCDATA)>
|
||||
<!ELEMENT product (#PCDATA)>
|
||||
<!ELEMENT priority (#PCDATA)>
|
||||
<!ELEMENT version (#PCDATA)>
|
||||
<!ELEMENT rep_platform (#PCDATA)>
|
||||
<!ELEMENT assigned_to (#PCDATA)>
|
||||
<!ELEMENT delta_ts (#PCDATA)>
|
||||
<!ELEMENT component (#PCDATA)>
|
||||
<!ELEMENT reporter (#PCDATA)>
|
||||
<!ELEMENT target_milestone (#PCDATA)>
|
||||
<!ELEMENT bug_severity (#PCDATA)>
|
||||
<!ELEMENT creation_ts (#PCDATA)>
|
||||
<!ELEMENT qa_contact (#PCDATA)>
|
||||
<!ELEMENT status_whiteboard (#PCDATA)>
|
||||
<!ELEMENT op_sys (#PCDATA)>
|
||||
<!ELEMENT resolution (#PCDATA)>
|
||||
<!ELEMENT bug_file_loc (#PCDATA)>
|
||||
<!ELEMENT short_desc (#PCDATA)>
|
||||
<!ELEMENT keywords (#PCDATA)>
|
||||
<!ELEMENT dependson (#PCDATA)>
|
||||
<!ELEMENT blocked (#PCDATA)>
|
||||
<!ELEMENT votes (#PCDATA)>
|
||||
<!ELEMENT everconfirmed (#PCDATA)>
|
||||
<!ELEMENT cc (#PCDATA)>
|
||||
<!ELEMENT group (#PCDATA)>
|
||||
<!ELEMENT estimated_time (#PCDATA)>
|
||||
<!ELEMENT remaining_time (#PCDATA)>
|
||||
<!ELEMENT actual_time (#PCDATA)>
|
||||
<!ELEMENT deadline (#PCDATA)>
|
||||
<!ELEMENT long_desc (who, bug_when, thetext)>
|
||||
<!ATTLIST long_desc
|
||||
encoding (base64) #IMPLIED
|
||||
isprivate (0|1) #IMPLIED
|
||||
>
|
||||
<!ELEMENT who (#PCDATA)>
|
||||
<!ELEMENT bug_when (#PCDATA)>
|
||||
<!ELEMENT thetext (#PCDATA)>
|
||||
<!ELEMENT attachment (attachid, date, desc, filename?, type?, data?, flag*)>
|
||||
<!ATTLIST attachment
|
||||
isobsolete (0|1) #IMPLIED
|
||||
ispatch (0|1) #IMPLIED
|
||||
isprivate (0|1) #IMPLIED
|
||||
>
|
||||
<!ELEMENT attachid (#PCDATA)>
|
||||
<!ELEMENT date (#PCDATA)>
|
||||
<!ELEMENT desc (#PCDATA)>
|
||||
<!ELEMENT filename (#PCDATA)>
|
||||
<!ELEMENT type (#PCDATA)>
|
||||
<!ELEMENT data (#PCDATA)>
|
||||
<!ATTLIST data
|
||||
encoding (base64) #IMPLIED
|
||||
>
|
||||
<!ELEMENT flag EMPTY>
|
||||
<!ATTLIST flag
|
||||
name CDATA #REQUIRED
|
||||
status CDATA #REQUIRED
|
||||
setter CDATA #IMPLIED
|
||||
requestee CDATA #IMPLIED
|
||||
>
|
||||
@@ -1,319 +0,0 @@
|
||||
#!/usr/bin/perl -wT
|
||||
# -*- 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): Gervase Markham <gerv@gerv.net>
|
||||
# Lance Larsh <lance.larsh@oracle.com>
|
||||
|
||||
# Glossary:
|
||||
# series: An individual, defined set of data plotted over time.
|
||||
# data set: What a series is called in the UI.
|
||||
# line: A set of one or more series, to be summed and drawn as a single
|
||||
# line when the series is plotted.
|
||||
# chart: A set of lines
|
||||
#
|
||||
# So when you select rows in the UI, you are selecting one or more lines, not
|
||||
# series.
|
||||
|
||||
# Generic Charting TODO:
|
||||
#
|
||||
# JS-less chart creation - hard.
|
||||
# Broken image on error or no data - need to do much better.
|
||||
# Centralise permission checking, so UserInGroup('editbugs') not scattered
|
||||
# everywhere.
|
||||
# User documentation :-)
|
||||
#
|
||||
# Bonus:
|
||||
# Offer subscription when you get a "series already exists" error?
|
||||
|
||||
use strict;
|
||||
use lib qw(.);
|
||||
|
||||
require "globals.pl";
|
||||
use Bugzilla;
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::Chart;
|
||||
use Bugzilla::Series;
|
||||
use Bugzilla::User;
|
||||
|
||||
my $cgi = Bugzilla->cgi;
|
||||
my $template = Bugzilla->template;
|
||||
my $vars = {};
|
||||
|
||||
# Go back to query.cgi if we are adding a boolean chart parameter.
|
||||
if (grep(/^cmd-/, $cgi->param())) {
|
||||
my $params = $cgi->canonicalise_query("format", "ctype", "action");
|
||||
print "Location: query.cgi?format=" . $cgi->param('query_format') .
|
||||
($params ? "&$params" : "") . "\n\n";
|
||||
exit;
|
||||
}
|
||||
|
||||
my $action = $cgi->param('action');
|
||||
my $series_id = $cgi->param('series_id');
|
||||
|
||||
# Because some actions are chosen by buttons, we can't encode them as the value
|
||||
# of the action param, because that value is localisation-dependent. So, we
|
||||
# encode it in the name, as "action-<action>". Some params even contain the
|
||||
# series_id they apply to (e.g. subscribe, unsubscribe.)
|
||||
my @actions = grep(/^action-/, $cgi->param());
|
||||
if ($actions[0] && $actions[0] =~ /^action-([^\d]+)(\d*)$/) {
|
||||
$action = $1;
|
||||
$series_id = $2 if $2;
|
||||
}
|
||||
|
||||
$action ||= "assemble";
|
||||
|
||||
# Go to buglist.cgi if we are doing a search.
|
||||
if ($action eq "search") {
|
||||
my $params = $cgi->canonicalise_query("format", "ctype", "action");
|
||||
print "Location: buglist.cgi" . ($params ? "?$params" : "") . "\n\n";
|
||||
exit;
|
||||
}
|
||||
|
||||
my $user = Bugzilla->login(LOGIN_REQUIRED);
|
||||
|
||||
UserInGroup(Param("chartgroup"))
|
||||
|| ThrowUserError("auth_failure", {group => Param("chartgroup"),
|
||||
action => "use",
|
||||
object => "charts"});
|
||||
|
||||
# Only admins may create public queries
|
||||
UserInGroup('admin') || $cgi->delete('public');
|
||||
|
||||
# All these actions relate to chart construction.
|
||||
if ($action =~ /^(assemble|add|remove|sum|subscribe|unsubscribe)$/) {
|
||||
# These two need to be done before the creation of the Chart object, so
|
||||
# that the changes they make will be reflected in it.
|
||||
if ($action =~ /^subscribe|unsubscribe$/) {
|
||||
detaint_natural($series_id) || ThrowCodeError("invalid_series_id");
|
||||
my $series = new Bugzilla::Series($series_id);
|
||||
$series->$action($user->id);
|
||||
}
|
||||
|
||||
my $chart = new Bugzilla::Chart($cgi);
|
||||
|
||||
if ($action =~ /^remove|sum$/) {
|
||||
$chart->$action(getSelectedLines());
|
||||
}
|
||||
elsif ($action eq "add") {
|
||||
my @series_ids = getAndValidateSeriesIDs();
|
||||
$chart->add(@series_ids);
|
||||
}
|
||||
|
||||
view($chart);
|
||||
}
|
||||
elsif ($action eq "plot") {
|
||||
plot();
|
||||
}
|
||||
elsif ($action eq "wrap") {
|
||||
# For CSV "wrap", we go straight to "plot".
|
||||
if ($cgi->param('ctype') && $cgi->param('ctype') eq "csv") {
|
||||
plot();
|
||||
}
|
||||
else {
|
||||
wrap();
|
||||
}
|
||||
}
|
||||
elsif ($action eq "create") {
|
||||
assertCanCreate($cgi);
|
||||
|
||||
my $series = new Bugzilla::Series($cgi);
|
||||
|
||||
if (!$series->existsInDatabase()) {
|
||||
$series->writeToDatabase();
|
||||
$vars->{'message'} = "series_created";
|
||||
}
|
||||
else {
|
||||
ThrowUserError("series_already_exists", {'series' => $series});
|
||||
}
|
||||
|
||||
$vars->{'series'} = $series;
|
||||
|
||||
print $cgi->header();
|
||||
$template->process("global/message.html.tmpl", $vars)
|
||||
|| ThrowTemplateError($template->error());
|
||||
}
|
||||
elsif ($action eq "edit") {
|
||||
detaint_natural($series_id) || ThrowCodeError("invalid_series_id");
|
||||
assertCanEdit($series_id);
|
||||
|
||||
my $series = new Bugzilla::Series($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);
|
||||
|
||||
my $series = new Bugzilla::Series($cgi);
|
||||
|
||||
# We need to check if there is _another_ series in the database with
|
||||
# our (potentially new) name. So we call existsInDatabase() to see if
|
||||
# the return value is us or some other series we need to avoid stomping
|
||||
# on.
|
||||
my $id_of_series_in_db = $series->existsInDatabase();
|
||||
if (defined($id_of_series_in_db) &&
|
||||
$id_of_series_in_db != $series->{'series_id'})
|
||||
{
|
||||
ThrowUserError("series_already_exists", {'series' => $series});
|
||||
}
|
||||
|
||||
$series->writeToDatabase();
|
||||
$vars->{'changes_saved'} = 1;
|
||||
|
||||
edit($series);
|
||||
}
|
||||
else {
|
||||
ThrowCodeError("unknown_action");
|
||||
}
|
||||
|
||||
exit;
|
||||
|
||||
# Find any selected series and return either the first or all of them.
|
||||
sub getAndValidateSeriesIDs {
|
||||
my @series_ids = grep(/^\d+$/, $cgi->param("name"));
|
||||
|
||||
return wantarray ? @series_ids : $series_ids[0];
|
||||
}
|
||||
|
||||
# Return a list of IDs of all the lines selected in the UI.
|
||||
sub getSelectedLines {
|
||||
my @ids = map { /^select(\d+)$/ ? $1 : () } $cgi->param();
|
||||
|
||||
return @ids;
|
||||
}
|
||||
|
||||
# Check if the user is the owner of series_id or is an admin.
|
||||
sub assertCanEdit {
|
||||
my ($series_id) = @_;
|
||||
my $user = Bugzilla->user;
|
||||
|
||||
return if $user->in_group('admin');
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
# Check if the user is permitted to create this series with these parameters.
|
||||
sub assertCanCreate {
|
||||
my ($cgi) = shift;
|
||||
|
||||
UserInGroup("editbugs") || ThrowUserError("illegal_series_creation");
|
||||
|
||||
# Check permission for frequency
|
||||
my $min_freq = 7;
|
||||
if ($cgi->param('frequency') < $min_freq && !UserInGroup("admin")) {
|
||||
ThrowUserError("illegal_frequency", { 'minimum' => $min_freq });
|
||||
}
|
||||
}
|
||||
|
||||
sub validateWidthAndHeight {
|
||||
$vars->{'width'} = $cgi->param('width');
|
||||
$vars->{'height'} = $cgi->param('height');
|
||||
|
||||
if (defined($vars->{'width'})) {
|
||||
(detaint_natural($vars->{'width'}) && $vars->{'width'} > 0)
|
||||
|| ThrowCodeError("invalid_dimensions");
|
||||
}
|
||||
|
||||
if (defined($vars->{'height'})) {
|
||||
(detaint_natural($vars->{'height'}) && $vars->{'height'} > 0)
|
||||
|| ThrowCodeError("invalid_dimensions");
|
||||
}
|
||||
|
||||
# The equivalent of 2000 square seems like a very reasonable maximum size.
|
||||
# This is merely meant to prevent accidental or deliberate DOS, and should
|
||||
# have no effect in practice.
|
||||
if ($vars->{'width'} && $vars->{'height'}) {
|
||||
(($vars->{'width'} * $vars->{'height'}) <= 4000000)
|
||||
|| ThrowUserError("chart_too_large");
|
||||
}
|
||||
}
|
||||
|
||||
sub edit {
|
||||
my $series = shift;
|
||||
|
||||
$vars->{'category'} = Bugzilla::Chart::getVisibleSeries();
|
||||
$vars->{'creator'} = new Bugzilla::User($series->{'creator'});
|
||||
$vars->{'default'} = $series;
|
||||
|
||||
print $cgi->header();
|
||||
$template->process("reports/edit-series.html.tmpl", $vars)
|
||||
|| ThrowTemplateError($template->error());
|
||||
}
|
||||
|
||||
sub plot {
|
||||
validateWidthAndHeight();
|
||||
$vars->{'chart'} = new Bugzilla::Chart($cgi);
|
||||
|
||||
my $format = $template->get_format("reports/chart", "", scalar($cgi->param('ctype')));
|
||||
|
||||
# Debugging PNGs is a pain; we need to be able to see the error messages
|
||||
if ($cgi->param('debug')) {
|
||||
print $cgi->header();
|
||||
$vars->{'chart'}->dump();
|
||||
}
|
||||
|
||||
print $cgi->header($format->{'ctype'});
|
||||
$template->process($format->{'template'}, $vars)
|
||||
|| ThrowTemplateError($template->error());
|
||||
}
|
||||
|
||||
sub wrap {
|
||||
validateWidthAndHeight();
|
||||
|
||||
# We create a Chart object so we can validate the parameters
|
||||
my $chart = new Bugzilla::Chart($cgi);
|
||||
|
||||
$vars->{'time'} = time();
|
||||
|
||||
$vars->{'imagebase'} = $cgi->canonicalise_query(
|
||||
"action", "action-wrap", "ctype", "format", "width", "height");
|
||||
|
||||
print $cgi->header();
|
||||
$template->process("reports/chart.html.tmpl", $vars)
|
||||
|| ThrowTemplateError($template->error());
|
||||
}
|
||||
|
||||
sub view {
|
||||
my $chart = shift;
|
||||
|
||||
# Set defaults
|
||||
foreach my $field ('category', 'subcategory', 'name', 'ctype') {
|
||||
$vars->{'default'}{$field} = $cgi->param($field) || 0;
|
||||
}
|
||||
|
||||
# Pass the state object to the display UI.
|
||||
$vars->{'chart'} = $chart;
|
||||
$vars->{'category'} = Bugzilla::Chart::getVisibleSeries();
|
||||
|
||||
print $cgi->header();
|
||||
|
||||
# If we have having problems with bad data, we can set debug=1 to dump
|
||||
# the data structure.
|
||||
$chart->dump() if $cgi->param('debug');
|
||||
|
||||
$template->process("reports/create-chart.html.tmpl", $vars)
|
||||
|| ThrowTemplateError($template->error());
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,157 +0,0 @@
|
||||
#!/usr/bin/perl -wT
|
||||
# -*- 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>
|
||||
# Gervase Markham <gerv@gerv.net>
|
||||
|
||||
use strict;
|
||||
|
||||
use lib qw(.);
|
||||
|
||||
use vars qw(
|
||||
@legal_keywords
|
||||
);
|
||||
|
||||
use Bugzilla;
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::User;
|
||||
require "globals.pl";
|
||||
|
||||
Bugzilla->login();
|
||||
|
||||
GetVersionTable();
|
||||
|
||||
my $cgi = Bugzilla->cgi;
|
||||
my $template = Bugzilla->template;
|
||||
my $vars = {};
|
||||
|
||||
# The master list not only says what fields are possible, but what order
|
||||
# they get displayed in.
|
||||
my @masterlist = ("opendate", "changeddate", "bug_severity", "priority",
|
||||
"rep_platform", "assigned_to", "assigned_to_realname",
|
||||
"reporter", "reporter_realname", "bug_status",
|
||||
"resolution");
|
||||
|
||||
if (Param("useclassification")) {
|
||||
push(@masterlist, "classification");
|
||||
}
|
||||
|
||||
push(@masterlist, ("product", "component", "version", "op_sys"));
|
||||
|
||||
if (Param("usevotes")) {
|
||||
push (@masterlist, "votes");
|
||||
}
|
||||
if (Param("usebugaliases")) {
|
||||
unshift(@masterlist, "alias");
|
||||
}
|
||||
if (Param("usetargetmilestone")) {
|
||||
push(@masterlist, "target_milestone");
|
||||
}
|
||||
if (Param("useqacontact")) {
|
||||
push(@masterlist, "qa_contact");
|
||||
push(@masterlist, "qa_contact_realname");
|
||||
}
|
||||
if (Param("usestatuswhiteboard")) {
|
||||
push(@masterlist, "status_whiteboard");
|
||||
}
|
||||
if (@::legal_keywords) {
|
||||
push(@masterlist, "keywords");
|
||||
}
|
||||
|
||||
if (UserInGroup(Param("timetrackinggroup"))) {
|
||||
push(@masterlist, ("estimated_time", "remaining_time", "actual_time",
|
||||
"percentage_complete", "deadline"));
|
||||
}
|
||||
|
||||
push(@masterlist, ("short_desc", "short_short_desc"));
|
||||
|
||||
$vars->{'masterlist'} = \@masterlist;
|
||||
|
||||
my @collist;
|
||||
if (defined $cgi->param('rememberedquery')) {
|
||||
my $splitheader = 0;
|
||||
if (defined $cgi->param('resetit')) {
|
||||
@collist = DEFAULT_COLUMN_LIST;
|
||||
} else {
|
||||
foreach my $i (@masterlist) {
|
||||
if (defined $cgi->param("column_$i")) {
|
||||
push @collist, $i;
|
||||
}
|
||||
}
|
||||
if (defined $cgi->param('splitheader')) {
|
||||
$splitheader = $cgi->param('splitheader')? 1: 0;
|
||||
}
|
||||
}
|
||||
my $list = join(" ", @collist);
|
||||
my $urlbase = Param("urlbase");
|
||||
|
||||
if ($list) {
|
||||
$cgi->send_cookie(-name => 'COLUMNLIST',
|
||||
-value => $list,
|
||||
-expires => 'Fri, 01-Jan-2038 00:00:00 GMT');
|
||||
}
|
||||
else {
|
||||
$cgi->remove_cookie('COLUMNLIST');
|
||||
}
|
||||
if ($splitheader) {
|
||||
$cgi->send_cookie(-name => 'SPLITHEADER',
|
||||
-value => $splitheader,
|
||||
-expires => 'Fri, 01-Jan-2038 00:00:00 GMT');
|
||||
}
|
||||
else {
|
||||
$cgi->remove_cookie('SPLITHEADER');
|
||||
}
|
||||
|
||||
$vars->{'message'} = "change_columns";
|
||||
$vars->{'redirect_url'} = "buglist.cgi?".$cgi->param('rememberedquery');
|
||||
|
||||
# If we're running on Microsoft IIS, using cgi->redirect discards
|
||||
# the Set-Cookie lines -- workaround is to use the old-fashioned
|
||||
# redirection mechanism. See bug 214466 for details.
|
||||
if ($ENV{'SERVER_SOFTWARE'} =~ /Microsoft-IIS/
|
||||
|| $ENV{'SERVER_SOFTWARE'} =~ /Sun ONE Web/)
|
||||
{
|
||||
print $cgi->header(-type => "text/html",
|
||||
-refresh => "0; URL=$vars->{'redirect_url'}");
|
||||
}
|
||||
else {
|
||||
print $cgi->redirect($vars->{'redirect_url'});
|
||||
}
|
||||
|
||||
$template->process("global/message.html.tmpl", $vars)
|
||||
|| ThrowTemplateError($template->error());
|
||||
exit;
|
||||
}
|
||||
|
||||
if (defined $cgi->cookie('COLUMNLIST')) {
|
||||
@collist = split(/ /, $cgi->cookie('COLUMNLIST'));
|
||||
} else {
|
||||
@collist = DEFAULT_COLUMN_LIST;
|
||||
}
|
||||
|
||||
$vars->{'collist'} = \@collist;
|
||||
$vars->{'splitheader'} = $cgi->cookie('SPLITHEADER') ? 1 : 0;
|
||||
|
||||
$vars->{'buffer'} = $cgi->query_string();
|
||||
|
||||
# Generate and return the UI (HTML page) from the appropriate template.
|
||||
print $cgi->header();
|
||||
$template->process("list/change-columns.html.tmpl", $vars)
|
||||
|| ThrowTemplateError($template->error());
|
||||
@@ -1,508 +0,0 @@
|
||||
#!/usr/bin/perl -w
|
||||
# -*- 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>,
|
||||
# Harrison Page <harrison@netscape.com>
|
||||
# Gervase Markham <gerv@gerv.net>
|
||||
# Richard Walters <rwalters@qualcomm.com>
|
||||
# Jean-Sebastien Guay <jean_seb@hybride.com>
|
||||
|
||||
# Run me out of cron at midnight to collect Bugzilla statistics.
|
||||
#
|
||||
# To run new charts for a specific date, pass it in on the command line in
|
||||
# ISO (2004-08-14) format.
|
||||
|
||||
use AnyDBM_File;
|
||||
use strict;
|
||||
use IO::Handle;
|
||||
use vars @::legal_product;
|
||||
|
||||
use lib ".";
|
||||
require "globals.pl";
|
||||
use Bugzilla::Search;
|
||||
use Bugzilla::User;
|
||||
|
||||
use Bugzilla;
|
||||
use Bugzilla::Config qw(:DEFAULT $datadir);
|
||||
|
||||
# Turn off output buffering (probably needed when displaying output feedback
|
||||
# in the regenerate mode.)
|
||||
$| = 1;
|
||||
|
||||
# Tidy up after graphing module
|
||||
if (chdir("graphs")) {
|
||||
unlink <./*.gif>;
|
||||
unlink <./*.png>;
|
||||
chdir("..");
|
||||
}
|
||||
|
||||
GetVersionTable();
|
||||
|
||||
Bugzilla->switch_to_shadow_db();
|
||||
|
||||
# To recreate the daily statistics, run "collectstats.pl --regenerate" .
|
||||
my $regenerate = 0;
|
||||
if ($#ARGV >= 0 && $ARGV[0] eq "--regenerate") {
|
||||
shift(@ARGV);
|
||||
$regenerate = 1;
|
||||
}
|
||||
|
||||
my @myproducts;
|
||||
push( @myproducts, "-All-", @::legal_product );
|
||||
|
||||
my $tstart = time;
|
||||
foreach (@myproducts) {
|
||||
my $dir = "$datadir/mining";
|
||||
|
||||
&check_data_dir ($dir);
|
||||
|
||||
if ($regenerate) {
|
||||
®enerate_stats($dir, $_);
|
||||
} else {
|
||||
&collect_stats($dir, $_);
|
||||
}
|
||||
}
|
||||
my $tend = time;
|
||||
# Uncomment the following line for performance testing.
|
||||
#print "Total time taken " . delta_time($tstart, $tend) . "\n";
|
||||
|
||||
&calculate_dupes();
|
||||
|
||||
CollectSeriesData();
|
||||
|
||||
{
|
||||
local $ENV{'GATEWAY_INTERFACE'} = 'cmdline';
|
||||
local $ENV{'REQUEST_METHOD'} = 'GET';
|
||||
local $ENV{'QUERY_STRING'} = 'ctype=rdf';
|
||||
|
||||
my $perl = $^X;
|
||||
trick_taint($perl);
|
||||
|
||||
# Generate a static RDF file containing the default view of the duplicates data.
|
||||
open(CGI, "$perl -T duplicates.cgi |")
|
||||
|| die "can't fork duplicates.cgi: $!";
|
||||
open(RDF, ">$datadir/duplicates.tmp")
|
||||
|| die "can't write to $datadir/duplicates.tmp: $!";
|
||||
my $headers_done = 0;
|
||||
while (<CGI>) {
|
||||
print RDF if $headers_done;
|
||||
$headers_done = 1 if $_ eq "\r\n";
|
||||
}
|
||||
close CGI;
|
||||
close RDF;
|
||||
}
|
||||
if (-s "$datadir/duplicates.tmp") {
|
||||
rename("$datadir/duplicates.rdf", "$datadir/duplicates-old.rdf");
|
||||
rename("$datadir/duplicates.tmp", "$datadir/duplicates.rdf");
|
||||
}
|
||||
|
||||
sub check_data_dir {
|
||||
my $dir = shift;
|
||||
|
||||
if (! -d $dir) {
|
||||
mkdir $dir, 0755;
|
||||
chmod 0755, $dir;
|
||||
}
|
||||
}
|
||||
|
||||
sub collect_stats {
|
||||
my $dir = shift;
|
||||
my $product = shift;
|
||||
my $when = localtime (time);
|
||||
my $product_id = get_product_id($product) unless $product eq '-All-';
|
||||
|
||||
die "Unknown product $product" unless ($product_id or $product eq '-All-');
|
||||
|
||||
# NB: Need to mangle the product for the filename, but use the real
|
||||
# product name in the query
|
||||
my $file_product = $product;
|
||||
$file_product =~ s/\//-/gs;
|
||||
my $file = join '/', $dir, $file_product;
|
||||
my $exists = -f $file;
|
||||
|
||||
if (open DATA, ">>$file") {
|
||||
push my @row, &today;
|
||||
|
||||
foreach my $status ('NEW', 'ASSIGNED', 'REOPENED', 'UNCONFIRMED', 'RESOLVED', 'VERIFIED', 'CLOSED') {
|
||||
if( $product eq "-All-" ) {
|
||||
SendSQL("SELECT COUNT(bug_status) FROM bugs WHERE bug_status='$status'");
|
||||
} else {
|
||||
SendSQL("SELECT COUNT(bug_status) FROM bugs WHERE bug_status='$status' AND product_id=$product_id");
|
||||
}
|
||||
|
||||
push @row, FetchOneColumn();
|
||||
}
|
||||
|
||||
foreach my $resolution ('FIXED', 'INVALID', 'WONTFIX', 'LATER', 'REMIND', 'DUPLICATE', 'WORKSFORME', 'MOVED') {
|
||||
if( $product eq "-All-" ) {
|
||||
SendSQL("SELECT COUNT(resolution) FROM bugs WHERE resolution='$resolution'");
|
||||
} else {
|
||||
SendSQL("SELECT COUNT(resolution) FROM bugs WHERE resolution='$resolution' AND product_id=$product_id");
|
||||
}
|
||||
|
||||
push @row, FetchOneColumn();
|
||||
}
|
||||
|
||||
if (! $exists) {
|
||||
print DATA <<FIN;
|
||||
# Bugzilla Daily Bug Stats
|
||||
#
|
||||
# Do not edit me! This file is generated.
|
||||
#
|
||||
# fields: DATE|NEW|ASSIGNED|REOPENED|UNCONFIRMED|RESOLVED|VERIFIED|CLOSED|FIXED|INVALID|WONTFIX|LATER|REMIND|DUPLICATE|WORKSFORME|MOVED
|
||||
# Product: $product
|
||||
# Created: $when
|
||||
FIN
|
||||
}
|
||||
|
||||
print DATA (join '|', @row) . "\n";
|
||||
close DATA;
|
||||
chmod 0644, $file;
|
||||
} else {
|
||||
print "$0: $file, $!";
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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 = shift;
|
||||
my $product = shift;
|
||||
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $when = localtime(time());
|
||||
my $tstart = time();
|
||||
|
||||
# NB: Need to mangle the product for the filename, but use the real
|
||||
# product name in the query
|
||||
my $file_product = $product;
|
||||
$file_product =~ s/\//-/gs;
|
||||
my $file = join '/', $dir, $file_product;
|
||||
|
||||
my @bugs;
|
||||
|
||||
my $and_product = "";
|
||||
my $from_product = "";
|
||||
|
||||
if ($product ne '-All-') {
|
||||
$and_product = " AND products.name = " . SqlQuote($product);
|
||||
$from_product = "INNER JOIN products " .
|
||||
"ON bugs.product_id = products.id";
|
||||
}
|
||||
|
||||
# Determine the start date from the date the first bug in the
|
||||
# database was created, and the end date from the current day.
|
||||
# If there were no bugs in the search, return early.
|
||||
SendSQL("SELECT " . $dbh->sql_to_days('creation_ts') . " AS start, " .
|
||||
$dbh->sql_to_days('current_date') . " AS end, " .
|
||||
$dbh->sql_to_days("'1970-01-01'") .
|
||||
" FROM bugs $from_product WHERE " .
|
||||
$dbh->sql_to_days('creation_ts') . " IS NOT NULL " .
|
||||
$and_product .
|
||||
" ORDER BY start " . $dbh->sql_limit(1));
|
||||
|
||||
my ($start, $end, $base) = FetchSQLData();
|
||||
if (!defined $start) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (open DATA, ">$file") {
|
||||
DATA->autoflush(1);
|
||||
print DATA <<FIN;
|
||||
# Bugzilla Daily Bug Stats
|
||||
#
|
||||
# Do not edit me! This file is generated.
|
||||
#
|
||||
# fields: DATE|NEW|ASSIGNED|REOPENED|UNCONFIRMED|RESOLVED|VERIFIED|CLOSED|FIXED|INVALID|WONTFIX|LATER|REMIND|DUPLICATE|WORKSFORME|MOVED
|
||||
# Product: $product
|
||||
# Created: $when
|
||||
FIN
|
||||
# For each day, generate a line of statistics.
|
||||
my $total_days = $end - $start;
|
||||
for (my $day = $start + 1; $day <= $end; $day++) {
|
||||
# Some output feedback
|
||||
my $percent_done = ($day - $start - 1) * 100 / $total_days;
|
||||
printf "\rRegenerating $product \[\%.1f\%\%]", $percent_done;
|
||||
|
||||
# Get a list of bugs that were created the previous day, and
|
||||
# add those bugs to the list of bugs for this product.
|
||||
SendSQL("SELECT bug_id FROM bugs $from_product " .
|
||||
" WHERE bugs.creation_ts < " . $dbh->sql_from_days($day - 1) .
|
||||
" AND bugs.creation_ts >= " . $dbh->sql_from_days($day - 2) .
|
||||
$and_product .
|
||||
" ORDER BY bug_id");
|
||||
|
||||
my @row;
|
||||
while (@row = FetchSQLData()) {
|
||||
push @bugs, $row[0];
|
||||
}
|
||||
|
||||
# For each bug that existed on that day, determine its status
|
||||
# at the beginning of the day. If there were no status
|
||||
# changes on or after that day, the status was the same as it
|
||||
# is today, which can be found in the bugs table. Otherwise,
|
||||
# the status was equal to the first "previous value" entry in
|
||||
# the bugs_activity table for that bug made on or after that
|
||||
# day.
|
||||
my %bugcount;
|
||||
my @logstates = qw(NEW ASSIGNED REOPENED UNCONFIRMED RESOLVED
|
||||
VERIFIED CLOSED);
|
||||
my @logresolutions = qw(FIXED INVALID WONTFIX LATER REMIND
|
||||
DUPLICATE WORKSFORME MOVED);
|
||||
foreach (@logstates) {
|
||||
$bugcount{$_} = 0;
|
||||
}
|
||||
|
||||
foreach (@logresolutions) {
|
||||
$bugcount{$_} = 0;
|
||||
}
|
||||
|
||||
for my $bug (@bugs) {
|
||||
# First, get information on various bug states.
|
||||
SendSQL("SELECT bugs_activity.removed " .
|
||||
" FROM bugs_activity " .
|
||||
"INNER JOIN fielddefs " .
|
||||
" ON bugs_activity.fieldid = fielddefs.fieldid " .
|
||||
" WHERE fielddefs.name = 'bug_status' " .
|
||||
" AND bugs_activity.bug_id = $bug " .
|
||||
" AND bugs_activity.bug_when >= " . $dbh->sql_from_days($day) .
|
||||
" ORDER BY bugs_activity.bug_when " .
|
||||
$dbh->sql_limit(1));
|
||||
|
||||
my $status;
|
||||
if (@row = FetchSQLData()) {
|
||||
$status = $row[0];
|
||||
} else {
|
||||
SendSQL("SELECT bug_status FROM bugs WHERE bug_id = $bug");
|
||||
$status = FetchOneColumn();
|
||||
}
|
||||
|
||||
if (defined $bugcount{$status}) {
|
||||
$bugcount{$status}++;
|
||||
}
|
||||
|
||||
# Next, get information on various bug resolutions.
|
||||
SendSQL("SELECT bugs_activity.removed " .
|
||||
" FROM bugs_activity " .
|
||||
"INNER JOIN fielddefs " .
|
||||
" ON bugs_activity.fieldid = fielddefs.fieldid " .
|
||||
" WHERE fielddefs.name = 'resolution' " .
|
||||
" AND bugs_activity.bug_id = $bug " .
|
||||
" AND bugs_activity.bug_when >= " . $dbh->sql_from_days($day) .
|
||||
" ORDER BY bugs_activity.bug_when " .
|
||||
$dbh->sql_limit(1));
|
||||
|
||||
if (@row = FetchSQLData()) {
|
||||
$status = $row[0];
|
||||
} else {
|
||||
SendSQL("SELECT resolution FROM bugs WHERE bug_id = $bug");
|
||||
$status = FetchOneColumn();
|
||||
}
|
||||
|
||||
if (defined $bugcount{$status}) {
|
||||
$bugcount{$status}++;
|
||||
}
|
||||
}
|
||||
|
||||
# Generate a line of output containing the date and counts
|
||||
# of bugs in each state.
|
||||
my $date = sqlday($day, $base);
|
||||
print DATA "$date";
|
||||
foreach (@logstates) {
|
||||
print DATA "|$bugcount{$_}";
|
||||
}
|
||||
|
||||
foreach (@logresolutions) {
|
||||
print DATA "|$bugcount{$_}";
|
||||
}
|
||||
|
||||
print DATA "\n";
|
||||
}
|
||||
|
||||
# Finish up output feedback for this product.
|
||||
my $tend = time;
|
||||
print "\rRegenerating $product \[100.0\%] - " .
|
||||
delta_time($tstart, $tend) . "\n";
|
||||
|
||||
close DATA;
|
||||
chmod 0640, $file;
|
||||
}
|
||||
}
|
||||
|
||||
sub today {
|
||||
my ($dom, $mon, $year) = (localtime(time))[3, 4, 5];
|
||||
return sprintf "%04d%02d%02d", 1900 + $year, ++$mon, $dom;
|
||||
}
|
||||
|
||||
sub today_dash {
|
||||
my ($dom, $mon, $year) = (localtime(time))[3, 4, 5];
|
||||
return sprintf "%04d-%02d-%02d", 1900 + $year, ++$mon, $dom;
|
||||
}
|
||||
|
||||
sub sqlday {
|
||||
my ($day, $base) = @_;
|
||||
$day = ($day - $base) * 86400;
|
||||
my ($dom, $mon, $year) = (gmtime($day))[3, 4, 5];
|
||||
return sprintf "%04d%02d%02d", 1900 + $year, ++$mon, $dom;
|
||||
}
|
||||
|
||||
sub delta_time {
|
||||
my $tstart = shift;
|
||||
my $tend = shift;
|
||||
my $delta = $tend - $tstart;
|
||||
my $hours = int($delta/3600);
|
||||
my $minutes = int($delta/60) - ($hours * 60);
|
||||
my $seconds = $delta - ($minutes * 60) - ($hours * 3600);
|
||||
return sprintf("%02d:%02d:%02d" , $hours, $minutes, $seconds);
|
||||
}
|
||||
|
||||
sub CollectSeriesData {
|
||||
# We need some way of randomising the distribution of series, such that
|
||||
# all of the series which are to be run every 7 days don't run on the same
|
||||
# day. This is because this might put the server under severe load if a
|
||||
# particular frequency, such as once a week, is very common. We achieve
|
||||
# this by only running queries when:
|
||||
# (days_since_epoch + series_id) % frequency = 0. So they'll run every
|
||||
# <frequency> days, but the start date depends on the series_id.
|
||||
my $days_since_epoch = int(time() / (60 * 60 * 24));
|
||||
my $today = $ARGV[0] || today_dash();
|
||||
|
||||
# We save a copy of the main $dbh and then switch to the shadow and get
|
||||
# that one too. Remember, these may be the same.
|
||||
my $dbh = Bugzilla->switch_to_main_db();
|
||||
my $shadow_dbh = Bugzilla->switch_to_shadow_db();
|
||||
|
||||
my $serieses = $dbh->selectall_hashref("SELECT series_id, query, creator " .
|
||||
"FROM series " .
|
||||
"WHERE frequency != 0 AND " .
|
||||
"($days_since_epoch + series_id) % frequency = 0",
|
||||
"series_id");
|
||||
|
||||
# We prepare the insertion into the data table, for efficiency.
|
||||
my $sth = $dbh->prepare("INSERT INTO series_data " .
|
||||
"(series_id, series_date, series_value) " .
|
||||
"VALUES (?, " . $dbh->quote($today) . ", ?)");
|
||||
|
||||
# We delete from the table beforehand, to avoid SQL errors if people run
|
||||
# collectstats.pl twice on the same day.
|
||||
my $deletesth = $dbh->prepare("DELETE FROM series_data
|
||||
WHERE series_id = ? AND series_date = " .
|
||||
$dbh->quote($today));
|
||||
|
||||
foreach my $series_id (keys %$serieses) {
|
||||
# We set up the user for Search.pm's permission checking - each series
|
||||
# runs with the permissions of its creator.
|
||||
my $user = new Bugzilla::User($serieses->{$series_id}->{'creator'});
|
||||
my $cgi = new Bugzilla::CGI($serieses->{$series_id}->{'query'});
|
||||
my $data;
|
||||
|
||||
# Do not die if Search->new() detects invalid data, such as an obsolete
|
||||
# login name or a renamed product or component, etc.
|
||||
eval {
|
||||
my $search = new Bugzilla::Search('params' => $cgi,
|
||||
'fields' => ["bugs.bug_id"],
|
||||
'user' => $user);
|
||||
my $sql = $search->getSQL();
|
||||
$data = $shadow_dbh->selectall_arrayref($sql);
|
||||
};
|
||||
|
||||
if (!$@) {
|
||||
# We need to count the returned rows. Without subselects, we can't
|
||||
# do this directly in the SQL for all queries. So we do it by hand.
|
||||
my $count = scalar(@$data) || 0;
|
||||
|
||||
$deletesth->execute($series_id);
|
||||
$sth->execute($series_id, $count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
#!/usr/bin/perl -wT
|
||||
# -*- 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>
|
||||
# Myk Melez <myk@mozilla.org>
|
||||
|
||||
################################################################################
|
||||
# Script Initialization
|
||||
################################################################################
|
||||
|
||||
# Make it harder for us to do dangerous things in Perl.
|
||||
use strict;
|
||||
|
||||
# Include the Bugzilla CGI and general utility library.
|
||||
use lib qw(.);
|
||||
require "globals.pl";
|
||||
use Bugzilla;
|
||||
use Bugzilla::Constants;
|
||||
|
||||
# Suppress "used only once" warnings.
|
||||
use vars
|
||||
qw(
|
||||
@legal_priority
|
||||
@legal_severity
|
||||
@legal_platform
|
||||
@legal_opsys
|
||||
@legal_resolution
|
||||
|
||||
@legal_components
|
||||
@legal_target_milestone
|
||||
@legal_versions
|
||||
@legal_keywords
|
||||
);
|
||||
|
||||
# Use the global template variables defined in globals.pl
|
||||
# to generate the output.
|
||||
|
||||
my $user = Bugzilla->login(LOGIN_OPTIONAL);
|
||||
|
||||
# If the 'requirelogin' parameter is on and the user is not
|
||||
# authenticated, return empty fields.
|
||||
if (Param('requirelogin') && !$user->id) {
|
||||
display_data();
|
||||
}
|
||||
|
||||
# Retrieve this installation's configuration.
|
||||
GetVersionTable();
|
||||
|
||||
# Pass a bunch of Bugzilla configuration to the templates.
|
||||
my $vars = {};
|
||||
$vars->{'priority'} = \@::legal_priority;
|
||||
$vars->{'severity'} = \@::legal_severity;
|
||||
$vars->{'platform'} = \@::legal_platform;
|
||||
$vars->{'op_sys'} = \@::legal_opsys;
|
||||
$vars->{'keyword'} = \@::legal_keywords;
|
||||
$vars->{'resolution'} = \@::legal_resolution;
|
||||
$vars->{'status'} = \@::legal_bug_status;
|
||||
|
||||
# Include a list of product objects.
|
||||
$vars->{'products'} = $user->get_selectable_products;
|
||||
|
||||
# Create separate lists of open versus resolved statuses. This should really
|
||||
# be made part of the configuration.
|
||||
my @open_status;
|
||||
my @closed_status;
|
||||
foreach my $status (@::legal_bug_status) {
|
||||
IsOpenedState($status) ? push(@open_status, $status)
|
||||
: push(@closed_status, $status);
|
||||
}
|
||||
$vars->{'open_status'} = \@open_status;
|
||||
$vars->{'closed_status'} = \@closed_status;
|
||||
|
||||
# Generate a list of fields that can be queried.
|
||||
$vars->{'field'} = [Bugzilla->dbh->bz_get_field_defs()];
|
||||
|
||||
display_data($vars);
|
||||
|
||||
|
||||
sub display_data {
|
||||
my $vars = shift;
|
||||
|
||||
my $cgi = Bugzilla->cgi;
|
||||
my $template = Bugzilla->template;
|
||||
|
||||
# Determine how the user would like to receive the output;
|
||||
# default is JavaScript.
|
||||
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)
|
||||
|| ThrowTemplateError($template->error());
|
||||
exit;
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
# -*- 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.
|
||||
#
|
||||
# This code is based on code found in bug_email.pl from the bugzilla
|
||||
# email tracker. Initial contributors are ::
|
||||
# Terry Weissman <terry@mozilla.org>
|
||||
# Gregor Fischer <fischer@suse.de>
|
||||
# Klaas Freitag <freitag@suse.de>
|
||||
# Seth Landsman <seth@dworkin.net>
|
||||
# Lance Larsh <lance.larsh@oracle.com>
|
||||
|
||||
# The purpose of this module is to abstract out a bunch of the code
|
||||
# that is central to email interfaces to bugzilla and its database
|
||||
|
||||
# Contributor : Seth Landsman <seth@dworkin.net>
|
||||
|
||||
# Initial checkin : 03/15/00 (SML)
|
||||
# findUser() function moved from bug_email.pl to here
|
||||
|
||||
push @INC, "../."; # this script now lives in contrib
|
||||
|
||||
require "globals.pl";
|
||||
|
||||
use strict;
|
||||
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
my $EMAIL_TRANSFORM_NONE = "email_transform_none";
|
||||
my $EMAIL_TRANSFORM_BASE_DOMAIN = "email_transform_base_domain";
|
||||
my $EMAIL_TRANSFORM_NAME_ONLY = "email_transform_name_only";
|
||||
|
||||
# change to do incoming email address fuzzy matching
|
||||
my $email_transform = $EMAIL_TRANSFORM_NAME_ONLY;
|
||||
|
||||
# findUser()
|
||||
# This function takes an email address and returns the user email.
|
||||
# matching is sloppy based on the $email_transform parameter
|
||||
sub findUser($) {
|
||||
my ($address) = @_;
|
||||
# if $email_transform is $EMAIL_TRANSFORM_NONE, return the address, otherwise, return undef
|
||||
if ($email_transform eq $EMAIL_TRANSFORM_NONE) {
|
||||
my $stmt = "SELECT login_name FROM profiles WHERE " .
|
||||
$dbh->sql_istrcmp('login_name', $dbh->quote($address));
|
||||
SendSQL($stmt);
|
||||
my $found_address = FetchOneColumn();
|
||||
return $found_address;
|
||||
} elsif ($email_transform eq $EMAIL_TRANSFORM_BASE_DOMAIN) {
|
||||
my ($username) = ($address =~ /(.+)@/);
|
||||
my $stmt = "SELECT login_name FROM profiles WHERE " . $dbh->sql_regexp(
|
||||
$dbh->sql_istring('login_name'), $dbh->sql_istring($dbh->quote($username)));
|
||||
SendSQL($stmt);
|
||||
|
||||
my $domain;
|
||||
my $found = undef;
|
||||
my $found_address;
|
||||
my $new_address = undef;
|
||||
while ((!$found) && ($found_address = FetchOneColumn())) {
|
||||
($domain) = ($found_address =~ /.+@(.+)/);
|
||||
if ($address =~ /$domain/) {
|
||||
$found = 1;
|
||||
$new_address = $found_address;
|
||||
}
|
||||
}
|
||||
return $new_address;
|
||||
} elsif ($email_transform eq $EMAIL_TRANSFORM_NAME_ONLY) {
|
||||
my ($username) = ($address =~ /(.+)@/);
|
||||
my $stmt = "SELECT login_name FROM profiles WHERE " .$dbh->sql_regexp(
|
||||
$dbh->sql_istring('login_name'), $dbh->sql_istring($dbh->quote($username)));
|
||||
SendSQL($stmt);
|
||||
my $found_address = FetchOneColumn();
|
||||
return $found_address;
|
||||
}
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -1,54 +0,0 @@
|
||||
This directory contains contributed software related to Bugzilla.
|
||||
Things in here have not necessarily been tested or tried by anyone
|
||||
except the original contributor, so tred carefully. But it may still
|
||||
be useful to you.
|
||||
|
||||
This file is encoded in UTF8 for purposes of contributor names.
|
||||
|
||||
This directory includes:
|
||||
|
||||
bugzilla-submit/ -- A standalone bug submission program.
|
||||
|
||||
mysqld-watcher.pl -- This script can be installed as a frequent cron
|
||||
job to clean up stalled/dead queries.
|
||||
|
||||
sendbugmail.pl -- This script is a drop-in replacement for the
|
||||
'processmail' script which used to be shipped
|
||||
with Bugzilla, but was replaced by the
|
||||
Bugzilla/BugMail.pm Perl module. You can use
|
||||
this script if you were previously calling
|
||||
processmail from other scripts external to
|
||||
Bugzilla. See the comments at the top of
|
||||
the file for usage information. Contributed
|
||||
by Nick Barnes of Ravenbrook Limited.
|
||||
|
||||
gnatsparse/ -- A Python script used to import a GNATS database
|
||||
into Bugzilla.
|
||||
|
||||
gnats2bz.pl -- A perl script to help import bugs from a GNATS
|
||||
database into a Bugzilla database. Contributed by
|
||||
Tom Schutter <tom@platte.com>
|
||||
|
||||
bug_email.pl -- A perl script that can receive email containing
|
||||
bug reports (email-interface). Contributed by
|
||||
Klaas Freitag <freitag@SuSE.de>
|
||||
|
||||
README.Mailif -- Readme describing the mail interface.
|
||||
bugmail_help.html -- User help page for the mail interface.
|
||||
|
||||
yp_nomail.sh -- Script you can run via cron that regularly updates
|
||||
the nomail file for terminated employees
|
||||
|
||||
bugzilla_ldapsync.rb -- Script you can run via cron that queries an LDAP
|
||||
server for e-mail addresses to add Bugzilla users
|
||||
for. Will optionally disable Bugzilla users with
|
||||
no matching LDAP record. Contributed by Thomas
|
||||
Stromberg <thomas+bugzilla@stromberg.org>
|
||||
|
||||
syncLDAP.pl -- Script you can run via cron that queries an LDAP
|
||||
server for users and e-mail addresses and adds
|
||||
missing users to Bugzilla. Can disable/update
|
||||
non-existing/changed information. Contributed by
|
||||
Andreas Höfler <andreas.hoefler@bearingpoint.com>
|
||||
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
|
||||
The Bugzilla Mail interface
|
||||
===========================
|
||||
|
||||
(UPDATE 03/14/00 to better reflect reality by SML)
|
||||
|
||||
The Bugzilla Mail interface allows to submit bugs to Bugzilla by email.
|
||||
|
||||
The Mail Interface Contribution consists of three files:
|
||||
README.Mailif - this readme.
|
||||
bug_email.pl - the script
|
||||
bugmail_help.html - a user help html site
|
||||
|
||||
Installation:
|
||||
|
||||
Next is to add a user who receives the bugmails, e. g. bugmail. Create a
|
||||
mail account and a home directory for the user.
|
||||
|
||||
The mailinterface script bug_email.pl needs to get the mail through stdin.
|
||||
I use procmail for that, with the following line in the .procmailrc:
|
||||
|
||||
BUGZILLA_HOME=/usr/local/httpd/htdocs/bugzilla
|
||||
:0 c
|
||||
|(cd $BUGZILLA_HOME/contrib; ./bug_email.pl)
|
||||
|
||||
This defines the Bugzilla directory as the variable BUGZILLA_HOME and passes
|
||||
all incoming mail to the script after cd'ing into the bugzilla home.
|
||||
|
||||
In some cases, it is necessary to alter the headers of incoming email. The
|
||||
additional line to procmail :
|
||||
|
||||
:0 fhw
|
||||
| formail -I "From " -a "From "
|
||||
|
||||
fixes many problems.
|
||||
|
||||
See bugzilla.procmailrc for a sample procmailrc that works for me (SML) and
|
||||
also deals with bugzilla_email_append.pl
|
||||
|
||||
Customation:
|
||||
|
||||
There are some values inside the script which need to be customized for your
|
||||
needs:
|
||||
|
||||
1. In sub-routine Reply (search 'sub Reply':
|
||||
there is the line
|
||||
print MAIL "From: Bugzilla Mailinterface<yourmail\@here.com>\n";
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
Fill in your correct mail here. That will make it easy for people to reply
|
||||
to the mail.
|
||||
|
||||
2. check, if your sendmail resides in /usr/sbin/sendmail, change the path if neccessary.
|
||||
Search the script after 'default' - you find some default-Settings for bug
|
||||
reports, which are used, if the sender did not send a field for it. The defaults
|
||||
should be checked and changed.
|
||||
|
||||
That's hopefully all, we will come up with any configuration file or something.
|
||||
|
||||
|
||||
If your mail works, your script will insert mails from now on.
|
||||
|
||||
The mailinterface supports two commandline switches:
|
||||
|
||||
There are two command line switches :
|
||||
|
||||
-t: Testmode
|
||||
The mailinterface does not really insert the bug into the database, but
|
||||
writes some debug output to stdout and writes the mail into the file
|
||||
bug_email_test.log in the data-dir.
|
||||
|
||||
-r: restricted mode
|
||||
All lines before the first line with a keyword character are skipped.
|
||||
In not restricted, default mode, these lines are added to the long
|
||||
description of the bug.
|
||||
|
||||
|
||||
02/2000 - Klaas Freitag, SuSE GmbH <freitag@suse.de>
|
||||
03/2000 - Seth M. Landsman <seth@cs.brandeis.edu>
|
||||
bug_email.pl now lives out of bugzilla/contrib
|
||||
added line about formail
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,223 +0,0 @@
|
||||
<HTML>
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//EN">
|
||||
<!--
|
||||
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): Klaas Freitag <Freitag@SuSE.de>
|
||||
-->
|
||||
|
||||
<HEAD> <TITLE>Bugzilla Mail Interface</TITLE> </HEAD>
|
||||
<BODY BGCOLOR="#FFFFFF">
|
||||
<CENTER><H1>The Bugzilla Mail Interface</H1>
|
||||
Contributor: <A HREF="mailto:freitag@suse.de">Klaas Freitag</A>, SuSE GmbH
|
||||
</CENTER>
|
||||
<P>
|
||||
The bugzilla Mail interface allows the registered bugzilla users to submit bugs by
|
||||
sending email with a bug description. This is useful for people, who do not work
|
||||
inhouse and want to submitt bugs to the bugzilla system.
|
||||
<p>
|
||||
|
||||
|
||||
I know, show me the <A HREF="#examplemail">example-mail !</A>
|
||||
|
||||
|
||||
<H2>What do you need to do to submitt a bug by mail ?</H2>
|
||||
You need to send a email in the described format to the bugmail-user of the
|
||||
bugzilla-system. This is <A HREF="mailto:our_bugzilla@xyz.com">yourbugzilla@here.com</A>
|
||||
|
||||
You receive a reply mail with the new bug-ID if your request was ok.
|
||||
If not, you get a mail with
|
||||
some help on the bugmail system and a specific analysis of your request.
|
||||
<P>
|
||||
Please don't refuse to send one or two wrong mails, you will get all the information
|
||||
you need in the replies, and <I>only</I> in the mail replies. The information on this
|
||||
page, concerning available products, versions and so on, is not dynamicly generated and
|
||||
may be old therefore.
|
||||
|
||||
<H1>The Mail Format</H1>
|
||||
The bugmail needs a special format , which consists of some keywords and suitable
|
||||
values for them and a description text. Note that the keyword block needs to be
|
||||
above of the description text.
|
||||
|
||||
<H2>Keywords</H2>
|
||||
You need to tell bugzilla some properties of the bugs. This is done by keywords, which
|
||||
start on a new line with a @, followed by the keyword and and equal-sign, followed by a
|
||||
hopefully valid value.
|
||||
|
||||
|
||||
<TABLE BORDER=4 FRAME=box CELLSPACING="5" width=95%> <COLGROUP> <col width="2*">
|
||||
<col width="5*"> <col width="1*"> </COLGROUP>
|
||||
<TR>
|
||||
<TH>Keyword</TH>
|
||||
<TH>Value description</TH>
|
||||
<TH>required and default value</TH>
|
||||
</TR>
|
||||
<TR>
|
||||
<TD>@product</TD>
|
||||
<TD>The product which has a bug</TD>
|
||||
<TD>yes. <br> This is the most important information. Many other
|
||||
fields depend on the product.</TD>
|
||||
</TR>
|
||||
<TR>
|
||||
<TD>@component</TD>
|
||||
<TD>the desired component which is affected by the bug</TD>
|
||||
<TD>yes. <br> As the @product, this is a very important
|
||||
field.</TD>
|
||||
</TR>
|
||||
<TR>
|
||||
<TD>@version</TD>
|
||||
<TD>The version of the product</TD>
|
||||
<TD>yes. <br>See @product and @component</TD>
|
||||
</TR>
|
||||
<TR>
|
||||
<TD>@short_desc</TD>
|
||||
<TD>A summary of your bug report</TD>
|
||||
<TD>yes. <br>This summary of the error you want to report
|
||||
describes what happen. You may skip the long description,
|
||||
but not this summary.<br>
|
||||
<b>Note:</b>The short description may be given in the mail subject
|
||||
instead of using the keyword !</TD>
|
||||
</TR>
|
||||
<TR>
|
||||
<TD>@rep_platform</TD>
|
||||
<TD>The desired platform</TD>
|
||||
<TD>no.<br>If you don't give a value, this field is set to <I>All</I>.</TD>
|
||||
</TR>
|
||||
<TR>
|
||||
<TD>@bug_severity</TD>
|
||||
<TD>The severity of the bug</TD>
|
||||
<TD>no. <br> If you don't give a value, this field is set to
|
||||
<I>normal</I></TD>
|
||||
</TR>
|
||||
<TR>
|
||||
<TD>@priority</TD>
|
||||
<TD>The priority of the bug</TD>
|
||||
<TD>no.<br>If you don't give a value, this field is set to <I>P3</I></TD>
|
||||
</TR>
|
||||
<TR>
|
||||
<TD>@op_sys</TD>
|
||||
<TD>The operating system</TD>
|
||||
<TD>no.<br>If you don't give a value, this field is set to <I>Linux</I>.</TD>
|
||||
</TR>
|
||||
<TR>
|
||||
<TD>@assigned_to</TD>
|
||||
<TD>The one to whom the bug is assigned to</TD>
|
||||
<TD>no. <br>There is a default assignee for every product/version/component.
|
||||
He owns the bug by default. The default assignee can only be found if
|
||||
product, version and component are valid.</TD>
|
||||
</TR>
|
||||
<TR>
|
||||
<TD>@bug_file_loc</TD>
|
||||
<TD>?</TD>
|
||||
<TD>no.</TD>
|
||||
</TR>
|
||||
<TR>
|
||||
<TD>@status_whiteboard</TD>
|
||||
<TD>?</TD>
|
||||
<TD>no.</TD>
|
||||
</TR>
|
||||
<TR>
|
||||
<TD>@target_milestone</TD>
|
||||
<TD>?</TD>
|
||||
<TD>no.</TD>
|
||||
</TR>
|
||||
<TR>
|
||||
<TD>@groupset</TD>
|
||||
<TD>rules the visibility of the bug.</TD>
|
||||
<TD>no.<br>This value defaults to the smallest of the available groups,
|
||||
which is <I>readInternal</I>.</TD>
|
||||
</TR>
|
||||
<TR>
|
||||
<TD>@qa_contact</TD>
|
||||
<TD>the quality manager for the product</TD>
|
||||
<TD>no.<br>This value can be retrieved from product, component and
|
||||
version</TD>
|
||||
</TR>
|
||||
|
||||
</TABLE>
|
||||
<H2>Valid values</H2>
|
||||
Give string values for the most keys above. Some keywords require special values:<br>
|
||||
<ol>
|
||||
<li>E-Mail addresses: If you want to set the qa-contact, specify an email-address for @qa_contact. The email must be known by bugzilla of course.</li>
|
||||
<li>Listvalues: Most of the values have to be one of a list of valid values. Try by sending
|
||||
a mail and read the reply. Skip fields if you don't get help for them unless you don't know
|
||||
which values you may choose.</li>
|
||||
<li>free Text: The descriptions may be free text. </li>
|
||||
<li>Special: The field groupset may be specified in different in three different kinds:
|
||||
<ol>
|
||||
<li> A plain numeric way, which is one usually huge number, e. g. <I>65536</I></li>
|
||||
<li> a string with added numbers e.g. <I>65536+131072</I></li>
|
||||
<li> a string list, e.g. <I>ReadInternal, ReadBeta </I></li>
|
||||
</ol>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<p>
|
||||
|
||||
But most of them need <b>valid</b> values.
|
||||
<p>
|
||||
Sorry, you will not find lists of valid products, components and the other stuff
|
||||
here. Send a mail to with any text, and you will get a list of valid keywords in the reply.
|
||||
|
||||
<p>
|
||||
Some of the values must be choosen from a list:<br>
|
||||
<ol>
|
||||
<li>bug_severity: blocker, critical, major, normal, minor, trivial, enhancement</li>
|
||||
<li>op_sys: Linux </li>
|
||||
<li>priority: P1, P2, P3, P4, P5</li>
|
||||
<li>rep_platform: All, i386, AXP, i686, Other</li></ol>
|
||||
|
||||
|
||||
<p>
|
||||
|
||||
After you have specified the required keywords and maybe some other value, you may
|
||||
describe your bug. You don't need a keyword for starting your bug description. All
|
||||
text which follows the keyword block is handled as long description of the bug.
|
||||
<p>
|
||||
|
||||
The bugmail interface is able to find required information by itself. E.g. if you specify
|
||||
a product which has exactly one component, this component will be found by the interface
|
||||
automatically.
|
||||
|
||||
<H1>Attachments</H1>
|
||||
|
||||
The mail interface is able to cope with MIME-attachments.
|
||||
People could for example add a logfile as a mail attachment, and it will appear in
|
||||
bugzilla as attachment. A comment for the attachment should be added, it will describe
|
||||
the attachment in bugzilla.
|
||||
|
||||
<H1><A NAME="examplemail">Example Mail</A></H1>
|
||||
|
||||
See the example of the mail <b>body</b> (Don't forget to specify the short description
|
||||
in the mail subject):<hr><pre>
|
||||
|
||||
@product = Bugzilla
|
||||
@component = general
|
||||
@version = All
|
||||
@groupset = ReadWorld ReadPartners
|
||||
@op_sys = Linux
|
||||
@priority = P3
|
||||
@rep_platform = i386
|
||||
|
||||
|
||||
This is the description of the bug I found. It is not neccessary to start
|
||||
it with a keyword.
|
||||
|
||||
Note: The short_description is neccessary and may be given with the keyword
|
||||
@short_description or will be retrieved from the mail subject.
|
||||
|
||||
|
||||
</pre><hr>
|
||||
|
||||
</BODY>
|
||||
</HTML>
|
||||
@@ -1,46 +0,0 @@
|
||||
bugzilla-submit
|
||||
===============
|
||||
|
||||
Authors: Christian Reis <kiko@async.com.br>
|
||||
Eric Raymond <esr@thyrsus.com>
|
||||
|
||||
bugzilla-submit is a simple Python program that creates bugs in a Bugzilla
|
||||
instance. It takes as input text resembling message headers (RFC-822
|
||||
formatted) via standard input, or optionally a number of commandline
|
||||
parameters. It communicates using HTTP, which allows it to work over a
|
||||
network.
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
||||
Its only requirement is Python 2.3 or higher; you should have the
|
||||
"python" executable in your path.
|
||||
|
||||
Usage Notes
|
||||
-----------
|
||||
|
||||
* Please constrain testing to your own installation of Bugzilla, or use
|
||||
* http://landfill.bugzilla.org/ for testing purposes -- opening test
|
||||
* bugs on production instances of Bugzilla is definitely not a good idea
|
||||
|
||||
Run "bugzilla-submit --help" for a description of the possible options.
|
||||
|
||||
An example input file, named bugdata.txt, is provided. You can pipe it
|
||||
in as standard input to bugzilla-submit, providing a Bugzilla URI through
|
||||
the command-line.
|
||||
|
||||
Note that you must create a ~/.netrc entry to authenticate against the
|
||||
Bugzilla instance. The entry's machine field is a *quoted* Bugzilla URI,
|
||||
the login field is your ID on that host, and the password field is the
|
||||
your password password. An example entry follows:
|
||||
|
||||
machine "http://bugzilla.mozilla.org/"
|
||||
login foo@bar.loo
|
||||
password snarf
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
Documentation for bugzilla-submit is provided in Docbook format; see
|
||||
bugzilla-submit.xml.
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
Product: FoodReplicator
|
||||
Component: Salt
|
||||
Version: 1.0
|
||||
Priority: P2
|
||||
Hardware: PC
|
||||
OS: Linux
|
||||
Severity: critical
|
||||
Summary: Impending electron shortage
|
||||
Depends-on: 8
|
||||
URL: http://www.google.com/
|
||||
|
||||
We need an emergency supply of electrons.
|
||||
|
||||
@@ -1,301 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# bugzilla-submit: a command-line script to post bugs to a Bugzilla instance
|
||||
#
|
||||
# Authors: Christian Reis <kiko@async.com.br>
|
||||
# Eric S. Raymond <esr@thyrsus.com>
|
||||
#
|
||||
# This is version 0.5.
|
||||
#
|
||||
# For a usage hint run bugzilla-submit --help
|
||||
#
|
||||
# TODO: use RDF output to pick up valid options, as in
|
||||
# http://www.async.com.br/~kiko/mybugzilla/config.cgi?ctype=rdf
|
||||
|
||||
import sys, string
|
||||
|
||||
def error(m):
|
||||
sys.stderr.write("bugzilla-submit: %s\n" % m)
|
||||
sys.stderr.flush()
|
||||
sys.exit(1)
|
||||
|
||||
version = string.split(string.split(sys.version)[0], ".")[:2]
|
||||
if map(int, version) < [2, 3]:
|
||||
error("you must upgrade to Python 2.3 or higher to use this script.")
|
||||
|
||||
import urllib, re, os, netrc, email.Parser, optparse
|
||||
|
||||
class ErrorURLopener(urllib.URLopener):
|
||||
"""URLopener that handles HTTP 404s"""
|
||||
def http_error_404(self, url, fp, errcode, errmsg, headers, *extra):
|
||||
raise ValueError, errmsg # 'File Not Found'
|
||||
|
||||
# Set up some aliases -- partly to hide the less friendly fieldnames
|
||||
# behind the names actually used for them in the stock web page presentation,
|
||||
# and partly to provide a place for mappings if the Bugzilla fieldnames
|
||||
# ever change.
|
||||
field_aliases = (('hardware', 'rep_platform'),
|
||||
('os', 'op_sys'),
|
||||
('summary', 'short_desc'),
|
||||
('description', 'comment'),
|
||||
('depends_on', 'dependson'),
|
||||
('status', 'bug_status'),
|
||||
('severity', 'bug_severity'),
|
||||
('url', 'bug_file_loc'),)
|
||||
|
||||
def header_to_field(hdr):
|
||||
hdr = hdr.lower().replace("-", "_")
|
||||
for (alias, name) in field_aliases:
|
||||
if hdr == alias:
|
||||
hdr = name
|
||||
break
|
||||
return hdr
|
||||
|
||||
def field_to_header(hdr):
|
||||
hdr = "-".join(map(lambda x: x.capitalize(), hdr.split("_")))
|
||||
for (alias, name) in field_aliases:
|
||||
if hdr == name:
|
||||
hdr = alias
|
||||
break
|
||||
return hdr
|
||||
|
||||
def setup_parser():
|
||||
# Take override values from the command line
|
||||
parser = optparse.OptionParser(usage="usage: %prog [options] bugzilla-url")
|
||||
parser.add_option('-b', '--status', dest='bug_status',
|
||||
help='Set the Status field.')
|
||||
parser.add_option('-u', '--url', dest='bug_file_loc',
|
||||
help='Set the URL field.')
|
||||
parser.add_option('-p', '--product', dest='product',
|
||||
help='Set the Product field.')
|
||||
parser.add_option('-v', '--version', dest='version',
|
||||
help='Set the Version field.')
|
||||
parser.add_option('-c', '--component', dest='component',
|
||||
help='Set the Component field.')
|
||||
parser.add_option('-s', '--summary', dest='short_desc',
|
||||
help='Set the Summary field.')
|
||||
parser.add_option('-H', '--hardware', dest='rep_platform',
|
||||
help='Set the Hardware field.')
|
||||
parser.add_option('-o', '--os', dest='op_sys',
|
||||
help='Set the Operating System field.')
|
||||
parser.add_option('-r', '--priority', dest='priority',
|
||||
help='Set the Priority field.')
|
||||
parser.add_option('-x', '--severity', dest='bug_severity',
|
||||
help='Set the Severity field.')
|
||||
parser.add_option('-d', '--description', dest='comment',
|
||||
help='Set the Description field.')
|
||||
parser.add_option('-a', '--assigned-to', dest='assigned_to',
|
||||
help='Set the Assigned-To field.')
|
||||
parser.add_option('-C', '--cc', dest='cc',
|
||||
help='Set the Cc field.')
|
||||
parser.add_option('-k', '--keywords', dest='keywords',
|
||||
help='Set the Keywords field.')
|
||||
parser.add_option('-D', '--depends-on', dest='dependson',
|
||||
help='Set the Depends-On field.')
|
||||
parser.add_option('-B', '--blocked', dest='blocked',
|
||||
help='Set the Blocked field.')
|
||||
parser.add_option('-n', '--no-stdin', dest='read',
|
||||
default=True, action='store_false',
|
||||
help='Suppress reading fields from stdin.')
|
||||
return parser
|
||||
|
||||
# Fetch user's credential for access to this Bugzilla instance.
|
||||
def get_credentials(bugzilla):
|
||||
# Work around a quirk in the Python implementation.
|
||||
# The URL has to be quoted, otherwise the parser barfs on the colon.
|
||||
# But the parser doesn't strip the quotes.
|
||||
authenticate_on = '"' + bugzilla + '"'
|
||||
try:
|
||||
credentials = netrc.netrc()
|
||||
except netrc.NetrcParseError, e:
|
||||
error("ill-formed .netrc: %s:%s %s" % (e.filename, e.lineno, e.msg))
|
||||
except IOError, e:
|
||||
error("missing .netrc file %s" % str(e).split()[-1])
|
||||
ret = credentials.authenticators(authenticate_on)
|
||||
if not ret:
|
||||
# Okay, the literal string passed in failed. Just to make sure,
|
||||
# try adding/removing a slash after the address and looking
|
||||
# again. We don't know what format was used in .netrc, which is
|
||||
# why this rather hackish approach is necessary.
|
||||
if bugzilla[-1] == "/":
|
||||
authenticate_on = '"' + bugzilla[:-1] + '"'
|
||||
else:
|
||||
authenticate_on = '"' + bugzilla + '/"'
|
||||
ret = credentials.authenticators(authenticate_on)
|
||||
if not ret:
|
||||
# Apparently, an invalid machine URL will cause credentials == None
|
||||
error("no credentials for Bugzilla instance at %s" % bugzilla)
|
||||
return ret
|
||||
|
||||
def process_options(options):
|
||||
data = {}
|
||||
# Initialize bug report fields from message on standard input
|
||||
if options.read:
|
||||
message_parser = email.Parser.Parser()
|
||||
message = message_parser.parse(sys.stdin)
|
||||
for (key, value) in message.items():
|
||||
data.update({header_to_field(key) : value})
|
||||
if not 'comment' in data:
|
||||
data['comment'] = message.get_payload()
|
||||
|
||||
# Merge in options from the command line; they override what's on stdin.
|
||||
for (key, value) in options.__dict__.items():
|
||||
if key != 'read' and value != None:
|
||||
data[key] = value
|
||||
return data
|
||||
|
||||
def ensure_defaults(data):
|
||||
# Provide some defaults if the user did not supply them.
|
||||
if 'op_sys' not in data:
|
||||
if sys.platform.startswith('linux'):
|
||||
data['op_sys'] = 'Linux'
|
||||
if 'rep_platform' not in data:
|
||||
data['rep_platform'] = 'PC'
|
||||
if 'bug_status' not in data:
|
||||
data['bug_status'] = 'NEW'
|
||||
if 'bug_severity' not in data:
|
||||
data['bug_severity'] = 'normal'
|
||||
if 'bug_file_loc' not in data:
|
||||
data['bug_file_loc'] = 'http://' # Yes, Bugzilla needs this
|
||||
if 'priority' not in data:
|
||||
data['priority'] = 'P2'
|
||||
|
||||
def validate_fields(data):
|
||||
# Fields for validation
|
||||
required_fields = (
|
||||
"bug_status", "bug_file_loc", "product", "version", "component",
|
||||
"short_desc", "rep_platform", "op_sys", "priority", "bug_severity",
|
||||
"comment",
|
||||
)
|
||||
legal_fields = required_fields + (
|
||||
"assigned_to", "cc", "keywords", "dependson", "blocked",
|
||||
)
|
||||
my_fields = data.keys()
|
||||
for field in my_fields:
|
||||
if field not in legal_fields:
|
||||
error("invalid field: %s" % field_to_header(field))
|
||||
for field in required_fields:
|
||||
if field not in my_fields:
|
||||
error("required field missing: %s" % field_to_header(field))
|
||||
|
||||
if not data['short_desc']:
|
||||
error("summary for bug submission must not be empty")
|
||||
|
||||
if not data['comment']:
|
||||
error("comment for bug submission must not be empty")
|
||||
|
||||
#
|
||||
# POST-specific functions
|
||||
#
|
||||
|
||||
def submit_bug_POST(bugzilla, data):
|
||||
# Move the request over the wire
|
||||
postdata = urllib.urlencode(data)
|
||||
try:
|
||||
url = ErrorURLopener().open("%s/post_bug.cgi" % bugzilla, postdata)
|
||||
except ValueError:
|
||||
error("Bugzilla site at %s not found (HTTP returned 404)" % bugzilla)
|
||||
ret = url.read()
|
||||
check_result_POST(ret, data)
|
||||
|
||||
def check_result_POST(ret, data):
|
||||
# XXX We can move pre-validation out of here as soon as we pick up
|
||||
# the valid options from config.cgi -- it will become a simple
|
||||
# assertion and ID-grabbing step.
|
||||
#
|
||||
# XXX: We use get() here which may return None, but since the user
|
||||
# might not have provided these options, we don't want to die on
|
||||
# them.
|
||||
version = data.get('version')
|
||||
product = data.get('product')
|
||||
component = data.get('component')
|
||||
priority = data.get('priority')
|
||||
severity = data.get('bug_severity')
|
||||
status = data.get('bug_status')
|
||||
assignee = data.get('assigned_to')
|
||||
platform = data.get('rep_platform')
|
||||
opsys = data.get('op_sys')
|
||||
keywords = data.get('keywords')
|
||||
deps = data.get('dependson', '') + " " + data.get('blocked', '')
|
||||
deps = deps.replace(" ", ", ")
|
||||
# XXX: we should really not be using plain find() here, as it can
|
||||
# match the bug content inadvertedly
|
||||
if ret.find("A legal Version was not") != -1:
|
||||
error("version %r does not exist for component %s:%s" %
|
||||
(version, product, component))
|
||||
if ret.find("A legal Priority was not") != -1:
|
||||
error("priority %r does not exist in "
|
||||
"this Bugzilla instance" % priority)
|
||||
if ret.find("A legal Severity was not") != -1:
|
||||
error("severity %r does not exist in "
|
||||
"this Bugzilla instance" % severity)
|
||||
if ret.find("A legal Status was not") != -1:
|
||||
error("status %r is not a valid creation status in "
|
||||
"this Bugzilla instance" % status)
|
||||
if ret.find("A legal Platform was not") != -1:
|
||||
error("platform %r is not a valid platform in "
|
||||
"this Bugzilla instance" % platform)
|
||||
if ret.find("A legal OS/Version was not") != -1:
|
||||
error("%r is not a valid OS in "
|
||||
"this Bugzilla instance" % opsys)
|
||||
if ret.find("Invalid Username") != -1:
|
||||
error("invalid credentials submitted")
|
||||
if ret.find("Component Needed") != -1:
|
||||
error("the component %r does not exist in "
|
||||
"this Bugzilla instance" % component)
|
||||
if ret.find("Unknown Keyword") != -1:
|
||||
error("keyword(s) %r not registered in "
|
||||
"this Bugzilla instance" % keywords)
|
||||
if ret.find("The product name") != -1:
|
||||
error("product %r does not exist in this "
|
||||
"Bugzilla instance" % product)
|
||||
# XXX: this should be smarter
|
||||
if ret.find("does not exist") != -1:
|
||||
error("could not mark dependencies for bugs %s: one or "
|
||||
"more bugs didn't exist in this Bugzilla instance" % deps)
|
||||
if ret.find("Match Failed") != -1:
|
||||
# XXX: invalid CC hits on this error too
|
||||
error("the bug assignee %r isn't registered in "
|
||||
"this Bugzilla instance" % assignee)
|
||||
# If all is well, return bug number posted
|
||||
if ret.find("process_bug.cgi") == -1:
|
||||
error("could not post bug to %s: are you sure this "
|
||||
"is Bugzilla instance's top-level directory?" % bugzilla)
|
||||
m = re.search("Bug ([0-9]+) Submitted", ret)
|
||||
if not m:
|
||||
print ret
|
||||
error("Internal error: bug id not found; please report a bug")
|
||||
id = m.group(1)
|
||||
print "Bug %s posted." % id
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = setup_parser()
|
||||
|
||||
# Parser will print help and exit here if we specified --help
|
||||
(options, args) = parser.parse_args()
|
||||
|
||||
if len(args) != 1:
|
||||
parser.error("missing Bugzilla host URL")
|
||||
|
||||
bugzilla = args[0]
|
||||
data = process_options(options)
|
||||
|
||||
login, account, password = get_credentials(bugzilla)
|
||||
if "@" not in login: # no use even trying to submit
|
||||
error("login %r is invalid (it should be an email address)" % login)
|
||||
|
||||
ensure_defaults(data)
|
||||
validate_fields(data)
|
||||
|
||||
# Attach authentication information
|
||||
data.update({'Bugzilla_login' : login,
|
||||
'Bugzilla_password' : password,
|
||||
'GoAheadAndLogIn' : 1,
|
||||
'form_name' : 'enter_bug'})
|
||||
|
||||
submit_bug_POST(bugzilla, data)
|
||||
|
||||
@@ -1,221 +0,0 @@
|
||||
<?xml version="1.0" encoding="ISO-8859-1"?>
|
||||
<!DOCTYPE refentry PUBLIC
|
||||
"-//OASIS//DTD DocBook XML V4.1.2//EN"
|
||||
"docbook/docbookx.dtd">
|
||||
<refentry id='bugzilla-submit.1'>
|
||||
<refmeta>
|
||||
<refentrytitle>bugzilla-submit</refentrytitle>
|
||||
<manvolnum>1</manvolnum>
|
||||
<refmiscinfo class='date'>Oct 30, 2003</refmiscinfo>
|
||||
</refmeta>
|
||||
<refnamediv id='name'>
|
||||
<refname>bugzilla-submit</refname>
|
||||
<refpurpose>post bugs to a Bugzilla instance</refpurpose>
|
||||
</refnamediv>
|
||||
<refsynopsisdiv id='synopsis'>
|
||||
|
||||
<cmdsynopsis>
|
||||
<command>bugzilla-submit</command>
|
||||
<arg choice='opt'>--status <replaceable>bug_status</replaceable></arg>
|
||||
<arg choice='opt'>--url <replaceable>bug_file_loc</replaceable></arg>
|
||||
<arg choice='opt'>--product <replaceable>product</replaceable></arg>
|
||||
<arg choice='opt'>--version <replaceable>version</replaceable></arg>
|
||||
<arg choice='opt'>--component <replaceable>component</replaceable></arg>
|
||||
<arg choice='opt'>--summary <replaceable>short_desc</replaceable></arg>
|
||||
<arg choice='opt'>--hardware <replaceable>rep_platform</replaceable></arg>
|
||||
<arg choice='opt'>--os <replaceable>op_sys</replaceable></arg>
|
||||
<arg choice='opt'>--priority <replaceable>priority</replaceable></arg>
|
||||
<arg choice='opt'>--severity <replaceable>bug_severity</replaceable></arg>
|
||||
<arg choice='opt'>--assigned-to <replaceable>assigned-to</replaceable></arg>
|
||||
<arg choice='opt'>--cc <replaceable>cc</replaceable></arg>
|
||||
<arg choice='opt'>--keywords <replaceable>keywords</replaceable></arg>
|
||||
<arg choice='opt'>--depends-on <replaceable>dependson</replaceable></arg>
|
||||
<arg choice='opt'>--blocked <replaceable>blocked</replaceable></arg>
|
||||
<arg choice='opt'>--description <replaceable>comment</replaceable></arg>
|
||||
<arg choice='opt'>--no-stdin </arg>
|
||||
<arg choice='plain'><replaceable>bugzilla-url</replaceable></arg>
|
||||
</cmdsynopsis>
|
||||
|
||||
</refsynopsisdiv>
|
||||
|
||||
<refsect1 id='description'><title>DESCRIPTION</title>
|
||||
|
||||
<para><application>bugzilla-submit</application> is a command-line tool
|
||||
for posting bug reports to any instance of Bugzilla. It accepts on
|
||||
standard input text resembling an RFC-822 message. The headers of
|
||||
that message, and its body, are used to set error-report field values.
|
||||
More field values are merged in from command-line options. If required
|
||||
fields have not been set, <application>bugzilla-submit</application>
|
||||
tries to compute them. Finally, the resulting error report is
|
||||
validated. If all required fields are present, and there are no
|
||||
illegal fields or values, the report is shipped off to the Mozilla
|
||||
instance specified by the single positional argument. Login/password
|
||||
credentials are read from the calling user's <filename>~/.netrc</filename>
|
||||
file.</para>
|
||||
|
||||
<para>bugzilla-submit accepts a single argument:
|
||||
<replaceable>bugzilla-url</replaceable>. Its value must match the
|
||||
relevant Bugzilla instance's base URL (technically, its
|
||||
<replaceable>urlbase</replaceable> param). The program also accepts the
|
||||
following options to set or override fields:</para>
|
||||
<variablelist>
|
||||
<varlistentry>
|
||||
<term>-b, --status</term>
|
||||
<listitem>
|
||||
<para>Set the bug_status field, overriding the Status header from
|
||||
standard input if present. (The stock Bugzilla web presentation
|
||||
identifies this field as <quote>Status</quote>.)</para>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
<varlistentry>
|
||||
<term>-u, --url</term>
|
||||
<listitem>
|
||||
<para>Set the bug_file_loc field, overriding the URL header from
|
||||
standard input if present. (The stock Bugzilla web presentation
|
||||
identifies this field as <quote>URL</quote>.)</para>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
<varlistentry>
|
||||
<term>-p, --product</term>
|
||||
<listitem>
|
||||
<para>Set the product field, overriding the Product header from
|
||||
standard input if necessary.</para>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
<varlistentry>
|
||||
<term>-v, --version</term>
|
||||
<listitem><para>Set the version field, overriding the Version header
|
||||
from standard input if necessary.</para></listitem>
|
||||
</varlistentry>
|
||||
<varlistentry>
|
||||
<term>-c, --component</term>
|
||||
<listitem><para>Set the component field, overriding the Component header
|
||||
from standard input if necessary.</para></listitem>
|
||||
</varlistentry>
|
||||
<varlistentry>
|
||||
<term>-s, --summary</term>
|
||||
<listitem><para>Set the short_desc field, overriding the Summary header
|
||||
from standard input if necessary. (The stock Bugzilla web presentation
|
||||
identifies this field as <quote>Summary</quote>.)</para></listitem>
|
||||
</varlistentry>
|
||||
<varlistentry>
|
||||
<term>-H, --hardware</term>
|
||||
<listitem><para>Set the rep_platform field, overriding the Hardware header
|
||||
from standard input if necessary. (The stock Bugzilla web presentation
|
||||
identifies this field as <quote>Hardware</quote>.)</para></listitem>
|
||||
</varlistentry>
|
||||
<varlistentry>
|
||||
<term>-o, --os</term>
|
||||
<listitem><para>Set the op_sys field, overriding the OS (Operating
|
||||
System) header from standard input if necessary. (The stock Bugzilla web
|
||||
presentation also identifies this field as
|
||||
<quote>OS</quote>.)</para></listitem>
|
||||
</varlistentry>
|
||||
<varlistentry>
|
||||
<term>-r, --priority</term>
|
||||
<listitem><para>Set the priority field, overriding the Priority header
|
||||
from standard input if necessary.</para></listitem>
|
||||
</varlistentry>
|
||||
<varlistentry>
|
||||
<term>-x, --severity</term>
|
||||
<listitem><para>Set the severity field, overriding the Severity header
|
||||
from standard input if necessary.</para></listitem>
|
||||
</varlistentry>
|
||||
<varlistentry>
|
||||
<term>-d, --description</term>
|
||||
<listitem><para>Set the comment field, overriding the Description header
|
||||
from standard input if necessary. (The stock Bugzilla web presentation
|
||||
identifies this field as <quote>Description</quote>.) If there is a
|
||||
message body and no Description field and this option is not
|
||||
specified, the message body is used as a description.
|
||||
</para></listitem>
|
||||
</varlistentry>
|
||||
<varlistentry>
|
||||
<term>-a, --assigned-to</term>
|
||||
<listitem>
|
||||
<para>Set the optional assigned_to field, overriding the Assigned-To
|
||||
header from standard input if necessary.</para>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
<varlistentry>
|
||||
<term>-C, --cc</term>
|
||||
<listitem>
|
||||
<para>Set the optional cc field, overriding the Cc
|
||||
header from standard input if necessary.</para>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
<varlistentry>
|
||||
<term>-k, --keywords</term>
|
||||
<listitem>
|
||||
<para>Set the optional keywords field, overriding the Keywords
|
||||
header from standard input if necessary.</para>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
<varlistentry>
|
||||
<term>-D, --depends-on</term>
|
||||
<listitem>
|
||||
<para>Set the optional dependson field, overriding the Depends-On
|
||||
header from standard input if necessary.</para>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
<varlistentry>
|
||||
<term>-B, --assigned-to</term>
|
||||
<listitem>
|
||||
<para>Set the optional blocked field, overriding the Blocked
|
||||
header from standard input if necessary.</para>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
<varlistentry>
|
||||
<term>-n, --no-stdin</term>
|
||||
<listitem><para>Suppress reading fields from standard input.</para></listitem>
|
||||
</varlistentry>
|
||||
<varlistentry>
|
||||
<term>-h, --help</term>
|
||||
<listitem><para>Print usage help and exit.</para></listitem>
|
||||
</varlistentry>
|
||||
</variablelist>
|
||||
|
||||
<para>This program will try to deduce OS and Hardware if those are not
|
||||
specified. If it fails, validation will fail before shipping the
|
||||
report.</para>
|
||||
|
||||
<para>There is expected to be a single positional argument following
|
||||
any options. It should be the URL of the Bugzilla instance to which
|
||||
the bug is to be submitted.</para>
|
||||
|
||||
</refsect1>
|
||||
<refsect1 id='files'><title>FILES</title>
|
||||
<variablelist>
|
||||
<varlistentry>
|
||||
<term><filename>~/.netrc</filename></term>
|
||||
<listitem><para>Must contain an entry in which the machine field is
|
||||
the Bugzilla instance URL, the login field is your ID on that host, and the
|
||||
password field is the right password. The URL in the machine field
|
||||
must be enclosed in double quotes.</para>
|
||||
|
||||
<para>For example, if your Bugzilla instance is at
|
||||
"http://landfill.bugzilla.org/bztest/", and your login and password
|
||||
there are "john@doe.com" and "foo", respectively, your
|
||||
<filename>.netrc</filename> entry should look something like:</para>
|
||||
|
||||
<screen>
|
||||
machine "http://landfill.bugzilla.org/bztest/"
|
||||
login john@doe.com
|
||||
password foo
|
||||
|
||||
</screen>
|
||||
|
||||
<para>Note that the machine entry should match exactly the instance URL
|
||||
specified to <application>bugzilla-submit</application>.</para>
|
||||
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
</variablelist>
|
||||
|
||||
</refsect1>
|
||||
<refsect1 id='author'><title>AUTHORS</title>
|
||||
<para>Christian Reis <kiko@async.com.br>, Eric S. Raymond
|
||||
<esr@thyrsus.com>.</para>
|
||||
</refsect1>
|
||||
</refentry>
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
:0 fhw
|
||||
| formail -I "From " -a "From "
|
||||
|
||||
BUGZILLA_HOME=/home/bugzilla/WEB/bugzilla/contrib
|
||||
|
||||
:0
|
||||
* ^Subject: .*\[Bug .*\]
|
||||
RESULT=|(cd $BUGZILLA_HOME && ./bugzilla_email_append.pl)
|
||||
|
||||
|
||||
# Feed mail to stdin of bug_email.pl
|
||||
:0 Ec
|
||||
#* !^Subject: .*[Bug .*]
|
||||
RESULT=|(cd $BUGZILLA_HOME && ./bug_email.pl )
|
||||
|
||||
# write result to a logfile
|
||||
:0 c
|
||||
|echo `date '+%d.%m.%y %H:%M: '` $RESULT >> $HOME/bug_email.log
|
||||
|
||||
|
||||
:0 c
|
||||
|echo "----------------------------------" >> $HOME/bug_email.log
|
||||
|
||||
:0 c
|
||||
$HOME/bug_email.log
|
||||
|
||||
# Move mail to the inbox
|
||||
:0
|
||||
$HOME/Mail/INBOX
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
#!/usr/bin/perl -w
|
||||
# -*- 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 purpose of this script is to take an email message, which
|
||||
# specifies a bugid and append it to the bug as part of the longdesc
|
||||
# table
|
||||
|
||||
# Contributor : Seth M. Landsman <seth@dworkin.net>
|
||||
|
||||
# 03/15/00 : Initial version by SML
|
||||
# 03/15/00 : processmail gets called
|
||||
|
||||
# Email subject must be of format :
|
||||
# .* Bug ### .*
|
||||
# replying to a typical bugzilla email should be valid
|
||||
|
||||
# TODO :
|
||||
# 1. better way to get the body text (I don't know what dump_entity() is
|
||||
# actually doing
|
||||
|
||||
use strict;
|
||||
use MIME::Parser;
|
||||
|
||||
BEGIN {
|
||||
chdir ".."; # this script lives in contrib, change to main
|
||||
push @INC, "contrib";
|
||||
push @INC, "."; # this script lives in contrib
|
||||
}
|
||||
|
||||
require "globals.pl";
|
||||
use BugzillaEmail;
|
||||
use Bugzilla::Config qw(:DEFAULT $datadir);
|
||||
use Bugzilla::BugMail;
|
||||
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
# Create a new MIME parser:
|
||||
my $parser = new MIME::Parser;
|
||||
|
||||
my $Comment = "";
|
||||
|
||||
# Create and set the output directory:
|
||||
# FIXME: There should be a $BUGZILLA_HOME variable (SML)
|
||||
(-d "$datadir/mimedump-tmp") or mkdir "$datadir/mimedump-tmp",0755 or die "mkdir: $!";
|
||||
(-w "$datadir/mimedump-tmp") or die "can't write to directory";
|
||||
|
||||
$parser->output_dir("$datadir/mimedump-tmp");
|
||||
|
||||
# Read the MIME message:
|
||||
my $entity = $parser->read(\*STDIN) or die "couldn't parse MIME stream";
|
||||
$entity->remove_sig(10); # Removes the signature in the last 10 lines
|
||||
|
||||
# Getting values from parsed mail
|
||||
my $Sender = $entity->get( 'From' );
|
||||
$Sender ||= $entity->get( 'Reply-To' );
|
||||
my $Message_ID = $entity->get( 'Message-Id' );
|
||||
|
||||
die (" *** Can't find Sender-address in sent mail ! ***\n" ) unless defined( $Sender );
|
||||
chomp( $Sender );
|
||||
chomp( $Message_ID );
|
||||
|
||||
print "Dealing with the sender $Sender\n";
|
||||
|
||||
my $SenderShort = $Sender;
|
||||
$SenderShort =~ s/^.*?([a-zA-Z0-9_.-]+?\@[a-zA-Z0-9_.-]+\.[a-zA-Z0-9_.-]+).*$/$1/;
|
||||
|
||||
$SenderShort = findUser($SenderShort);
|
||||
|
||||
print "SenderShort is $SenderShort\n";
|
||||
if (!defined($SenderShort)) {
|
||||
$SenderShort = $Sender;
|
||||
$SenderShort =~ s/^.*?([a-zA-Z0-9_.-]+?\@[a-zA-Z0-9_.-]+\.[a-zA-Z0-9_.-]+).*$/$1/;
|
||||
}
|
||||
print "The sendershort is now $SenderShort\n";
|
||||
|
||||
if (!defined($SenderShort)) {
|
||||
DealWithError("No such user $SenderShort exists.");
|
||||
}
|
||||
|
||||
my $Subject = $entity->get('Subject');
|
||||
print "The subject is $Subject\n";
|
||||
|
||||
my ($bugid) = ($Subject =~ /\[Bug ([\d]+)\]/);
|
||||
print "The bugid is $bugid\n";
|
||||
|
||||
# make sure the bug exists
|
||||
|
||||
SendSQL("SELECT bug_id FROM bugs WHERE bug_id = $bugid;");
|
||||
my $found_id = FetchOneColumn();
|
||||
print "Did we find the bug? $found_id-\n";
|
||||
if (!defined($found_id)) {
|
||||
DealWithError("Bug $bugid does not exist");
|
||||
}
|
||||
|
||||
# get the user id
|
||||
SendSQL("SELECT userid FROM profiles WHERE " .
|
||||
$dbh->sql_istrcmp('login_name', $dbh->quote($SenderShort)));
|
||||
my $userid = FetchOneColumn();
|
||||
if (!defined($userid)) {
|
||||
DealWithError("Userid not found for $SenderShort");
|
||||
}
|
||||
|
||||
# parse out the text of the message
|
||||
dump_entity($entity);
|
||||
|
||||
# Get rid of the bug id
|
||||
$Subject =~ s/\[Bug [\d]+\]//;
|
||||
#my $Comment = "This is only a test ...";
|
||||
my $Body = "Subject: " . $Subject . "\n" . $Comment;
|
||||
|
||||
# shove it in the table
|
||||
my $long_desc_query = "INSERT INTO longdescs SET bug_id=$found_id, who=$userid, bug_when=NOW(), thetext=" . SqlQuote($Body) . ";";
|
||||
SendSQL($long_desc_query);
|
||||
|
||||
Bugzilla::BugMail::Send( $found_id, { changer => $SenderShort } );
|
||||
|
||||
sub DealWithError {
|
||||
my ($reason) = @_;
|
||||
print $reason . "\n";
|
||||
exit 100;
|
||||
}
|
||||
|
||||
# Yanking this wholesale from bug_email, 'cause I know this works. I'll
|
||||
# figure out what it really does later
|
||||
#------------------------------
|
||||
#
|
||||
# dump_entity ENTITY, NAME
|
||||
#
|
||||
# Recursive routine for parsing a mime coded mail.
|
||||
# One mail may contain more than one mime blocks, which need to be
|
||||
# handled. Therefore, this function is called recursively.
|
||||
#
|
||||
# It gets the for bugzilla important information from the mailbody and
|
||||
# stores them into the global attachment-list @attachments. The attachment-list
|
||||
# is needed in storeAttachments.
|
||||
#
|
||||
sub dump_entity {
|
||||
my ($entity, $name) = @_;
|
||||
defined($name) or $name = "'anonymous'";
|
||||
my $IO;
|
||||
|
||||
|
||||
# Output the body:
|
||||
my @parts = $entity->parts;
|
||||
if (@parts) { # multipart...
|
||||
my $i;
|
||||
foreach $i (0 .. $#parts) { # dump each part...
|
||||
dump_entity($parts[$i], ("$name, part ".(1+$i)));
|
||||
}
|
||||
} else { # single part...
|
||||
|
||||
# Get MIME type, and display accordingly...
|
||||
my $msg_part = $entity->head->get( 'Content-Disposition' );
|
||||
|
||||
$msg_part ||= "";
|
||||
|
||||
my ($type, $subtype) = split('/', $entity->head->mime_type);
|
||||
my $body = $entity->bodyhandle;
|
||||
my ($data, $on_disk );
|
||||
|
||||
if( $msg_part =~ /^attachment/ ) {
|
||||
# Attached File
|
||||
my $des = $entity->head->get('Content-Description');
|
||||
$des ||= "";
|
||||
|
||||
if( defined( $body->path )) { # Data is on disk
|
||||
$on_disk = 1;
|
||||
$data = $body->path;
|
||||
|
||||
} else { # Data is in core
|
||||
$on_disk = 0;
|
||||
$data = $body->as_string;
|
||||
}
|
||||
# push ( @attachments, [ $data, $entity->head->mime_type, $on_disk, $des ] );
|
||||
} else {
|
||||
# Real Message
|
||||
if ($type =~ /^(text|message)$/) { # text: display it...
|
||||
if ($IO = $body->open("r")) {
|
||||
$Comment .= $_ while (defined($_ = $IO->getline));
|
||||
$IO->close;
|
||||
} else { # d'oh!
|
||||
print "$0: couldn't find/open '$name': $!";
|
||||
}
|
||||
} else { print "Oooops - no Body !\n"; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
#!/usr/local/bin/ruby
|
||||
|
||||
# Queries an LDAP server for all email addresses (tested against Exchange 5.5),
|
||||
# and makes nice bugzilla user entries out of them. Also disables Bugzilla users
|
||||
# that are not found in LDAP.
|
||||
|
||||
# $Id: bugzilla_ldapsync.rb,v 1.2 2003-04-26 16:35:04 jake%bugzilla.org Exp $
|
||||
|
||||
require 'ldap'
|
||||
require 'dbi'
|
||||
require 'getoptlong'
|
||||
|
||||
opts = GetoptLong.new(
|
||||
['--dbname', '-d', GetoptLong::OPTIONAL_ARGUMENT],
|
||||
['--dbpassword', '-p', GetoptLong::OPTIONAL_ARGUMENT],
|
||||
['--dbuser', '-u', GetoptLong::OPTIONAL_ARGUMENT],
|
||||
['--dbpassfile', '-P', GetoptLong::OPTIONAL_ARGUMENT],
|
||||
['--ldaphost', '-h', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--ldapbase', '-b', GetoptLong::OPTIONAL_ARGUMENT],
|
||||
['--ldapquery', '-q', GetoptLong::OPTIONAL_ARGUMENT],
|
||||
['--maildomain', '-m', GetoptLong::OPTIONAL_ARGUMENT],
|
||||
['--noremove', '-n', GetoptLong::OPTIONAL_ARGUMENT],
|
||||
['--defaultpass', '-D', GetoptLong::OPTIONAL_ARGUMENT],
|
||||
['--checkmode', '-c', GetoptLong::OPTIONAL_ARGUMENT]
|
||||
)
|
||||
|
||||
|
||||
# in hash to make it easy
|
||||
optHash = Hash.new
|
||||
opts.each do |opt, arg|
|
||||
optHash[opt]=arg
|
||||
end
|
||||
|
||||
# grab password from file if it's an option
|
||||
if optHash['--dbpassfile']
|
||||
dbPassword=File.open(optHash['--dbpassfile'], 'r').readlines[0].chomp!
|
||||
else
|
||||
dbPassword=optHash['--dbpassword'] || nil
|
||||
end
|
||||
|
||||
# make bad assumptions.
|
||||
dbName = optHash['--dbname'] || 'bugzilla'
|
||||
dbUser = optHash['--dbuser'] || 'bugzilla'
|
||||
ldapHost = optHash['--ldaphost'] || 'ldap'
|
||||
ldapBase = optHash['--ldapbase'] || ''
|
||||
mailDomain = optHash['--maildomain'] || `domainname`.chomp!
|
||||
ldapQuery = optHash['--ldapquery'] || "(&(objectclass=person)(rfc822Mailbox=*@#{mailDomain}))"
|
||||
checkMode = optHash['--checkmode'] || nil
|
||||
noRemove = optHash['--noremove'] || nil
|
||||
defaultPass = optHash['--defaultpass'] || 'bugzilla'
|
||||
|
||||
if (! dbPassword)
|
||||
puts "bugzilla_ldapsync v1.3 (c) 2003 Thomas Stromberg <thomas+bugzilla@stromberg.org>"
|
||||
puts ""
|
||||
puts " -d | --dbname name of MySQL database [#{dbName}]"
|
||||
puts " -u | --dbuser username for MySQL database [#{dbUser}]"
|
||||
puts " -p | --dbpassword password for MySQL user [#{dbPassword}]"
|
||||
puts " -P | --dbpassfile filename containing password for MySQL user"
|
||||
puts " -h | --ldaphost hostname for LDAP server [#{ldapHost}]"
|
||||
puts " -b | --ldapbase Base of LDAP query, for instance, o=Bugzilla.com"
|
||||
puts " -q | --ldapquery LDAP query, uses maildomain [#{ldapQuery}]"
|
||||
puts " -m | --maildomain e-mail domain to use records from"
|
||||
puts " -n | --noremove do not remove Bugzilla users that are not in LDAP"
|
||||
puts " -c | --checkmode checkmode, does not perform any SQL changes"
|
||||
puts " -D | --defaultpass default password for new users [#{defaultPass}]"
|
||||
puts
|
||||
puts "example:"
|
||||
puts
|
||||
puts " bugzilla_ldapsync.rb -c -u taskzilla -P /tmp/test -d taskzilla -h bhncmail -m \"bowebellhowell.com\""
|
||||
exit
|
||||
end
|
||||
|
||||
|
||||
if (checkMode)
|
||||
puts '(checkmode enabled, no SQL writes will actually happen)'
|
||||
puts "ldapquery is #{ldapQuery}"
|
||||
puts
|
||||
end
|
||||
|
||||
|
||||
bugzillaUsers = Hash.new
|
||||
ldapUsers = Hash.new
|
||||
encPassword = defaultPass.crypt('xx')
|
||||
sqlNewUser = "INSERT INTO profiles VALUES ('', ?, '#{encPassword}', ?, '', 1, NULL, '0000-00-00 00:00:00');"
|
||||
|
||||
# presumes that the MySQL database is local.
|
||||
dbh = DBI.connect("DBI:Mysql:#{dbName}", dbUser, dbPassword)
|
||||
|
||||
# select all e-mail addresses where there is no disabledtext defined. Only valid users, please!
|
||||
dbh.select_all('select login_name, realname, disabledtext from profiles') { |row|
|
||||
login = row[0].downcase
|
||||
bugzillaUsers[login] = Hash.new
|
||||
bugzillaUsers[login]['desc'] = row[1]
|
||||
bugzillaUsers[login]['disabled'] = row[2]
|
||||
#puts "bugzilla has #{login} - \"#{bugzillaUsers[login]['desc']}\" (#{bugzillaUsers[login]['disabled']})"
|
||||
}
|
||||
|
||||
|
||||
LDAP::Conn.new(ldapHost, 389).bind{|conn|
|
||||
sub = nil
|
||||
# perform the query, but only get the e-mail address, location, and name returned to us.
|
||||
conn.search(ldapBase, LDAP::LDAP_SCOPE_SUBTREE, ldapQuery,
|
||||
['rfc822Mailbox', 'physicalDeliveryOfficeName', 'cn']) { |entry|
|
||||
|
||||
# Get the users first (primary) e-mail address, but I only want what's before the @ sign.
|
||||
entry.vals("rfc822Mailbox")[0] =~ /([\w\.-]+)\@/
|
||||
email = $1
|
||||
|
||||
# We put the officename in the users description, and nothing otherwise.
|
||||
if entry.vals("physicalDeliveryOfficeName")
|
||||
location = entry.vals("physicalDeliveryOfficeName")[0]
|
||||
else
|
||||
location = ''
|
||||
end
|
||||
|
||||
# for whatever reason, we get blank entries. Do some double checking here.
|
||||
if (email && (email.length > 4) && (location !~ /Generic/) && (entry.vals("cn")))
|
||||
if (location.length > 2)
|
||||
desc = entry.vals("cn")[0] + " (" + location + ")"
|
||||
else
|
||||
desc = entry.vals("cn")[0]
|
||||
end
|
||||
|
||||
# take care of the whitespace.
|
||||
desc.sub!("\s+$", "")
|
||||
desc.sub!("^\s+", "")
|
||||
|
||||
# dumb hack. should be properly escaped, and apostrophes should never ever ever be in email.
|
||||
email.sub!("\'", "%")
|
||||
email.sub!('%', "\'")
|
||||
email=email.downcase
|
||||
ldapUsers[email.downcase] = Hash.new
|
||||
ldapUsers[email.downcase]['desc'] = desc
|
||||
ldapUsers[email.downcase]['disabled'] = nil
|
||||
#puts "ldap has #{email} - #{ldapUsers[email.downcase]['desc']}"
|
||||
end
|
||||
}
|
||||
}
|
||||
|
||||
# This is the loop that takes the users that we found in Bugzilla originally, and
|
||||
# checks to see if they are still in the LDAP server. If they are not, away they go!
|
||||
|
||||
ldapUsers.each_key { |user|
|
||||
# user does not exist at all.
|
||||
#puts "checking ldap user #{user}"
|
||||
if (! bugzillaUsers[user])
|
||||
puts "+ Adding #{user} - #{ldapUsers[user]['desc']}"
|
||||
|
||||
if (! checkMode)
|
||||
dbh.do(sqlNewUser, user, ldapUsers[user]['desc'])
|
||||
end
|
||||
|
||||
# short-circuit now.
|
||||
next
|
||||
end
|
||||
|
||||
if (bugzillaUsers[user]['desc'] != ldapUsers[user]['desc'])
|
||||
puts "* Changing #{user} from \"#{bugzillaUsers[user]['desc']}\" to \"#{ldapUsers[user]['desc']}\""
|
||||
if (! checkMode)
|
||||
# not efficient.
|
||||
dbh.do("UPDATE profiles SET realname = ? WHERE login_name = ?", ldapUsers[user]['desc'], user)
|
||||
end
|
||||
end
|
||||
|
||||
if (bugzillaUsers[user]['disabled'].length > 0)
|
||||
puts "+ Enabling #{user} (was \"#{bugzillaUsers[user]['disabled']}\")"
|
||||
if (! checkMode)
|
||||
dbh.do("UPDATE profiles SET disabledtext = NULL WHERE login_name=\"#{user}\"")
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
if (! noRemove)
|
||||
bugzillaUsers.each_key { |user|
|
||||
if ((bugzillaUsers[user]['disabled'].length < 1) && (! ldapUsers[user]))
|
||||
puts "- Disabling #{user} (#{bugzillaUsers[user]['disabled']})"
|
||||
|
||||
if (! checkMode)
|
||||
dbh.do("UPDATE profiles SET disabledtext = \'auto-disabled by ldap sync\' WHERE login_name=\"#{user}\"")
|
||||
end
|
||||
end
|
||||
}
|
||||
end
|
||||
dbh.disconnect
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
#!/usr/bin/perl -w
|
||||
#
|
||||
# bzdbcopy.pl - Copies data from one Bugzilla database to another.
|
||||
#
|
||||
# Author: Max Kanat-Alexander <mkanat@bugzilla.org>
|
||||
#
|
||||
# The intended use of this script is to copy data from an installation
|
||||
# running on one DB platform to an installation running on another
|
||||
# DB platform.
|
||||
#
|
||||
# It must be run from the directory containing your Bugzilla installation.
|
||||
# That means if this script is in the contrib/ directory, you should
|
||||
# be running it as: ./contrib/bzdbcopy.pl
|
||||
#
|
||||
# Note: Both schemas must already exist and be IDENTICAL. (That is,
|
||||
# they must have both been created/updated by the same version of
|
||||
# checksetup.pl.) This script will DESTROY ALL CURRENT DATA in the
|
||||
# target database.
|
||||
#
|
||||
# Both Schemas must be at least from Bugzilla 2.19.3, but if you're
|
||||
# running a Bugzilla from before 2.20rc2, you'll need the patch at:
|
||||
# https://bugzilla.mozilla.org/show_bug.cgi?id=300311 in order to
|
||||
# be able to run this script.
|
||||
#
|
||||
# Before you using it, you have to correctly set all the variables
|
||||
# in the "User-Configurable Settings" section, below. The "SOURCE"
|
||||
# settings are for the database you're copying from, and the "TARGET"
|
||||
# settings are for the database you're copying to. The DB_TYPE is
|
||||
# the name of a DB driver from the Bugzilla/DB/ directory.
|
||||
#
|
||||
|
||||
use strict;
|
||||
use lib ".";
|
||||
use Bugzilla::DB;
|
||||
use Bugzilla::Util;
|
||||
|
||||
#####################################################################
|
||||
# User-Configurable Settings
|
||||
#####################################################################
|
||||
|
||||
# Settings for the 'Source' DB that you are copying from.
|
||||
use constant SOURCE_DB_TYPE => 'Mysql';
|
||||
use constant SOURCE_DB_NAME => 'bugs';
|
||||
use constant SOURCE_DB_USER => 'bugs';
|
||||
use constant SOURCE_DB_PASSWORD => '';
|
||||
|
||||
# Settings for the 'Target' DB that you are copying to.
|
||||
use constant TARGET_DB_TYPE => 'Pg';
|
||||
use constant TARGET_DB_NAME => 'bugs';
|
||||
use constant TARGET_DB_USER => 'bugs';
|
||||
use constant TARGET_DB_PASSWORD => '';
|
||||
|
||||
#####################################################################
|
||||
# MAIN SCRIPT
|
||||
#####################################################################
|
||||
|
||||
print "Connecting to the '" . SOURCE_DB_NAME . "' source database on "
|
||||
. SOURCE_DB_TYPE . "...\n";
|
||||
my $source_db = Bugzilla::DB::_connect(SOURCE_DB_TYPE, 'localhost',
|
||||
SOURCE_DB_NAME, undef, undef, SOURCE_DB_USER, SOURCE_DB_PASSWORD);
|
||||
|
||||
print "Connecting to the '" . TARGET_DB_NAME . "' target database on "
|
||||
. TARGET_DB_TYPE . "...\n";
|
||||
my $target_db = Bugzilla::DB::_connect(TARGET_DB_TYPE, 'localhost',
|
||||
TARGET_DB_NAME, undef, undef, TARGET_DB_USER, TARGET_DB_PASSWORD);
|
||||
|
||||
# We use the table list from the target DB, because if somebody
|
||||
# has customized their source DB, we still want the script to work,
|
||||
# and it may otherwise fail in that situation (that is, the tables
|
||||
# may not exist in the target DB).
|
||||
my @table_list = $target_db->bz_table_list_real();
|
||||
|
||||
# We don't want to copy over the bz_schema table's contents.
|
||||
my $bz_schema_location = lsearch(\@table_list, 'bz_schema');
|
||||
splice(@table_list, $bz_schema_location, 1) if $bz_schema_location > 0;
|
||||
|
||||
# We turn off autocommit on the target DB, because we're doing so
|
||||
# much copying.
|
||||
$target_db->{AutoCommit} = 0;
|
||||
$target_db->{AutoCommit} == 0
|
||||
|| warn "Failed to disable autocommit on " . TARGET_DB_TYPE;
|
||||
foreach my $table (@table_list) {
|
||||
my @serial_cols;
|
||||
print "Reading data from the source '$table' table on "
|
||||
. SOURCE_DB_TYPE . "...\n";
|
||||
my @table_columns = $target_db->bz_table_columns_real($table);
|
||||
my $select_query = "SELECT " . join(',', @table_columns) . " FROM $table";
|
||||
my $data_in = $source_db->selectall_arrayref($select_query);
|
||||
|
||||
my $insert_query = "INSERT INTO $table ( " . join(',', @table_columns)
|
||||
. " ) VALUES (";
|
||||
$insert_query .= '?,' foreach (@table_columns);
|
||||
# Remove the last comma.
|
||||
chop($insert_query);
|
||||
$insert_query .= ")";
|
||||
my $insert_sth = $target_db->prepare($insert_query);
|
||||
|
||||
print "Clearing out the target '$table' table on "
|
||||
. TARGET_DB_TYPE . "...\n";
|
||||
$target_db->do("DELETE FROM $table");
|
||||
|
||||
print "Writing data to the target '$table' table on "
|
||||
. TARGET_DB_TYPE . "...";
|
||||
foreach my $row (@$data_in) {
|
||||
# Each column needs to be bound separately, because
|
||||
# many columns need to be dealt with specially.
|
||||
my $colnum = 0;
|
||||
foreach my $column (@table_columns) {
|
||||
# bind_param args start at 1, but arrays start at 0.
|
||||
my $param_num = $colnum + 1;
|
||||
my $already_bound;
|
||||
|
||||
# Certain types of columns need special handling.
|
||||
my $col_info = $source_db->bz_column_info($table, $column);
|
||||
if ($col_info && $col_info->{TYPE} eq 'LONGBLOB') {
|
||||
$insert_sth->bind_param($param_num,
|
||||
$row->[$colnum], $target_db->BLOB_TYPE);
|
||||
$already_bound = 1;
|
||||
}
|
||||
elsif ($col_info && $col_info->{TYPE} =~ /decimal/) {
|
||||
# In MySQL, decimal cols can be too long.
|
||||
my $col_type = $col_info->{TYPE};
|
||||
$col_type =~ /decimal\((\d+),(\d+)\)/;
|
||||
my ($precision, $decimals) = ($1, $2);
|
||||
# If it's longer than precision + decimal point
|
||||
if ( length($row->[$colnum]) > ($precision + 1) ) {
|
||||
# Truncate it to the highest allowed value.
|
||||
my $orig_value = $row->[$colnum];
|
||||
$row->[$colnum] = '';
|
||||
my $non_decimal = $precision - $decimals;
|
||||
$row->[$colnum] .= '9' while ($non_decimal--);
|
||||
$row->[$colnum] .= '.';
|
||||
$row->[$colnum] .= '9' while ($decimals--);
|
||||
print "Truncated value $orig_value to " . $row->[$colnum]
|
||||
. " for $table.$column.\n";
|
||||
}
|
||||
}
|
||||
elsif ($col_info && $col_info->{TYPE} =~ /DATETIME/i) {
|
||||
my $date = $row->[$colnum];
|
||||
# MySQL can have strange invalid values for Datetimes.
|
||||
$row->[$colnum] = '1901-01-01 00:00:00'
|
||||
if $date && $date eq '0000-00-00 00:00:00';
|
||||
}
|
||||
|
||||
$insert_sth->bind_param($param_num, $row->[$colnum])
|
||||
unless $already_bound;
|
||||
$colnum++;
|
||||
}
|
||||
|
||||
$insert_sth->execute();
|
||||
}
|
||||
|
||||
# PostgreSQL doesn't like it when you insert values into
|
||||
# a serial field; it doesn't increment the counter
|
||||
# automatically.
|
||||
if ($target_db->isa('Bugzilla::DB::Pg')) {
|
||||
foreach my $column (@table_columns) {
|
||||
my $col_info = $source_db->bz_column_info($table, $column);
|
||||
if ($col_info && $col_info->{TYPE} =~ /SERIAL/i) {
|
||||
# Set the sequence to the current max value + 1.
|
||||
my ($max_val) = $target_db->selectrow_array(
|
||||
"SELECT MAX($column) FROM $table");
|
||||
$max_val = 0 if !defined $max_val;
|
||||
$max_val++;
|
||||
print "\nSetting the next value for $table.$column to $max_val.";
|
||||
$target_db->do("SELECT pg_catalog.setval
|
||||
('${table}_${column}_seq', $max_val, false)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
print "\n\n";
|
||||
}
|
||||
|
||||
print "Committing changes to the target database...\n";
|
||||
$target_db->commit;
|
||||
|
||||
print "All done! Make sure to run checksetup on the new DB.\n";
|
||||
$source_db->disconnect;
|
||||
$target_db->disconnect;
|
||||
1;
|
||||
@@ -1,7 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
thisdir=`dirname "$0"`
|
||||
bugids=`$thisdir/bugids "$@"`
|
||||
if test "$?" != "0"; then echo "$bugids" 1>&2; exit 1; fi
|
||||
|
||||
echo "$bugids" | wc -w
|
||||
@@ -1,32 +0,0 @@
|
||||
#!/bin/sh
|
||||
# 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
|
||||
# Andreas Franke <afranke@mathweb.org>.
|
||||
# Corporation. Portions created by Andreas Franke are
|
||||
# Copyright (C) 2001,2005 Andreas Franke. All
|
||||
# Rights Reserved.
|
||||
#
|
||||
# Contributor(s):
|
||||
|
||||
thisdir=`dirname "$0"`
|
||||
buglist="$thisdir/buglist"
|
||||
csvfile="$thisdir/buglist.csv"
|
||||
|
||||
$thisdir/buglist "$@" 2>&1 1>${csvfile}
|
||||
if test "$?" != "0"; then cat "$csvfile" 1>&2; exit 1; fi
|
||||
|
||||
# 1. use 'awk' to select the first column (bug_id)
|
||||
# 2. use 'grep -v' to remove the first line with the column headers
|
||||
# 3. use backquotes & 'echo' to get all values in one line, space separated
|
||||
echo `cat ${csvfile} | awk -F, '{printf $1 "\n"}' | grep -v bug_id`
|
||||
@@ -1,31 +0,0 @@
|
||||
#!/bin/sh
|
||||
# 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
|
||||
# Andreas Franke <afranke@mathweb.org>.
|
||||
# Corporation. Portions created by Andreas Franke are
|
||||
# Copyright (C) 2001,2005 Andreas Franke. All
|
||||
# Rights Reserved.
|
||||
#
|
||||
# Contributor(s):
|
||||
|
||||
defaultcolumnlist="severity priority platform status resolution target_milestone status_whiteboard keywords summaryfull"
|
||||
|
||||
thisdir=`dirname "$0"`
|
||||
query=`$thisdir/makequery "$@"`
|
||||
if test "$?" != "0"; then exit 1; fi
|
||||
|
||||
outputfile="/dev/stdout"
|
||||
#outputfile="buglist.html"
|
||||
#\rm -f ${outputfile}
|
||||
wget -q -O ${outputfile} --header="Cookie: COLUMNLIST=${COLUMNLIST-${defaultcolumnlist}}" "${query}"
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
thisdir=`dirname "$0"`
|
||||
bugids=`$thisdir/bugids "$@"` || echo "$bugids" 1>&2 && exit 1
|
||||
|
||||
echo "$bugids" | sed -e 's/ /\,/g'
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
thisdir=`dirname "$0"`
|
||||
bugids=`$thisdir/bugids "$@"` || echo "$bugids" 1>&2 && exit 1
|
||||
|
||||
bugids=`echo "$bugids" | sed -e 's/ /\,/g'`
|
||||
echo "https://bugzilla.mozilla.org/buglist.cgi?ctype=html&bug_id=$bugids"
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
#!/bin/sh
|
||||
# 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
|
||||
# Andreas Franke <afranke@mathweb.org>.
|
||||
# Corporation. Portions created by Andreas Franke are
|
||||
# Copyright (C) 2001,2005 Andreas Franke. All
|
||||
# Rights Reserved.
|
||||
#
|
||||
# Contributor(s):
|
||||
|
||||
conf="`dirname $0`/query.conf"
|
||||
|
||||
query="https://bugzilla.mozilla.org/buglist.cgi?ctype=csv"
|
||||
|
||||
chart=0
|
||||
and=0
|
||||
while test "X$1" != "X"; do
|
||||
arg="$1"
|
||||
shift
|
||||
if test 0 != `expr "X$arg" : 'X--[^=]*\$'`; then
|
||||
# long option: --name val (without '=')
|
||||
name=`expr "X$arg" : 'X--\(.*\)'`
|
||||
val="$1"
|
||||
shift
|
||||
elif test 0 != `expr "X$arg" : 'X--[^=][^=]*='`; then
|
||||
# long option: --name=val
|
||||
name=`expr "X$arg" : 'X--\([^=]*\)'`
|
||||
val=`expr "X$arg" : 'X--[^=]*=\(.*\)'`
|
||||
elif test 0 != `expr "X$arg" : 'X-[a-zA-Z]\$'`; then
|
||||
# short option like -X foo (with space in between)
|
||||
name=`expr "X$arg" : 'X-\(.\)'`
|
||||
val="$1"
|
||||
shift
|
||||
elif test 0 != `expr "X$arg" : 'X-[a-zA-Z]='`; then
|
||||
# reject things like -X=foo
|
||||
echo "Unrecognized option $arg" 1>&2
|
||||
echo "Use -Xfoo or -X foo instead of -X=foo" 1>&2
|
||||
exit 1
|
||||
elif test 0 != `expr "X$arg" : 'X-[a-zA-Z]'`; then
|
||||
# short option like -Xfoo (without space)
|
||||
name=`expr "X$arg" : 'X-\(.\)'`
|
||||
val=`expr "X$arg" : 'X-.\(.*\)'`
|
||||
else
|
||||
name="default"
|
||||
val="$arg"
|
||||
#echo "Unrecognized option $arg" 1>&2
|
||||
#exit 1
|
||||
fi
|
||||
|
||||
# plausibility check: val must not be empty, nor start with '-'
|
||||
if test "X$val" = "X"; then
|
||||
echo "No value found for '$name'!" 1>&2
|
||||
exit 1
|
||||
elif test 0 != `expr "X$val" : "X-"` && \
|
||||
test 0 = `expr "X$val" : "X---"`; then
|
||||
echo "Suspicious value for '$name': '$val' looks like an option!" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# find field and comparison type for option ${name}
|
||||
field=`grep "\"$name\"" "$conf" | awk '{printf $1}'`
|
||||
type=`grep "\"$name\"" "$conf" | awk '{printf $2}'`
|
||||
if test "X$field" = "X" || test "X$type" = "X"; then
|
||||
if test "X$name" = "Xdefault"; then
|
||||
echo 1>&2 "Error: unexpected argument '$arg'"
|
||||
cat 1>&2 <<EOF
|
||||
Use short options like -P1 or long options like --priority=1 ,
|
||||
or enable the 'default' behaviour in the 'query.conf' file.
|
||||
EOF
|
||||
else
|
||||
echo "Unknown field name '$name'." 1>&2
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# split val into comma-separated alternative values
|
||||
or=0
|
||||
while test "X$val" != "X"; do
|
||||
# val1 gets everything before the first comma; val gets the rest
|
||||
if test 0 != `expr "X$val" : 'X[^,]*,'`; then
|
||||
val1=`expr "X$val" : 'X\([^,]*\),'`
|
||||
val=`expr "X$val" : 'X[^,]*,\(.*\)'`
|
||||
else
|
||||
val1="$val"
|
||||
val=""
|
||||
fi
|
||||
# append to query
|
||||
query="${query}&field${chart}-${and}-${or}=${field}"
|
||||
query="${query}&type${chart}-${and}-${or}=${type}"
|
||||
query="${query}&value${chart}-${and}-${or}=${val1}"
|
||||
#echo "----- ${name} : ${field} : ${type} : ${val1} -----" 1>&2
|
||||
or=`expr ${or} + 1`
|
||||
done
|
||||
chart=`expr ${chart} + 1`
|
||||
done
|
||||
|
||||
echo "${query}"
|
||||
@@ -1,49 +0,0 @@
|
||||
# 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
|
||||
# Andreas Franke <afranke@mathweb.org>.
|
||||
# Corporation. Portions created by Andreas Franke are
|
||||
# Copyright (C) 2001 Andreas Franke. All
|
||||
# Rights Reserved.
|
||||
#
|
||||
# Contributor(s):
|
||||
|
||||
#
|
||||
# This is `query.conf', the config file for `buglist'.
|
||||
#
|
||||
# Columns: 1: field_name, 2: comparison_type, 3: cmd-line options
|
||||
#
|
||||
bug_status substring "s","status"
|
||||
resolution substring "r","resolution"
|
||||
rep_platform substring "p","platform"
|
||||
op_sys substring "o","os","opsys"
|
||||
priority substring "P","priority"
|
||||
bug_severity substring "S","severity"
|
||||
assigned_to substring "A","O","owner","assignedto"
|
||||
reporter substring "R","reporter"
|
||||
qa_contact substring "Q","qa","qacontact"
|
||||
cc substring "C","cc"
|
||||
product substring "product"
|
||||
version substring "V","version"
|
||||
component substring "c","component"
|
||||
target_milestone substring "M","milestone"
|
||||
short_desc substring "summary","defaultREMOVEME"
|
||||
longdesc substring "d","description","longdesc"
|
||||
bug_file_loc substring "u","url"
|
||||
status_whiteboard substring "w","whiteboard"
|
||||
keywords substring "k","K","keywords"
|
||||
attachments.description substring "attachdesc"
|
||||
attach_data.thedata substring "attachdata"
|
||||
attachments.mimetype substring "attachmime"
|
||||
dependson substring # bug 30823
|
||||
blocked substring # bug 30823
|
||||
@@ -1,51 +0,0 @@
|
||||
#!/usr/bin/perl -w
|
||||
# -*- 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): Dawn Endico <endico@mozilla.org>
|
||||
# Jacob Steenhagen <jake@bugzilla.org>
|
||||
# Jouni Heikniemi <jouni@heikniemi.net>
|
||||
|
||||
# Keep a record of all cvs updates made from a given directory.
|
||||
#
|
||||
# Later, if changes need to be backed out, look at the log file
|
||||
# and run the cvs command with the date that you want to back
|
||||
# out to. (Probably the second to last entry).
|
||||
|
||||
# Because this script lives in contrib, you may want to
|
||||
# ln -s contrib/cvs-update.pl cvs-update.pl
|
||||
# from your bugzilla install directory so you can run
|
||||
# the script easily from there (./cvs-update.pl)
|
||||
|
||||
#DATE=`date +%e/%m/%Y\ %k:%M:%S\ %Z`
|
||||
|
||||
my ($second, $minute, $hour, $day, $month, $year) = gmtime;
|
||||
my $date = sprintf("%04d-%02d-%02d %d:%02d:%02dZ",
|
||||
$year+1900, $month+1, $day, $hour, $minute, $second);
|
||||
my $cmd = "cvs -q update -dP";
|
||||
open LOG, ">>cvs-update.log" or die("Couldn't open cvs update log!");
|
||||
print LOG "$cmd -D \"$date\"\n";
|
||||
close LOG;
|
||||
system("$cmd -A");
|
||||
|
||||
# sample log file
|
||||
#cvs update -P -D "11/04/2000 20:22:08 PDT"
|
||||
#cvs update -P -D "11/05/2000 20:22:22 PDT"
|
||||
#cvs update -P -D "11/07/2000 20:26:29 PDT"
|
||||
#cvs update -P -D "11/08/2000 20:27:10 PDT"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,62 +0,0 @@
|
||||
gnatsparse
|
||||
==========
|
||||
|
||||
Author: Daniel Berlin <dan@dberlin.org>
|
||||
|
||||
gnatsparse is a simple Python program that imports a GNATS database
|
||||
into a Bugzilla system. It is based on the gnats2bz.pl Perl script
|
||||
but it's a rewrite at the same time. Its parser is based on gnatsweb,
|
||||
which gives a 10 times speed improvement compared to the previous code.
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
* Chunks audit trail into separate comments, with the right From's, times, etc.
|
||||
|
||||
* Handles followup emails that are in the report, with the right From's, times,
|
||||
etc.
|
||||
|
||||
* Properly handles duplicates, adding the standard bugzilla duplicate message.
|
||||
|
||||
* Extracts and handles gnatsweb attachments, as well as uuencoded attachments
|
||||
appearing in either followup emails, the how-to-repeat field, etc. Replaces
|
||||
them with a message to look at the attachments list, and adds the standard
|
||||
"Created an attachment" message that bugzilla uses. Handling them includes
|
||||
giving them the right name and mime-type. "attachments" means multiple
|
||||
uuencoded things/gnatsweb attachments are handled properly.
|
||||
|
||||
* Handles reopened bug reports.
|
||||
|
||||
* Builds the cc list from the people who have commented on the report,
|
||||
and the reporter.
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
||||
It requires python 2.2+, it won't work with 1.5.2 (Linux distributions
|
||||
ship with 2.2+ these days, so that shouldn't be an issue).
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
Documentation can be found inside the scripts. The source code is self
|
||||
documenting.
|
||||
|
||||
Issues for someone trying to use it to convert a gnats install
|
||||
-----------------------------------
|
||||
|
||||
1. We have three custom fields bugzilla doesn't ship with,
|
||||
gcchost, gcctarget, and gccbuild.
|
||||
We removed two bugzilla fields, rep_platform and op_sys.
|
||||
If you use the latter instead of the former, you'll need to
|
||||
update the script to account for this.
|
||||
2. Because gcc attachments consist of preprocessed source, all attachments
|
||||
inserted into the attachment database are compressed with zlib.compress.
|
||||
This requires associated bugzilla changes to decompress before sending to
|
||||
the browser.
|
||||
Unless you want to make those changes (it's roughly 3 lines), you'll
|
||||
need to remove the zlib.compress call.
|
||||
3. You will need to come up with your own release to version mapping and
|
||||
install it.
|
||||
4. Obviously, any extra gnats fields you have added will have to
|
||||
be handled in some manner.
|
||||
@@ -1,807 +0,0 @@
|
||||
try:
|
||||
# Using Psyco makes it about 25% faster, but there's a bug in psyco in
|
||||
# handling of eval causing it to use unlimited memory with the magic
|
||||
# file enabled.
|
||||
# import psyco
|
||||
# psyco.full()
|
||||
# from psyco.classes import *
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
import re
|
||||
import base64
|
||||
import cStringIO
|
||||
import specialuu
|
||||
import array
|
||||
import email.Utils
|
||||
import zlib
|
||||
import magic
|
||||
|
||||
# Comment out if you don't want magic detection
|
||||
magicf = magic.MagicFile()
|
||||
|
||||
# Open our output file
|
||||
outfile = open("gnats2bz_data.sql", "w")
|
||||
|
||||
# List of GNATS fields
|
||||
fieldnames = ("Number", "Category", "Synopsis", "Confidential", "Severity",
|
||||
"Priority", "Responsible", "State", "Quarter", "Keywords",
|
||||
"Date-Required", "Class", "Submitter-Id", "Arrival-Date",
|
||||
"Closed-Date", "Last-Modified", "Originator", "Release",
|
||||
"Organization", "Environment", "Description", "How-To-Repeat",
|
||||
"Fix", "Release-Note", "Audit-Trail", "Unformatted")
|
||||
|
||||
# Dictionary telling us which GNATS fields are multiline
|
||||
multilinefields = {"Organization":1, "Environment":1, "Description":1,
|
||||
"How-To-Repeat":1, "Fix":1, "Release-Note":1,
|
||||
"Audit-Trail":1, "Unformatted":1}
|
||||
|
||||
# Mapping of GCC release to version. Our version string is updated every
|
||||
# so we need to funnel all release's with 3.4 in the string to be version
|
||||
# 3.4 for bug tracking purposes
|
||||
# The key is a regex to match, the value is the version it corresponds
|
||||
# with
|
||||
releasetovermap = {r"3\.4":"3.4", r"3\.3":"3.3", r"3\.2\.2":"3.2.2",
|
||||
r"3\.2\.1":"3.2.1", r"3\.2":"3.2", r"3\.1\.2":"3.1.2",
|
||||
r"3\.1\.1":"3.1.1", r"3\.1":"3.1", r"3\.0\.4":"3.0.4",
|
||||
r"3\.0\.3":"3.0.3", r"3\.0\.2":"3.0.2", r"3\.0\.1":"3.0.1",
|
||||
r"3\.0":"3.0", r"2\.95\.4":"2.95.4", r"2\.95\.3":"2.95.3",
|
||||
r"2\.95\.2":"2.95.2", r"2\.95\.1":"2.95.1",
|
||||
r"2\.95":"2.95", r"2\.97":"2.97",
|
||||
r"2\.96.*[rR][eE][dD].*[hH][aA][tT]":"2.96 (redhat)",
|
||||
r"2\.96":"2.96"}
|
||||
|
||||
# These map the field name to the field id bugzilla assigns. We need
|
||||
# the id when doing bug activity.
|
||||
fieldids = {"State":8, "Responsible":15}
|
||||
|
||||
# These are the keywords we use in gcc bug tracking. They are transformed
|
||||
# into bugzilla keywords. The format here is <keyword>-><bugzilla keyword id>
|
||||
keywordids = {"wrong-code":1, "ice-on-legal-code":2, "ice-on-illegal-code":3,
|
||||
"rejects-legal":4, "accepts-illegal":5, "pessimizes-code":6}
|
||||
|
||||
# Map from GNATS states to Bugzilla states. Duplicates and reopened bugs
|
||||
# are handled when parsing the audit trail, so no need for them here.
|
||||
state_lookup = {"":"NEW", "open":"ASSIGNED", "analyzed":"ASSIGNED",
|
||||
"feedback":"WAITING", "closed":"CLOSED",
|
||||
"suspended":"SUSPENDED"}
|
||||
|
||||
# Table of versions that exist in the bugs, built up as we go along
|
||||
versions_table = {}
|
||||
|
||||
# Delimiter gnatsweb uses for attachments
|
||||
attachment_delimiter = "----gnatsweb-attachment----\n"
|
||||
|
||||
# Here starts the various regular expressions we use
|
||||
# Matches an entire GNATS single line field
|
||||
gnatfieldre = re.compile(r"""^([>\w\-]+)\s*:\s*(.*)\s*$""")
|
||||
|
||||
# Matches the name of a GNATS field
|
||||
fieldnamere = re.compile(r"""^>(.*)$""")
|
||||
|
||||
# Matches the useless part of an envelope
|
||||
uselessre = re.compile(r"""^(\S*?):\s*""", re.MULTILINE)
|
||||
|
||||
# Matches the filename in a content disposition
|
||||
dispositionre = re.compile("(\\S+);\\s*filename=\"([^\"]+)\"")
|
||||
|
||||
# Matches the last changed date in the entire text of a bug
|
||||
# If you have other editable fields that get audit trail entries, modify this
|
||||
# The field names are explicitly listed in order to speed up matching
|
||||
lastdatere = re.compile(r"""^(?:(?:State|Responsible|Priority|Severity)-Changed-When: )(.+?)$""", re.MULTILINE)
|
||||
|
||||
# Matches the From line of an email or the first line of an audit trail entry
|
||||
# We use this re to find the begin lines of all the audit trail entries
|
||||
# The field names are explicitly listed in order to speed up matching
|
||||
fromtore=re.compile(r"""^(?:(?:State|Responsible|Priority|Severity)-Changed-From-To: |From: )""", re.MULTILINE)
|
||||
|
||||
# These re's match the various parts of an audit trail entry
|
||||
changedfromtore=re.compile(r"""^(\w+?)-Changed-From-To: (.+?)$""", re.MULTILINE)
|
||||
changedbyre=re.compile(r"""^\w+?-Changed-By: (.+?)$""", re.MULTILINE)
|
||||
changedwhenre=re.compile(r"""^\w+?-Changed-When: (.+?)$""", re.MULTILINE)
|
||||
changedwhyre=re.compile(r"""^\w+?-Changed-Why:\s*(.*?)$""", re.MULTILINE)
|
||||
|
||||
# This re matches audit trail text saying that the current bug is a duplicate of another
|
||||
duplicatere=re.compile(r"""(?:")?Dup(?:licate)?(?:d)?(?:")? of .*?(\d+)""", re.IGNORECASE | re.MULTILINE)
|
||||
|
||||
# Get the text of a From: line
|
||||
fromre=re.compile(r"""^From: (.*?)$""", re.MULTILINE)
|
||||
|
||||
# Get the text of a Date: Line
|
||||
datere=re.compile(r"""^Date: (.*?)$""", re.MULTILINE)
|
||||
|
||||
# Map of the responsible file to email addresses
|
||||
responsible_map = {}
|
||||
# List of records in the responsible file
|
||||
responsible_list = []
|
||||
# List of records in the categories file
|
||||
categories_list = []
|
||||
# List of pr's in the index
|
||||
pr_list = []
|
||||
# Map usernames to user ids
|
||||
usermapping = {}
|
||||
# Start with this user id
|
||||
userid_base = 2
|
||||
|
||||
# Name of gnats user
|
||||
gnats_username = "gnats@gcc.gnu.org"
|
||||
# Name of unassigned user
|
||||
unassigned_username = "unassigned@gcc.gnu.org"
|
||||
|
||||
gnats_db_dir = "."
|
||||
product = "gcc"
|
||||
productdesc = "GNU Compiler Connection"
|
||||
milestoneurl = "http://gcc/gnu.org"
|
||||
defaultmilestone = "3.4"
|
||||
|
||||
def write_non_bug_tables():
|
||||
""" Write out the non-bug related tables, such as products, profiles, etc."""
|
||||
# Set all non-unconfirmed bugs's everconfirmed flag
|
||||
print >>outfile, "update bugs set everconfirmed=1 where bug_status != 'UNCONFIRMED';"
|
||||
|
||||
# Set all bugs assigned to the unassigned user to NEW
|
||||
print >>outfile, "update bugs set bug_status='NEW',assigned_to='NULL' where bug_status='ASSIGNED' AND assigned_to=3;"
|
||||
|
||||
# Insert the products
|
||||
print >>outfile, "\ninsert into products ("
|
||||
print >>outfile, " product, description, milestoneurl, disallownew,"
|
||||
print >>outfile, " defaultmilestone, votestoconfirm) values ("
|
||||
print >>outfile, " '%s', '%s', '%s', 0, '%s', 1);" % (product,
|
||||
productdesc,
|
||||
milestoneurl,
|
||||
defaultmilestone)
|
||||
|
||||
# Insert the components
|
||||
for category in categories_list:
|
||||
component = SqlQuote(category[0])
|
||||
productstr = SqlQuote(product)
|
||||
description = SqlQuote(category[1])
|
||||
initialowner = SqlQuote("3")
|
||||
print >>outfile, "\ninsert into components (";
|
||||
print >>outfile, " value, program, initialowner, initialqacontact,"
|
||||
print >>outfile, " description) values ("
|
||||
print >>outfile, " %s, %s, %s, '', %s);" % (component, productstr,
|
||||
initialowner, description)
|
||||
|
||||
# Insert the versions
|
||||
for productstr, version_list in versions_table.items():
|
||||
productstr = SqlQuote(productstr)
|
||||
for version in version_list:
|
||||
version = SqlQuote(version)
|
||||
print >>outfile, "\ninsert into versions (value, program) "
|
||||
print >>outfile, " values (%s, %s);" % (version, productstr)
|
||||
|
||||
# Insert the users
|
||||
for username, userid in usermapping.items():
|
||||
realname = map_username_to_realname(username)
|
||||
username = SqlQuote(username)
|
||||
realname = SqlQuote(realname)
|
||||
print >>outfile, "\ninsert into profiles ("
|
||||
print >>outfile, " userid, login_name, password, cryptpassword, realname, groupset"
|
||||
print >>outfile, ") values ("
|
||||
print >>outfile, "%s,%s,'password',encrypt('password'), %s, 0);" % (userid, username, realname)
|
||||
print >>outfile, "update profiles set groupset=1 << 32 where login_name like '%\@gcc.gnu.org';"
|
||||
|
||||
def unixdate2datetime(unixdate):
|
||||
""" Convert a unix date to a datetime value """
|
||||
year, month, day, hour, min, sec, x, x, x, x = email.Utils.parsedate_tz(unixdate)
|
||||
return "%d-%02d-%02d %02d:%02d:%02d" % (year,month,day,hour,min,sec)
|
||||
|
||||
def unixdate2timestamp(unixdate):
|
||||
""" Convert a unix date to a timestamp value """
|
||||
year, month, day, hour, min, sec, x, x, x, x = email.Utils.parsedate_tz(unixdate)
|
||||
return "%d%02d%02d%02d%02d%02d" % (year,month,day,hour,min,sec)
|
||||
|
||||
def SqlQuote(str):
|
||||
""" Perform SQL quoting on a string """
|
||||
return "'%s'" % str.replace("'", """''""").replace("\\", "\\\\").replace("\0","\\0")
|
||||
|
||||
def convert_gccver_to_ver(gccver):
|
||||
""" Given a gcc version, convert it to a Bugzilla version. """
|
||||
for k in releasetovermap.keys():
|
||||
if re.search(".*%s.*" % k, gccver) is not None:
|
||||
return releasetovermap[k]
|
||||
result = re.search(r""".*(\d\.\d) \d+ \(experimental\).*""", gccver)
|
||||
if result is not None:
|
||||
return result.group(1)
|
||||
return "unknown"
|
||||
|
||||
def load_index(fname):
|
||||
""" Load in the GNATS index file """
|
||||
global pr_list
|
||||
ifp = open(fname)
|
||||
for record in ifp.xreadlines():
|
||||
fields = record.split("|")
|
||||
pr_list.append(fields[0])
|
||||
ifp.close()
|
||||
|
||||
def load_categories(fname):
|
||||
""" Load in the GNATS categories file """
|
||||
global categories_list
|
||||
cfp = open(fname)
|
||||
for record in cfp.xreadlines():
|
||||
if re.search("^#", record) is not None:
|
||||
continue
|
||||
categories_list.append(record.split(":"))
|
||||
cfp.close()
|
||||
|
||||
def map_username_to_realname(username):
|
||||
""" Given a username, find the real name """
|
||||
name = username
|
||||
name = re.sub("@.*", "", name)
|
||||
for responsible_record in responsible_list:
|
||||
if responsible_record[0] == name:
|
||||
return responsible_record[1]
|
||||
if len(responsible_record) > 2:
|
||||
if responsible_record[2] == username:
|
||||
return responsible_record[1]
|
||||
return ""
|
||||
|
||||
|
||||
def get_userid(responsible):
|
||||
""" Given an email address, get the user id """
|
||||
global responsible_map
|
||||
global usermapping
|
||||
global userid_base
|
||||
if responsible is None:
|
||||
return -1
|
||||
responsible = responsible.lower()
|
||||
responsible = re.sub("sources.redhat.com", "gcc.gnu.org", responsible)
|
||||
if responsible_map.has_key(responsible):
|
||||
responsible = responsible_map[responsible]
|
||||
if usermapping.has_key(responsible):
|
||||
return usermapping[responsible]
|
||||
else:
|
||||
usermapping[responsible] = userid_base
|
||||
userid_base += 1
|
||||
return usermapping[responsible]
|
||||
|
||||
def load_responsible(fname):
|
||||
""" Load in the GNATS responsible file """
|
||||
global responsible_map
|
||||
global responsible_list
|
||||
rfp = open(fname)
|
||||
for record in rfp.xreadlines():
|
||||
if re.search("^#", record) is not None:
|
||||
continue
|
||||
split_record = record.split(":")
|
||||
responsible_map[split_record[0]] = split_record[2].rstrip()
|
||||
responsible_list.append(record.split(":"))
|
||||
rfp.close()
|
||||
|
||||
def split_csl(list):
|
||||
""" Split a comma separated list """
|
||||
newlist = re.split(r"""\s*,\s*""", list)
|
||||
return newlist
|
||||
|
||||
def fix_email_addrs(addrs):
|
||||
""" Perform various fixups and cleaning on an e-mail address """
|
||||
addrs = split_csl(addrs)
|
||||
trimmed_addrs = []
|
||||
for addr in addrs:
|
||||
addr = re.sub(r"""\(.*\)""","",addr)
|
||||
addr = re.sub(r""".*<(.*)>.*""","\\1",addr)
|
||||
addr = addr.rstrip()
|
||||
addr = addr.lstrip()
|
||||
trimmed_addrs.append(addr)
|
||||
addrs = ", ".join(trimmed_addrs)
|
||||
return addrs
|
||||
|
||||
class Bugzillabug(object):
|
||||
""" Class representing a bugzilla bug """
|
||||
def __init__(self, gbug):
|
||||
""" Initialize a bugzilla bug from a GNATS bug. """
|
||||
self.bug_id = gbug.bug_id
|
||||
self.long_descs = []
|
||||
self.bug_ccs = [get_userid("gcc-bugs@gcc.gnu.org")]
|
||||
self.bug_activity = []
|
||||
self.attachments = gbug.attachments
|
||||
self.gnatsfields = gbug.fields
|
||||
self.need_unformatted = gbug.has_unformatted_attach == 0
|
||||
self.need_unformatted &= gbug.fields.has_key("Unformatted")
|
||||
self.translate_pr()
|
||||
self.update_versions()
|
||||
if self.fields.has_key("Audit-Trail"):
|
||||
self.parse_audit_trail()
|
||||
self.write_bug()
|
||||
|
||||
def parse_fromto(type, string):
|
||||
""" Parses the from and to parts of a changed-from-to line """
|
||||
fromstr = ""
|
||||
tostr = ""
|
||||
|
||||
# Some slightly messed up changed lines have unassigned-new,
|
||||
# instead of unassigned->new. So we make the > optional.
|
||||
result = re.search(r"""(.*)-(?:>?)(.*)""", string)
|
||||
|
||||
# Only know how to handle parsing of State and Responsible
|
||||
# changed-from-to right now
|
||||
if type == "State":
|
||||
fromstr = state_lookup[result.group(1)]
|
||||
tostr = state_lookup[result.group(2)]
|
||||
elif type == "Responsible":
|
||||
if result.group(1) != "":
|
||||
fromstr = result.group(1)
|
||||
if result.group(2) != "":
|
||||
tostr = result.group(2)
|
||||
if responsible_map.has_key(fromstr):
|
||||
fromstr = responsible_map[fromstr]
|
||||
if responsible_map.has_key(tostr):
|
||||
tostr = responsible_map[tostr]
|
||||
return (fromstr, tostr)
|
||||
parse_fromto = staticmethod(parse_fromto)
|
||||
|
||||
def parse_audit_trail(self):
|
||||
""" Parse a GNATS audit trail """
|
||||
trail = self.fields["Audit-Trail"]
|
||||
# Begin to split the audit trail into pieces
|
||||
result = fromtore.finditer(trail)
|
||||
starts = []
|
||||
ends = []
|
||||
pieces = []
|
||||
# Make a list of the pieces
|
||||
for x in result:
|
||||
pieces.append (x)
|
||||
# Find the start and end of each piece
|
||||
if len(pieces) > 0:
|
||||
for x in xrange(len(pieces)-1):
|
||||
starts.append(pieces[x].start())
|
||||
ends.append(pieces[x+1].start())
|
||||
starts.append(pieces[-1].start())
|
||||
ends.append(len(trail))
|
||||
pieces = []
|
||||
# Now make the list of actual text of the pieces
|
||||
for x in xrange(len(starts)):
|
||||
pieces.append(trail[starts[x]:ends[x]])
|
||||
# And parse the actual pieces
|
||||
for piece in pieces:
|
||||
result = changedfromtore.search(piece)
|
||||
# See what things we actually have inside this entry, and
|
||||
# handle them appropriately
|
||||
if result is not None:
|
||||
type = result.group(1)
|
||||
changedfromto = result.group(2)
|
||||
# If the bug was reopened, mark it as such
|
||||
if changedfromto.find("closed->analyzed") != -1:
|
||||
if self.fields["bug_status"] == "'NEW'":
|
||||
self.fields["bug_status"] = "'REOPENED'"
|
||||
if type == "State" or type == "Responsible":
|
||||
oldstate, newstate = self.parse_fromto (type, changedfromto)
|
||||
result = changedbyre.search(piece)
|
||||
if result is not None:
|
||||
changedby = result.group(1)
|
||||
result = changedwhenre.search(piece)
|
||||
if result is not None:
|
||||
changedwhen = result.group(1)
|
||||
changedwhen = unixdate2datetime(changedwhen)
|
||||
changedwhen = SqlQuote(changedwhen)
|
||||
result = changedwhyre.search(piece)
|
||||
changedwhy = piece[result.start(1):]
|
||||
#changedwhy = changedwhy.lstrip()
|
||||
changedwhy = changedwhy.rstrip()
|
||||
changedby = get_userid(changedby)
|
||||
# Put us on the cc list if we aren't there already
|
||||
if changedby != self.fields["userid"] \
|
||||
and changedby not in self.bug_ccs:
|
||||
self.bug_ccs.append(changedby)
|
||||
# If it's a duplicate, mark it as such
|
||||
result = duplicatere.search(changedwhy)
|
||||
if result is not None:
|
||||
newtext = "*** This bug has been marked as a duplicate of %s ***" % result.group(1)
|
||||
newtext = SqlQuote(newtext)
|
||||
self.long_descs.append((self.bug_id, changedby,
|
||||
changedwhen, newtext))
|
||||
self.fields["bug_status"] = "'RESOLVED'"
|
||||
self.fields["resolution"] = "'DUPLICATE'"
|
||||
self.fields["userid"] = changedby
|
||||
else:
|
||||
newtext = "%s-Changed-From-To: %s\n%s-Changed-Why: %s\n" % (type, changedfromto, type, changedwhy)
|
||||
newtext = SqlQuote(newtext)
|
||||
self.long_descs.append((self.bug_id, changedby,
|
||||
changedwhen, newtext))
|
||||
if type == "State" or type == "Responsible":
|
||||
newstate = SqlQuote("%s" % newstate)
|
||||
oldstate = SqlQuote("%s" % oldstate)
|
||||
fieldid = fieldids[type]
|
||||
self.bug_activity.append((newstate, oldstate, fieldid, changedby, changedwhen))
|
||||
|
||||
else:
|
||||
# It's an email
|
||||
result = fromre.search(piece)
|
||||
if result is None:
|
||||
continue
|
||||
fromstr = result.group(1)
|
||||
fromstr = fix_email_addrs(fromstr)
|
||||
fromstr = get_userid(fromstr)
|
||||
result = datere.search(piece)
|
||||
if result is None:
|
||||
continue
|
||||
datestr = result.group(1)
|
||||
datestr = SqlQuote(unixdate2timestamp(datestr))
|
||||
if fromstr != self.fields["userid"] \
|
||||
and fromstr not in self.bug_ccs:
|
||||
self.bug_ccs.append(fromstr)
|
||||
self.long_descs.append((self.bug_id, fromstr, datestr,
|
||||
SqlQuote(piece)))
|
||||
|
||||
|
||||
|
||||
def write_bug(self):
|
||||
""" Output a bug to the data file """
|
||||
fields = self.fields
|
||||
print >>outfile, "\ninsert into bugs("
|
||||
print >>outfile, " bug_id, assigned_to, bug_severity, priority, bug_status, creation_ts, delta_ts,"
|
||||
print >>outfile, " short_desc,"
|
||||
print >>outfile, " reporter, version,"
|
||||
print >>outfile, " product, component, resolution, target_milestone, qa_contact,"
|
||||
print >>outfile, " gccbuild, gcctarget, gcchost, keywords"
|
||||
print >>outfile, " ) values ("
|
||||
print >>outfile, "%s, %s, %s, %s, %s, %s, %s," % (self.bug_id, fields["userid"], fields["bug_severity"], fields["priority"], fields["bug_status"], fields["creation_ts"], fields["delta_ts"])
|
||||
print >>outfile, "%s," % (fields["short_desc"])
|
||||
print >>outfile, "%s, %s," % (fields["reporter"], fields["version"])
|
||||
print >>outfile, "%s, %s, %s, %s, 0," %(fields["product"], fields["component"], fields["resolution"], fields["target_milestone"])
|
||||
print >>outfile, "%s, %s, %s, %s" % (fields["gccbuild"], fields["gcctarget"], fields["gcchost"], fields["keywords"])
|
||||
print >>outfile, ");"
|
||||
if self.fields["keywords"] != 0:
|
||||
print >>outfile, "\ninsert into keywords (bug_id, keywordid) values ("
|
||||
print >>outfile, " %s, %s);" % (self.bug_id, fields["keywordid"])
|
||||
for id, who, when, text in self.long_descs:
|
||||
print >>outfile, "\ninsert into longdescs ("
|
||||
print >>outfile, " bug_id, who, bug_when, thetext) values("
|
||||
print >>outfile, " %s, %s, %s, %s);" % (id, who, when, text)
|
||||
for name, data, who in self.attachments:
|
||||
print >>outfile, "\ninsert into attachments ("
|
||||
print >>outfile, " bug_id, filename, description, mimetype, ispatch, submitter_id) values ("
|
||||
ftype = None
|
||||
# It's *magic*!
|
||||
if name.endswith(".ii") == 1:
|
||||
ftype = "text/x-c++"
|
||||
elif name.endswith(".i") == 1:
|
||||
ftype = "text/x-c"
|
||||
else:
|
||||
ftype = magicf.detect(cStringIO.StringIO(data))
|
||||
if ftype is None:
|
||||
ftype = "application/octet-stream"
|
||||
|
||||
print >>outfile, "%s,%s,%s, %s,0, %s,%s);" %(self.bug_id, SqlQuote(name), SqlQuote(name), SqlQuote (ftype), who)
|
||||
print >>outfile, "\ninsert into attach_data ("
|
||||
print >>outfile, "\n(id, thedata) values (last_insert_id(),"
|
||||
print >>outfile, "%s);" % (SqlQuote(zlib.compress(data)))
|
||||
for newstate, oldstate, fieldid, changedby, changedwhen in self.bug_activity:
|
||||
print >>outfile, "\ninsert into bugs_activity ("
|
||||
print >>outfile, " bug_id, who, bug_when, fieldid, added, removed) values ("
|
||||
print >>outfile, " %s, %s, %s, %s, %s, %s);" % (self.bug_id,
|
||||
changedby,
|
||||
changedwhen,
|
||||
fieldid,
|
||||
newstate,
|
||||
oldstate)
|
||||
for cc in self.bug_ccs:
|
||||
print >>outfile, "\ninsert into cc(bug_id, who) values (%s, %s);" %(self.bug_id, cc)
|
||||
def update_versions(self):
|
||||
""" Update the versions table to account for the version on this bug """
|
||||
global versions_table
|
||||
if self.fields.has_key("Release") == 0 \
|
||||
or self.fields.has_key("Category") == 0:
|
||||
return
|
||||
curr_product = "gcc"
|
||||
curr_version = self.fields["Release"]
|
||||
if curr_version == "":
|
||||
return
|
||||
curr_version = convert_gccver_to_ver (curr_version)
|
||||
if versions_table.has_key(curr_product) == 0:
|
||||
versions_table[curr_product] = []
|
||||
for version in versions_table[curr_product]:
|
||||
if version == curr_version:
|
||||
return
|
||||
versions_table[curr_product].append(curr_version)
|
||||
def translate_pr(self):
|
||||
""" Transform a GNATS PR into a Bugzilla bug """
|
||||
self.fields = self.gnatsfields
|
||||
if (self.fields.has_key("Organization") == 0) \
|
||||
or self.fields["Organization"].find("GCC"):
|
||||
self.fields["Originator"] = ""
|
||||
self.fields["Organization"] = ""
|
||||
self.fields["Organization"].lstrip()
|
||||
if (self.fields.has_key("Release") == 0) \
|
||||
or self.fields["Release"] == "" \
|
||||
or self.fields["Release"].find("unknown-1.0") != -1:
|
||||
self.fields["Release"]="unknown"
|
||||
if self.fields.has_key("Responsible"):
|
||||
result = re.search(r"""\w+""", self.fields["Responsible"])
|
||||
self.fields["Responsible"] = "%s%s" % (result.group(0), "@gcc.gnu.org")
|
||||
self.fields["gcchost"] = ""
|
||||
self.fields["gcctarget"] = ""
|
||||
self.fields["gccbuild"] = ""
|
||||
if self.fields.has_key("Environment"):
|
||||
result = re.search("^host: (.+?)$", self.fields["Environment"],
|
||||
re.MULTILINE)
|
||||
if result is not None:
|
||||
self.fields["gcchost"] = result.group(1)
|
||||
result = re.search("^target: (.+?)$", self.fields["Environment"],
|
||||
re.MULTILINE)
|
||||
if result is not None:
|
||||
self.fields["gcctarget"] = result.group(1)
|
||||
result = re.search("^build: (.+?)$", self.fields["Environment"],
|
||||
re.MULTILINE)
|
||||
if result is not None:
|
||||
self.fields["gccbuild"] = result.group(1)
|
||||
self.fields["userid"] = get_userid(self.fields["Responsible"])
|
||||
self.fields["bug_severity"] = "normal"
|
||||
if self.fields["Class"] == "change-request":
|
||||
self.fields["bug_severity"] = "enhancement"
|
||||
elif self.fields.has_key("Severity"):
|
||||
if self.fields["Severity"] == "critical":
|
||||
self.fields["bug_severity"] = "critical"
|
||||
elif self.fields["Severity"] == "serious":
|
||||
self.fields["bug_severity"] = "major"
|
||||
elif self.fields.has_key("Synopsis"):
|
||||
if re.search("crash|assert", self.fields["Synopsis"]):
|
||||
self.fields["bug_severity"] = "critical"
|
||||
elif re.search("wrong|error", self.fields["Synopsis"]):
|
||||
self.fields["bug_severity"] = "major"
|
||||
self.fields["bug_severity"] = SqlQuote(self.fields["bug_severity"])
|
||||
self.fields["keywords"] = 0
|
||||
if keywordids.has_key(self.fields["Class"]):
|
||||
self.fields["keywords"] = self.fields["Class"]
|
||||
self.fields["keywordid"] = keywordids[self.fields["Class"]]
|
||||
self.fields["keywords"] = SqlQuote(self.fields["keywords"])
|
||||
self.fields["priority"] = "P1"
|
||||
if self.fields.has_key("Severity") and self.fields.has_key("Priority"):
|
||||
severity = self.fields["Severity"]
|
||||
priority = self.fields["Priority"]
|
||||
if severity == "critical":
|
||||
if priority == "high":
|
||||
self.fields["priority"] = "P1"
|
||||
else:
|
||||
self.fields["priority"] = "P2"
|
||||
elif severity == "serious":
|
||||
if priority == "low":
|
||||
self.fields["priority"] = "P4"
|
||||
else:
|
||||
self.fields["priority"] = "P3"
|
||||
else:
|
||||
if priority == "high":
|
||||
self.fields["priority"] = "P4"
|
||||
else:
|
||||
self.fields["priority"] = "P5"
|
||||
self.fields["priority"] = SqlQuote(self.fields["priority"])
|
||||
state = self.fields["State"]
|
||||
if (state == "open" or state == "analyzed") and self.fields["userid"] != 3:
|
||||
self.fields["bug_status"] = "ASSIGNED"
|
||||
self.fields["resolution"] = ""
|
||||
elif state == "feedback":
|
||||
self.fields["bug_status"] = "WAITING"
|
||||
self.fields["resolution"] = ""
|
||||
elif state == "closed":
|
||||
self.fields["bug_status"] = "CLOSED"
|
||||
if self.fields.has_key("Class"):
|
||||
theclass = self.fields["Class"]
|
||||
if theclass.find("duplicate") != -1:
|
||||
self.fields["resolution"]="DUPLICATE"
|
||||
elif theclass.find("mistaken") != -1:
|
||||
self.fields["resolution"]="INVALID"
|
||||
else:
|
||||
self.fields["resolution"]="FIXED"
|
||||
else:
|
||||
self.fields["resolution"]="FIXED"
|
||||
elif state == "suspended":
|
||||
self.fields["bug_status"] = "SUSPENDED"
|
||||
self.fields["resolution"] = ""
|
||||
elif state == "analyzed" and self.fields["userid"] == 3:
|
||||
self.fields["bug_status"] = "NEW"
|
||||
self.fields["resolution"] = ""
|
||||
else:
|
||||
self.fields["bug_status"] = "UNCONFIRMED"
|
||||
self.fields["resolution"] = ""
|
||||
self.fields["bug_status"] = SqlQuote(self.fields["bug_status"])
|
||||
self.fields["resolution"] = SqlQuote(self.fields["resolution"])
|
||||
self.fields["creation_ts"] = ""
|
||||
if self.fields.has_key("Arrival-Date") and self.fields["Arrival-Date"] != "":
|
||||
self.fields["creation_ts"] = unixdate2datetime(self.fields["Arrival-Date"])
|
||||
self.fields["creation_ts"] = SqlQuote(self.fields["creation_ts"])
|
||||
self.fields["delta_ts"] = ""
|
||||
if self.fields.has_key("Audit-Trail"):
|
||||
result = lastdatere.findall(self.fields["Audit-Trail"])
|
||||
result.reverse()
|
||||
if len(result) > 0:
|
||||
self.fields["delta_ts"] = unixdate2timestamp(result[0])
|
||||
if self.fields["delta_ts"] == "":
|
||||
if self.fields.has_key("Arrival-Date") and self.fields["Arrival-Date"] != "":
|
||||
self.fields["delta_ts"] = unixdate2timestamp(self.fields["Arrival-Date"])
|
||||
self.fields["delta_ts"] = SqlQuote(self.fields["delta_ts"])
|
||||
self.fields["short_desc"] = SqlQuote(self.fields["Synopsis"])
|
||||
if self.fields.has_key("Reply-To") and self.fields["Reply-To"] != "":
|
||||
self.fields["reporter"] = get_userid(self.fields["Reply-To"])
|
||||
elif self.fields.has_key("Mail-Header"):
|
||||
result = re.search(r"""From .*?([\w.]+@[\w.]+)""", self.fields["Mail-Header"])
|
||||
if result:
|
||||
self.fields["reporter"] = get_userid(result.group(1))
|
||||
else:
|
||||
self.fields["reporter"] = get_userid(gnats_username)
|
||||
else:
|
||||
self.fields["reporter"] = get_userid(gnats_username)
|
||||
long_desc = self.fields["Description"]
|
||||
long_desc2 = ""
|
||||
for field in ["Release", "Environment", "How-To-Repeat"]:
|
||||
if self.fields.has_key(field) and self.fields[field] != "":
|
||||
long_desc += ("\n\n%s:\n" % field) + self.fields[field]
|
||||
if self.fields.has_key("Fix") and self.fields["Fix"] != "":
|
||||
long_desc2 = "Fix:\n" + self.fields["Fix"]
|
||||
if self.need_unformatted == 1 and self.fields["Unformatted"] != "":
|
||||
long_desc += "\n\nUnformatted:\n" + self.fields["Unformatted"]
|
||||
if long_desc != "":
|
||||
self.long_descs.append((self.bug_id, self.fields["reporter"],
|
||||
self.fields["creation_ts"],
|
||||
SqlQuote(long_desc)))
|
||||
if long_desc2 != "":
|
||||
self.long_descs.append((self.bug_id, self.fields["reporter"],
|
||||
self.fields["creation_ts"],
|
||||
SqlQuote(long_desc2)))
|
||||
for field in ["gcchost", "gccbuild", "gcctarget"]:
|
||||
self.fields[field] = SqlQuote(self.fields[field])
|
||||
self.fields["version"] = ""
|
||||
if self.fields["Release"] != "":
|
||||
self.fields["version"] = convert_gccver_to_ver (self.fields["Release"])
|
||||
self.fields["version"] = SqlQuote(self.fields["version"])
|
||||
self.fields["product"] = SqlQuote("gcc")
|
||||
self.fields["component"] = "invalid"
|
||||
if self.fields.has_key("Category"):
|
||||
self.fields["component"] = self.fields["Category"]
|
||||
self.fields["component"] = SqlQuote(self.fields["component"])
|
||||
self.fields["target_milestone"] = "---"
|
||||
if self.fields["version"].find("3.4") != -1:
|
||||
self.fields["target_milestone"] = "3.4"
|
||||
self.fields["target_milestone"] = SqlQuote(self.fields["target_milestone"])
|
||||
if self.fields["userid"] == 2:
|
||||
self.fields["userid"] = "\'NULL\'"
|
||||
|
||||
class GNATSbug(object):
|
||||
""" Represents a single GNATS PR """
|
||||
def __init__(self, filename):
|
||||
self.attachments = []
|
||||
self.has_unformatted_attach = 0
|
||||
fp = open (filename)
|
||||
self.fields = self.parse_pr(fp.xreadlines())
|
||||
self.bug_id = int(self.fields["Number"])
|
||||
if self.fields.has_key("Unformatted"):
|
||||
self.find_gnatsweb_attachments()
|
||||
if self.fields.has_key("How-To-Repeat"):
|
||||
self.find_regular_attachments("How-To-Repeat")
|
||||
if self.fields.has_key("Fix"):
|
||||
self.find_regular_attachments("Fix")
|
||||
|
||||
def get_attacher(fields):
|
||||
if fields.has_key("Reply-To") and fields["Reply-To"] != "":
|
||||
return get_userid(fields["Reply-To"])
|
||||
else:
|
||||
result = None
|
||||
if fields.has_key("Mail-Header"):
|
||||
result = re.search(r"""From .*?([\w.]+\@[\w.]+)""",
|
||||
fields["Mail-Header"])
|
||||
if result is not None:
|
||||
reporter = get_userid(result.group(1))
|
||||
else:
|
||||
reporter = get_userid(gnats_username)
|
||||
get_attacher = staticmethod(get_attacher)
|
||||
def find_regular_attachments(self, which):
|
||||
fields = self.fields
|
||||
while re.search("^begin [0-7]{3}", fields[which],
|
||||
re.DOTALL | re.MULTILINE):
|
||||
outfp = cStringIO.StringIO()
|
||||
infp = cStringIO.StringIO(fields[which])
|
||||
filename, start, end = specialuu.decode(infp, outfp, quiet=0)
|
||||
fields[which]=fields[which].replace(fields[which][start:end],
|
||||
"See attachments for %s\n" % filename)
|
||||
self.attachments.append((filename, outfp.getvalue(),
|
||||
self.get_attacher(fields)))
|
||||
|
||||
def decode_gnatsweb_attachment(self, attachment):
|
||||
result = re.split(r"""\n\n""", attachment, 1)
|
||||
if len(result) == 1:
|
||||
return -1
|
||||
envelope, body = result
|
||||
envelope = uselessre.split(envelope)
|
||||
envelope.pop(0)
|
||||
# Turn the list of key, value into a dict of key => value
|
||||
attachinfo = dict([(envelope[i], envelope[i+1]) for i in xrange(0,len(envelope),2)])
|
||||
for x in attachinfo.keys():
|
||||
attachinfo[x] = attachinfo[x].rstrip()
|
||||
if (attachinfo.has_key("Content-Type") == 0) or \
|
||||
(attachinfo.has_key("Content-Disposition") == 0):
|
||||
raise ValueError, "Unable to parse file attachment"
|
||||
result = dispositionre.search(attachinfo["Content-Disposition"])
|
||||
filename = result.group(2)
|
||||
filename = re.sub(".*/","", filename)
|
||||
filename = re.sub(".*\\\\","", filename)
|
||||
attachinfo["filename"]=filename
|
||||
result = re.search("""(\S+);.*""", attachinfo["Content-Type"])
|
||||
if result is not None:
|
||||
attachinfo["Content-Type"] = result.group(1)
|
||||
if attachinfo.has_key("Content-Transfer-Encoding"):
|
||||
if attachinfo["Content-Transfer-Encoding"] == "base64":
|
||||
attachinfo["data"] = base64.decodestring(body)
|
||||
else:
|
||||
attachinfo["data"]=body
|
||||
|
||||
return (attachinfo["filename"], attachinfo["data"],
|
||||
self.get_attacher(self.fields))
|
||||
|
||||
def find_gnatsweb_attachments(self):
|
||||
fields = self.fields
|
||||
attachments = re.split(attachment_delimiter, fields["Unformatted"])
|
||||
fields["Unformatted"] = attachments.pop(0)
|
||||
for attachment in attachments:
|
||||
result = self.decode_gnatsweb_attachment (attachment)
|
||||
if result != -1:
|
||||
self.attachments.append(result)
|
||||
self.has_unformatted_attach = 1
|
||||
def parse_pr(lines):
|
||||
#fields = {"envelope":[]}
|
||||
fields = {"envelope":array.array("c")}
|
||||
hdrmulti = "envelope"
|
||||
for line in lines:
|
||||
line = line.rstrip('\n')
|
||||
line += '\n'
|
||||
result = gnatfieldre.search(line)
|
||||
if result is None:
|
||||
if hdrmulti != "":
|
||||
if fields.has_key(hdrmulti):
|
||||
#fields[hdrmulti].append(line)
|
||||
fields[hdrmulti].fromstring(line)
|
||||
else:
|
||||
#fields[hdrmulti] = [line]
|
||||
fields[hdrmulti] = array.array("c", line)
|
||||
continue
|
||||
hdr, arg = result.groups()
|
||||
ghdr = "*not valid*"
|
||||
result = fieldnamere.search(hdr)
|
||||
if result != None:
|
||||
ghdr = result.groups()[0]
|
||||
if ghdr in fieldnames:
|
||||
if multilinefields.has_key(ghdr):
|
||||
hdrmulti = ghdr
|
||||
#fields[ghdr] = [""]
|
||||
fields[ghdr] = array.array("c")
|
||||
else:
|
||||
hdrmulti = ""
|
||||
#fields[ghdr] = [arg]
|
||||
fields[ghdr] = array.array("c", arg)
|
||||
elif hdrmulti != "":
|
||||
#fields[hdrmulti].append(line)
|
||||
fields[hdrmulti].fromstring(line)
|
||||
if hdrmulti == "envelope" and \
|
||||
(hdr == "Reply-To" or hdr == "From" \
|
||||
or hdr == "X-GNATS-Notify"):
|
||||
arg = fix_email_addrs(arg)
|
||||
#fields[hdr] = [arg]
|
||||
fields[hdr] = array.array("c", arg)
|
||||
if fields.has_key("Reply-To") and len(fields["Reply-To"]) > 0:
|
||||
fields["Reply-To"] = fields["Reply-To"]
|
||||
else:
|
||||
fields["Reply-To"] = fields["From"]
|
||||
if fields.has_key("From"):
|
||||
del fields["From"]
|
||||
if fields.has_key("X-GNATS-Notify") == 0:
|
||||
fields["X-GNATS-Notify"] = array.array("c")
|
||||
#fields["X-GNATS-Notify"] = ""
|
||||
for x in fields.keys():
|
||||
fields[x] = fields[x].tostring()
|
||||
#fields[x] = "".join(fields[x])
|
||||
for x in fields.keys():
|
||||
if multilinefields.has_key(x):
|
||||
fields[x] = fields[x].rstrip()
|
||||
|
||||
return fields
|
||||
parse_pr = staticmethod(parse_pr)
|
||||
load_index("%s/gnats-adm/index" % gnats_db_dir)
|
||||
load_categories("%s/gnats-adm/categories" % gnats_db_dir)
|
||||
load_responsible("%s/gnats-adm/responsible" % gnats_db_dir)
|
||||
get_userid(gnats_username)
|
||||
get_userid(unassigned_username)
|
||||
for x in pr_list:
|
||||
print "Processing %s..." % x
|
||||
a = GNATSbug ("%s/%s" % (gnats_db_dir, x))
|
||||
b = Bugzillabug(a)
|
||||
write_non_bug_tables()
|
||||
outfile.close()
|
||||
@@ -1,712 +0,0 @@
|
||||
# Found on a russian zope mailing list, and modified to fix bugs in parsing
|
||||
# the magic file and string making
|
||||
# -- Daniel Berlin <dberlin@dberlin.org>
|
||||
import sys, struct, time, re, exceptions, pprint, stat, os, pwd, grp
|
||||
|
||||
_mew = 0
|
||||
|
||||
# _magic='/tmp/magic'
|
||||
# _magic='/usr/share/magic.mime'
|
||||
_magic='/usr/share/magic.mime'
|
||||
mime = 1
|
||||
|
||||
_ldate_adjust = lambda x: time.mktime( time.gmtime(x) )
|
||||
|
||||
BUFFER_SIZE = 1024 * 128 # 128K should be enough...
|
||||
|
||||
class MagicError(exceptions.Exception): pass
|
||||
|
||||
def _handle(fmt='@x',adj=None): return fmt, struct.calcsize(fmt), adj
|
||||
|
||||
KnownTypes = {
|
||||
# 'byte':_handle('@b'),
|
||||
'byte':_handle('@B'),
|
||||
'ubyte':_handle('@B'),
|
||||
|
||||
'string':('s',0,None),
|
||||
'pstring':_handle('p'),
|
||||
|
||||
# 'short':_handle('@h'),
|
||||
# 'beshort':_handle('>h'),
|
||||
# 'leshort':_handle('<h'),
|
||||
'short':_handle('@H'),
|
||||
'beshort':_handle('>H'),
|
||||
'leshort':_handle('<H'),
|
||||
'ushort':_handle('@H'),
|
||||
'ubeshort':_handle('>H'),
|
||||
'uleshort':_handle('<H'),
|
||||
|
||||
'long':_handle('@l'),
|
||||
'belong':_handle('>l'),
|
||||
'lelong':_handle('<l'),
|
||||
'ulong':_handle('@L'),
|
||||
'ubelong':_handle('>L'),
|
||||
'ulelong':_handle('<L'),
|
||||
|
||||
'date':_handle('=l'),
|
||||
'bedate':_handle('>l'),
|
||||
'ledate':_handle('<l'),
|
||||
'ldate':_handle('=l',_ldate_adjust),
|
||||
'beldate':_handle('>l',_ldate_adjust),
|
||||
'leldate':_handle('<l',_ldate_adjust),
|
||||
}
|
||||
|
||||
_mew_cnt = 0
|
||||
def mew(x):
|
||||
global _mew_cnt
|
||||
if _mew :
|
||||
if x=='.' :
|
||||
_mew_cnt += 1
|
||||
if _mew_cnt % 64 == 0 : sys.stderr.write( '\n' )
|
||||
sys.stderr.write( '.' )
|
||||
else:
|
||||
sys.stderr.write( '\b'+x )
|
||||
|
||||
def has_format(s):
|
||||
n = 0
|
||||
l = None
|
||||
for c in s :
|
||||
if c == '%' :
|
||||
if l == '%' : n -= 1
|
||||
else : n += 1
|
||||
l = c
|
||||
return n
|
||||
|
||||
def read_asciiz(file,size=None,pos=None):
|
||||
s = []
|
||||
if pos :
|
||||
mew('s')
|
||||
file.seek( pos, 0 )
|
||||
mew('z')
|
||||
if size is not None :
|
||||
s = [file.read( size ).split('\0')[0]]
|
||||
else:
|
||||
while 1 :
|
||||
c = file.read(1)
|
||||
if (not c) or (ord(c)==0) or (c=='\n') : break
|
||||
s.append (c)
|
||||
mew('Z')
|
||||
return ''.join(s)
|
||||
|
||||
def a2i(v,base=0):
|
||||
if v[-1:] in 'lL' : v = v[:-1]
|
||||
return int( v, base )
|
||||
|
||||
_cmap = {
|
||||
'\\' : '\\',
|
||||
'0' : '\0',
|
||||
}
|
||||
for c in range(ord('a'),ord('z')+1) :
|
||||
try : e = eval('"\\%c"' % chr(c))
|
||||
except ValueError : pass
|
||||
else : _cmap[chr(c)] = e
|
||||
else:
|
||||
del c
|
||||
del e
|
||||
|
||||
def make_string(s):
|
||||
return eval( '"'+s.replace('"','\\"')+'"')
|
||||
|
||||
class MagicTestError(MagicError): pass
|
||||
|
||||
class MagicTest:
|
||||
def __init__(self,offset,mtype,test,message,line=None,level=None):
|
||||
self.line, self.level = line, level
|
||||
self.mtype = mtype
|
||||
self.mtest = test
|
||||
self.subtests = []
|
||||
self.mask = None
|
||||
self.smod = None
|
||||
self.nmod = None
|
||||
self.offset, self.type, self.test, self.message = \
|
||||
offset,mtype,test,message
|
||||
if self.mtype == 'true' : return # XXX hack to enable level skips
|
||||
if test[-1:]=='\\' and test[-2:]!='\\\\' :
|
||||
self.test += 'n' # looks like someone wanted EOL to match?
|
||||
if mtype[:6]=='string' :
|
||||
if '/' in mtype : # for strings
|
||||
self.type, self.smod = \
|
||||
mtype[:mtype.find('/')], mtype[mtype.find('/')+1:]
|
||||
else:
|
||||
for nm in '&+-' :
|
||||
if nm in mtype : # for integer-based
|
||||
self.nmod, self.type, self.mask = (
|
||||
nm,
|
||||
mtype[:mtype.find(nm)],
|
||||
# convert mask to int, autodetect base
|
||||
int( mtype[mtype.find(nm)+1:], 0 )
|
||||
)
|
||||
break
|
||||
self.struct, self.size, self.cast = KnownTypes[ self.type ]
|
||||
def __str__(self):
|
||||
return '%s %s %s %s' % (
|
||||
self.offset, self.mtype, self.mtest, self.message
|
||||
)
|
||||
def __repr__(self):
|
||||
return 'MagicTest(%s,%s,%s,%s,line=%s,level=%s,subtests=\n%s%s)' % (
|
||||
`self.offset`, `self.mtype`, `self.mtest`, `self.message`,
|
||||
`self.line`, `self.level`,
|
||||
'\t'*self.level, pprint.pformat(self.subtests)
|
||||
)
|
||||
def run(self,file):
|
||||
result = ''
|
||||
do_close = 0
|
||||
try:
|
||||
if type(file) == type('x') :
|
||||
file = open( file, 'r', BUFFER_SIZE )
|
||||
do_close = 1
|
||||
# else:
|
||||
# saved_pos = file.tell()
|
||||
if self.mtype != 'true' :
|
||||
data = self.read(file)
|
||||
last = file.tell()
|
||||
else:
|
||||
data = last = None
|
||||
if self.check( data ) :
|
||||
result = self.message+' '
|
||||
if has_format( result ) : result %= data
|
||||
for test in self.subtests :
|
||||
m = test.run(file)
|
||||
if m is not None : result += m
|
||||
return make_string( result )
|
||||
finally:
|
||||
if do_close :
|
||||
file.close()
|
||||
# else:
|
||||
# file.seek( saved_pos, 0 )
|
||||
def get_mod_and_value(self):
|
||||
if self.type[-6:] == 'string' :
|
||||
# "something like\tthis\n"
|
||||
if self.test[0] in '=<>' :
|
||||
mod, value = self.test[0], make_string( self.test[1:] )
|
||||
else:
|
||||
mod, value = '=', make_string( self.test )
|
||||
else:
|
||||
if self.test[0] in '=<>&^' :
|
||||
mod, value = self.test[0], a2i(self.test[1:])
|
||||
elif self.test[0] == 'x':
|
||||
mod = self.test[0]
|
||||
value = 0
|
||||
else:
|
||||
mod, value = '=', a2i(self.test)
|
||||
return mod, value
|
||||
def read(self,file):
|
||||
mew( 's' )
|
||||
file.seek( self.offset(file), 0 ) # SEEK_SET
|
||||
mew( 'r' )
|
||||
try:
|
||||
data = rdata = None
|
||||
# XXX self.size might be 0 here...
|
||||
if self.size == 0 :
|
||||
# this is an ASCIIZ string...
|
||||
size = None
|
||||
if self.test != '>\\0' : # magic's hack for string read...
|
||||
value = self.get_mod_and_value()[1]
|
||||
size = (value=='\0') and None or len(value)
|
||||
rdata = data = read_asciiz( file, size=size )
|
||||
else:
|
||||
rdata = file.read( self.size )
|
||||
if not rdata or (len(rdata)!=self.size) : return None
|
||||
data = struct.unpack( self.struct, rdata )[0] # XXX hack??
|
||||
except:
|
||||
print >>sys.stderr, self
|
||||
print >>sys.stderr, '@%s struct=%s size=%d rdata=%s' % (
|
||||
self.offset, `self.struct`, self.size,`rdata`)
|
||||
raise
|
||||
mew( 'R' )
|
||||
if self.cast : data = self.cast( data )
|
||||
if self.mask :
|
||||
try:
|
||||
if self.nmod == '&' : data &= self.mask
|
||||
elif self.nmod == '+' : data += self.mask
|
||||
elif self.nmod == '-' : data -= self.mask
|
||||
else: raise MagicTestError(self.nmod)
|
||||
except:
|
||||
print >>sys.stderr,'data=%s nmod=%s mask=%s' % (
|
||||
`data`, `self.nmod`, `self.mask`
|
||||
)
|
||||
raise
|
||||
return data
|
||||
def check(self,data):
|
||||
mew('.')
|
||||
if self.mtype == 'true' :
|
||||
return '' # not None !
|
||||
mod, value = self.get_mod_and_value()
|
||||
if self.type[-6:] == 'string' :
|
||||
# "something like\tthis\n"
|
||||
if self.smod :
|
||||
xdata = data
|
||||
if 'b' in self.smod : # all blanks are optional
|
||||
xdata = ''.join( data.split() )
|
||||
value = ''.join( value.split() )
|
||||
if 'c' in self.smod : # all blanks are optional
|
||||
xdata = xdata.upper()
|
||||
value = value.upper()
|
||||
# if 'B' in self.smod : # compact blanks
|
||||
### XXX sorry, i don't understand this :-(
|
||||
# data = ' '.join( data.split() )
|
||||
# if ' ' not in data : return None
|
||||
else:
|
||||
xdata = data
|
||||
try:
|
||||
if mod == '=' : result = data == value
|
||||
elif mod == '<' : result = data < value
|
||||
elif mod == '>' : result = data > value
|
||||
elif mod == '&' : result = data & value
|
||||
elif mod == '^' : result = (data & (~value)) == 0
|
||||
elif mod == 'x' : result = 1
|
||||
else : raise MagicTestError(self.test)
|
||||
if result :
|
||||
zdata, zval = `data`, `value`
|
||||
if self.mtype[-6:]!='string' :
|
||||
try: zdata, zval = hex(data), hex(value)
|
||||
except: zdata, zval = `data`, `value`
|
||||
if 0 : print >>sys.stderr, '%s @%s %s:%s %s %s => %s (%s)' % (
|
||||
'>'*self.level, self.offset,
|
||||
zdata, self.mtype, `mod`, zval, `result`,
|
||||
self.message
|
||||
)
|
||||
return result
|
||||
except:
|
||||
print >>sys.stderr,'mtype=%s data=%s mod=%s value=%s' % (
|
||||
`self.mtype`, `data`, `mod`, `value`
|
||||
)
|
||||
raise
|
||||
def add(self,mt):
|
||||
if not isinstance(mt,MagicTest) :
|
||||
raise MagicTestError((mt,'incorrect subtest type %s'%(type(mt),)))
|
||||
if mt.level == self.level+1 :
|
||||
self.subtests.append( mt )
|
||||
elif self.subtests :
|
||||
self.subtests[-1].add( mt )
|
||||
elif mt.level > self.level+1 :
|
||||
# it's possible to get level 3 just after level 1 !!! :-(
|
||||
level = self.level + 1
|
||||
while level < mt.level :
|
||||
xmt = MagicTest(None,'true','x','',line=self.line,level=level)
|
||||
self.add( xmt )
|
||||
level += 1
|
||||
else:
|
||||
self.add( mt ) # retry...
|
||||
else:
|
||||
raise MagicTestError((mt,'incorrect subtest level %s'%(`mt.level`,)))
|
||||
def last_test(self):
|
||||
return self.subtests[-1]
|
||||
#end class MagicTest
|
||||
|
||||
class OffsetError(MagicError): pass
|
||||
|
||||
class Offset:
|
||||
pos_format = {'b':'<B','B':'>B','s':'<H','S':'>H','l':'<I','L':'>I',}
|
||||
pattern0 = re.compile(r''' # mere offset
|
||||
^
|
||||
&? # possible ampersand
|
||||
( 0 # just zero
|
||||
| [1-9]{1,1}[0-9]* # decimal
|
||||
| 0[0-7]+ # octal
|
||||
| 0x[0-9a-f]+ # hex
|
||||
)
|
||||
$
|
||||
''', re.X|re.I
|
||||
)
|
||||
pattern1 = re.compile(r''' # indirect offset
|
||||
^\(
|
||||
(?P<base>&?0 # just zero
|
||||
|&?[1-9]{1,1}[0-9]* # decimal
|
||||
|&?0[0-7]* # octal
|
||||
|&?0x[0-9A-F]+ # hex
|
||||
)
|
||||
(?P<type>
|
||||
\. # this dot might be alone
|
||||
[BSL]? # one of this chars in either case
|
||||
)?
|
||||
(?P<sign>
|
||||
[-+]{0,1}
|
||||
)?
|
||||
(?P<off>0 # just zero
|
||||
|[1-9]{1,1}[0-9]* # decimal
|
||||
|0[0-7]* # octal
|
||||
|0x[0-9a-f]+ # hex
|
||||
)?
|
||||
\)$''', re.X|re.I
|
||||
)
|
||||
def __init__(self,s):
|
||||
self.source = s
|
||||
self.value = None
|
||||
self.relative = 0
|
||||
self.base = self.type = self.sign = self.offs = None
|
||||
m = Offset.pattern0.match( s )
|
||||
if m : # just a number
|
||||
if s[0] == '&' :
|
||||
self.relative, self.value = 1, int( s[1:], 0 )
|
||||
else:
|
||||
self.value = int( s, 0 )
|
||||
return
|
||||
m = Offset.pattern1.match( s )
|
||||
if m : # real indirect offset
|
||||
try:
|
||||
self.base = m.group('base')
|
||||
if self.base[0] == '&' :
|
||||
self.relative, self.base = 1, int( self.base[1:], 0 )
|
||||
else:
|
||||
self.base = int( self.base, 0 )
|
||||
if m.group('type') : self.type = m.group('type')[1:]
|
||||
self.sign = m.group('sign')
|
||||
if m.group('off') : self.offs = int( m.group('off'), 0 )
|
||||
if self.sign == '-' : self.offs = 0 - self.offs
|
||||
except:
|
||||
print >>sys.stderr, '$$', m.groupdict()
|
||||
raise
|
||||
return
|
||||
raise OffsetError(`s`)
|
||||
def __call__(self,file=None):
|
||||
if self.value is not None : return self.value
|
||||
pos = file.tell()
|
||||
try:
|
||||
if not self.relative : file.seek( self.offset, 0 )
|
||||
frmt = Offset.pos_format.get( self.type, 'I' )
|
||||
size = struct.calcsize( frmt )
|
||||
data = struct.unpack( frmt, file.read( size ) )
|
||||
if self.offs : data += self.offs
|
||||
return data
|
||||
finally:
|
||||
file.seek( pos, 0 )
|
||||
def __str__(self): return self.source
|
||||
def __repr__(self): return 'Offset(%s)' % `self.source`
|
||||
#end class Offset
|
||||
|
||||
class MagicFileError(MagicError): pass
|
||||
|
||||
class MagicFile:
|
||||
def __init__(self,filename=_magic):
|
||||
self.file = None
|
||||
self.tests = []
|
||||
self.total_tests = 0
|
||||
self.load( filename )
|
||||
self.ack_tests = None
|
||||
self.nak_tests = None
|
||||
def __del__(self):
|
||||
self.close()
|
||||
def load(self,filename=None):
|
||||
self.open( filename )
|
||||
self.parse()
|
||||
self.close()
|
||||
def open(self,filename=None):
|
||||
self.close()
|
||||
if filename is not None :
|
||||
self.filename = filename
|
||||
self.file = open( self.filename, 'r', BUFFER_SIZE )
|
||||
def close(self):
|
||||
if self.file :
|
||||
self.file.close()
|
||||
self.file = None
|
||||
def parse(self):
|
||||
line_no = 0
|
||||
for line in self.file.xreadlines() :
|
||||
line_no += 1
|
||||
if not line or line[0]=='#' : continue
|
||||
line = line.lstrip().rstrip('\r\n')
|
||||
if not line or line[0]=='#' : continue
|
||||
try:
|
||||
x = self.parse_line( line )
|
||||
if x is None :
|
||||
print >>sys.stderr, '#[%04d]#'%line_no, line
|
||||
continue
|
||||
except:
|
||||
print >>sys.stderr, '###[%04d]###'%line_no, line
|
||||
raise
|
||||
self.total_tests += 1
|
||||
level, offset, mtype, test, message = x
|
||||
new_test = MagicTest(offset,mtype,test,message,
|
||||
line=line_no,level=level)
|
||||
try:
|
||||
if level == 0 :
|
||||
self.tests.append( new_test )
|
||||
else:
|
||||
self.tests[-1].add( new_test )
|
||||
except:
|
||||
if 1 :
|
||||
print >>sys.stderr, 'total tests=%s' % (
|
||||
`self.total_tests`,
|
||||
)
|
||||
print >>sys.stderr, 'level=%s' % (
|
||||
`level`,
|
||||
)
|
||||
print >>sys.stderr, 'tests=%s' % (
|
||||
pprint.pformat(self.tests),
|
||||
)
|
||||
raise
|
||||
else:
|
||||
while self.tests[-1].level > 0 :
|
||||
self.tests.pop()
|
||||
def parse_line(self,line):
|
||||
# print >>sys.stderr, 'line=[%s]' % line
|
||||
if (not line) or line[0]=='#' : return None
|
||||
level = 0
|
||||
offset = mtype = test = message = ''
|
||||
mask = None
|
||||
# get optional level (count leading '>')
|
||||
while line and line[0]=='>' :
|
||||
line, level = line[1:], level+1
|
||||
# get offset
|
||||
while line and not line[0].isspace() :
|
||||
offset, line = offset+line[0], line[1:]
|
||||
try:
|
||||
offset = Offset(offset)
|
||||
except:
|
||||
print >>sys.stderr, 'line=[%s]' % line
|
||||
raise
|
||||
# skip spaces
|
||||
line = line.lstrip()
|
||||
# get type
|
||||
c = None
|
||||
while line :
|
||||
last_c, c, line = c, line[0], line[1:]
|
||||
if last_c!='\\' and c.isspace() :
|
||||
break # unescaped space - end of field
|
||||
else:
|
||||
mtype += c
|
||||
if last_c == '\\' :
|
||||
c = None # don't fuck my brain with sequential backslashes
|
||||
# skip spaces
|
||||
line = line.lstrip()
|
||||
# get test
|
||||
c = None
|
||||
while line :
|
||||
last_c, c, line = c, line[0], line[1:]
|
||||
if last_c!='\\' and c.isspace() :
|
||||
break # unescaped space - end of field
|
||||
else:
|
||||
test += c
|
||||
if last_c == '\\' :
|
||||
c = None # don't fuck my brain with sequential backslashes
|
||||
# skip spaces
|
||||
line = line.lstrip()
|
||||
# get message
|
||||
message = line
|
||||
if mime and line.find("\t") != -1:
|
||||
message=line[0:line.find("\t")]
|
||||
#
|
||||
# print '>>', level, offset, mtype, test, message
|
||||
return level, offset, mtype, test, message
|
||||
def detect(self,file):
|
||||
self.ack_tests = 0
|
||||
self.nak_tests = 0
|
||||
answers = []
|
||||
for test in self.tests :
|
||||
message = test.run( file )
|
||||
if message :
|
||||
self.ack_tests += 1
|
||||
answers.append( message )
|
||||
else:
|
||||
self.nak_tests += 1
|
||||
if answers :
|
||||
return '; '.join( answers )
|
||||
#end class MagicFile
|
||||
|
||||
def username(uid):
|
||||
try:
|
||||
return pwd.getpwuid( uid )[0]
|
||||
except:
|
||||
return '#%s'%uid
|
||||
|
||||
def groupname(gid):
|
||||
try:
|
||||
return grp.getgrgid( gid )[0]
|
||||
except:
|
||||
return '#%s'%gid
|
||||
|
||||
def get_file_type(fname,follow):
|
||||
t = None
|
||||
if not follow :
|
||||
try:
|
||||
st = os.lstat( fname ) # stat that entry, don't follow links!
|
||||
except os.error, why :
|
||||
pass
|
||||
else:
|
||||
if stat.S_ISLNK(st[stat.ST_MODE]) :
|
||||
t = 'symbolic link'
|
||||
try:
|
||||
lnk = os.readlink( fname )
|
||||
except:
|
||||
t += ' (unreadable)'
|
||||
else:
|
||||
t += ' to '+lnk
|
||||
if t is None :
|
||||
try:
|
||||
st = os.stat( fname )
|
||||
except os.error, why :
|
||||
return "can't stat `%s' (%s)." % (why.filename,why.strerror)
|
||||
|
||||
dmaj, dmin = (st.st_rdev>>8)&0x0FF, st.st_rdev&0x0FF
|
||||
|
||||
if 0 : pass
|
||||
elif stat.S_ISSOCK(st.st_mode) : t = 'socket'
|
||||
elif stat.S_ISLNK (st.st_mode) : t = follow and 'symbolic link' or t
|
||||
elif stat.S_ISREG (st.st_mode) : t = 'file'
|
||||
elif stat.S_ISBLK (st.st_mode) : t = 'block special (%d/%d)'%(dmaj,dmin)
|
||||
elif stat.S_ISDIR (st.st_mode) : t = 'directory'
|
||||
elif stat.S_ISCHR (st.st_mode) : t = 'character special (%d/%d)'%(dmaj,dmin)
|
||||
elif stat.S_ISFIFO(st.st_mode) : t = 'pipe'
|
||||
else: t = '<unknown>'
|
||||
|
||||
if st.st_mode & stat.S_ISUID :
|
||||
t = 'setuid(%d=%s) %s'%(st.st_uid,username(st.st_uid),t)
|
||||
if st.st_mode & stat.S_ISGID :
|
||||
t = 'setgid(%d=%s) %s'%(st.st_gid,groupname(st.st_gid),t)
|
||||
if st.st_mode & stat.S_ISVTX :
|
||||
t = 'sticky '+t
|
||||
|
||||
return t
|
||||
|
||||
HELP = '''%s [options] [files...]
|
||||
|
||||
Options:
|
||||
|
||||
-?, --help -- this help
|
||||
-m, --magic=<file> -- use this magic <file> instead of %s
|
||||
-f, --files=<namefile> -- read filenames for <namefile>
|
||||
* -C, --compile -- write "compiled" magic file
|
||||
-b, --brief -- don't prepend filenames to output lines
|
||||
+ -c, --check -- check the magic file
|
||||
-i, --mime -- output MIME types
|
||||
* -k, --keep-going -- don't stop st the first match
|
||||
-n, --flush -- flush stdout after each line
|
||||
-v, --verson -- print version and exit
|
||||
* -z, --compressed -- try to look inside compressed files
|
||||
-L, --follow -- follow symlinks
|
||||
-s, --special -- don't skip special files
|
||||
|
||||
* -- not implemented so far ;-)
|
||||
+ -- implemented, but in another way...
|
||||
'''
|
||||
|
||||
def main():
|
||||
import getopt
|
||||
global _magic
|
||||
try:
|
||||
brief = 0
|
||||
flush = 0
|
||||
follow= 0
|
||||
mime = 0
|
||||
check = 0
|
||||
special=0
|
||||
try:
|
||||
opts, args = getopt.getopt(
|
||||
sys.argv[1:],
|
||||
'?m:f:CbciknvzLs',
|
||||
( 'help',
|
||||
'magic=',
|
||||
'names=',
|
||||
'compile',
|
||||
'brief',
|
||||
'check',
|
||||
'mime',
|
||||
'keep-going',
|
||||
'flush',
|
||||
'version',
|
||||
'compressed',
|
||||
'follow',
|
||||
'special',
|
||||
)
|
||||
)
|
||||
except getopt.error, why:
|
||||
print >>sys.stderr, sys.argv[0], why
|
||||
return 1
|
||||
else:
|
||||
files = None
|
||||
for o,v in opts :
|
||||
if o in ('-?','--help'):
|
||||
print HELP % (
|
||||
sys.argv[0],
|
||||
_magic,
|
||||
)
|
||||
return 0
|
||||
elif o in ('-f','--files='):
|
||||
files = v
|
||||
elif o in ('-m','--magic='):
|
||||
_magic = v[:]
|
||||
elif o in ('-C','--compile'):
|
||||
pass
|
||||
elif o in ('-b','--brief'):
|
||||
brief = 1
|
||||
elif o in ('-c','--check'):
|
||||
check = 1
|
||||
elif o in ('-i','--mime'):
|
||||
mime = 1
|
||||
if os.path.exists( _magic+'.mime' ) :
|
||||
_magic += '.mime'
|
||||
print >>sys.stderr,sys.argv[0]+':',\
|
||||
"Using regular magic file `%s'" % _magic
|
||||
elif o in ('-k','--keep-going'):
|
||||
pass
|
||||
elif o in ('-n','--flush'):
|
||||
flush = 1
|
||||
elif o in ('-v','--version'):
|
||||
print 'VERSION'
|
||||
return 0
|
||||
elif o in ('-z','--compressed'):
|
||||
pass
|
||||
elif o in ('-L','--follow'):
|
||||
follow = 1
|
||||
elif o in ('-s','--special'):
|
||||
special = 1
|
||||
else:
|
||||
if files :
|
||||
files = map(lambda x: x.strip(), v.split(','))
|
||||
if '-' in files and '-' in args :
|
||||
error( 1, 'cannot use STDIN simultaneously for file list and data' )
|
||||
for file in files :
|
||||
for name in (
|
||||
(file=='-')
|
||||
and sys.stdin
|
||||
or open(file,'r',BUFFER_SIZE)
|
||||
).xreadlines():
|
||||
name = name.strip()
|
||||
if name not in args :
|
||||
args.append( name )
|
||||
try:
|
||||
if check : print >>sys.stderr, 'Loading magic database...'
|
||||
t0 = time.time()
|
||||
m = MagicFile(_magic)
|
||||
t1 = time.time()
|
||||
if check :
|
||||
print >>sys.stderr, \
|
||||
m.total_tests, 'tests loaded', \
|
||||
'for', '%.2f' % (t1-t0), 'seconds'
|
||||
print >>sys.stderr, len(m.tests), 'tests at top level'
|
||||
return 0 # XXX "shortened" form ;-)
|
||||
|
||||
mlen = max( map(len, args) )+1
|
||||
for arg in args :
|
||||
if not brief : print (arg + ':').ljust(mlen),
|
||||
ftype = get_file_type( arg, follow )
|
||||
if (special and ftype.find('special')>=0) \
|
||||
or ftype[-4:] == 'file' :
|
||||
t0 = time.time()
|
||||
try:
|
||||
t = m.detect( arg )
|
||||
except (IOError,os.error), why:
|
||||
t = "can't read `%s' (%s)" % (why.filename,why.strerror)
|
||||
if ftype[-4:] == 'file' : t = ftype[:-4] + t
|
||||
t1 = time.time()
|
||||
print t and t or 'data'
|
||||
if 0 : print \
|
||||
'#\t%d tests ok, %d tests failed for %.2f seconds'%\
|
||||
(m.ack_tests, m.nak_tests, t1-t0)
|
||||
else:
|
||||
print mime and 'application/x-not-regular-file' or ftype
|
||||
if flush : sys.stdout.flush()
|
||||
# print >>sys.stderr, 'DONE'
|
||||
except:
|
||||
if check : return 1
|
||||
raise
|
||||
else:
|
||||
return 0
|
||||
finally:
|
||||
pass
|
||||
|
||||
if __name__ == '__main__' :
|
||||
sys.exit( main() )
|
||||
# vim:ai
|
||||
# EOF #
|
||||
@@ -1,104 +0,0 @@
|
||||
#! /usr/bin/env python2.2
|
||||
|
||||
# Copyright 1994 by Lance Ellinghouse
|
||||
# Cathedral City, California Republic, United States of America.
|
||||
# All Rights Reserved
|
||||
# Permission to use, copy, modify, and distribute this software and its
|
||||
# documentation for any purpose and without fee is hereby granted,
|
||||
# provided that the above copyright notice appear in all copies and that
|
||||
# both that copyright notice and this permission notice appear in
|
||||
# supporting documentation, and that the name of Lance Ellinghouse
|
||||
# not be used in advertising or publicity pertaining to distribution
|
||||
# of the software without specific, written prior permission.
|
||||
# LANCE ELLINGHOUSE DISCLAIMS ALL WARRANTIES WITH REGARD TO
|
||||
# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
# FITNESS, IN NO EVENT SHALL LANCE ELLINGHOUSE CENTRUM BE LIABLE
|
||||
# FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
|
||||
# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
#
|
||||
# Modified by Jack Jansen, CWI, July 1995:
|
||||
# - Use binascii module to do the actual line-by-line conversion
|
||||
# between ascii and binary. This results in a 1000-fold speedup. The C
|
||||
# version is still 5 times faster, though.
|
||||
# - Arguments more compliant with python standard
|
||||
|
||||
"""Implementation of the UUencode and UUdecode functions.
|
||||
|
||||
encode(in_file, out_file [,name, mode])
|
||||
decode(in_file [, out_file, mode])
|
||||
"""
|
||||
|
||||
import binascii
|
||||
import os
|
||||
import sys
|
||||
from types import StringType
|
||||
|
||||
__all__ = ["Error", "decode"]
|
||||
|
||||
class Error(Exception):
|
||||
pass
|
||||
|
||||
def decode(in_file, out_file=None, mode=None, quiet=0):
|
||||
"""Decode uuencoded file"""
|
||||
#
|
||||
# Open the input file, if needed.
|
||||
#
|
||||
if in_file == '-':
|
||||
in_file = sys.stdin
|
||||
elif isinstance(in_file, StringType):
|
||||
in_file = open(in_file)
|
||||
#
|
||||
# Read until a begin is encountered or we've exhausted the file
|
||||
#
|
||||
while 1:
|
||||
hdr = in_file.readline()
|
||||
if not hdr:
|
||||
raise Error, 'No valid begin line found in input file'
|
||||
if hdr[:5] != 'begin':
|
||||
continue
|
||||
hdrfields = hdr.split(" ", 2)
|
||||
if len(hdrfields) == 3 and hdrfields[0] == 'begin':
|
||||
try:
|
||||
int(hdrfields[1], 8)
|
||||
start_pos = in_file.tell() - len (hdr)
|
||||
break
|
||||
except ValueError:
|
||||
pass
|
||||
if out_file is None:
|
||||
out_file = hdrfields[2].rstrip()
|
||||
if os.path.exists(out_file):
|
||||
raise Error, 'Cannot overwrite existing file: %s' % out_file
|
||||
if mode is None:
|
||||
mode = int(hdrfields[1], 8)
|
||||
#
|
||||
# Open the output file
|
||||
#
|
||||
if out_file == '-':
|
||||
out_file = sys.stdout
|
||||
elif isinstance(out_file, StringType):
|
||||
fp = open(out_file, 'wb')
|
||||
try:
|
||||
os.path.chmod(out_file, mode)
|
||||
except AttributeError:
|
||||
pass
|
||||
out_file = fp
|
||||
#
|
||||
# Main decoding loop
|
||||
#
|
||||
s = in_file.readline()
|
||||
while s and s.strip() != 'end':
|
||||
try:
|
||||
data = binascii.a2b_uu(s)
|
||||
except binascii.Error, v:
|
||||
# Workaround for broken uuencoders by /Fredrik Lundh
|
||||
nbytes = (((ord(s[0])-32) & 63) * 4 + 5) / 3
|
||||
data = binascii.a2b_uu(s[:nbytes])
|
||||
if not quiet:
|
||||
sys.stderr.write("Warning: %s\n" % str(v))
|
||||
out_file.write(data)
|
||||
s = in_file.readline()
|
||||
# if not s:
|
||||
# raise Error, 'Truncated input file'
|
||||
return (hdrfields[2].rstrip(), start_pos, in_file.tell())
|
||||
@@ -1,308 +0,0 @@
|
||||
#!/usr/local/bin/python
|
||||
# -*- mode: python -*-
|
||||
|
||||
"""
|
||||
jb2bz.py - a nonce script to import bugs from JitterBug to Bugzilla
|
||||
Written by Tom Emerson, tree@basistech.com
|
||||
|
||||
This script is provided in the hopes that it will be useful. No
|
||||
rights reserved. No guarantees expressed or implied. Use at your own
|
||||
risk. May be dangerous if swallowed. If it doesn't work for you, don't
|
||||
blame me. It did what I needed it to do.
|
||||
|
||||
This code requires a recent version of Andy Dustman's MySQLdb interface,
|
||||
|
||||
http://sourceforge.net/projects/mysql-python
|
||||
|
||||
Share and enjoy.
|
||||
"""
|
||||
|
||||
import rfc822, mimetools, multifile, mimetypes
|
||||
import sys, re, glob, StringIO, os, stat, time
|
||||
import MySQLdb, getopt
|
||||
|
||||
# mimetypes doesn't include everything we might encounter, yet.
|
||||
if not mimetypes.types_map.has_key('.doc'):
|
||||
mimetypes.types_map['.doc'] = 'application/msword'
|
||||
|
||||
if not mimetypes.encodings_map.has_key('.bz2'):
|
||||
mimetypes.encodings_map['.bz2'] = "bzip2"
|
||||
|
||||
bug_status='NEW'
|
||||
component="default"
|
||||
version=""
|
||||
product="" # this is required, the rest of these are defaulted as above
|
||||
|
||||
"""
|
||||
Each bug in JitterBug is stored as a text file named by the bug number.
|
||||
Additions to the bug are indicated by suffixes to this:
|
||||
|
||||
<bug>
|
||||
<bug>.followup.*
|
||||
<bug>.reply.*
|
||||
<bug>.notes
|
||||
|
||||
The dates on the files represent the respective dates they were created/added.
|
||||
|
||||
All <bug>s and <bug>.reply.*s include RFC 822 mail headers. These could include
|
||||
MIME file attachments as well that would need to be extracted.
|
||||
|
||||
There are other additions to the file names, such as
|
||||
|
||||
<bug>.notify
|
||||
|
||||
which are ignored.
|
||||
|
||||
Bugs in JitterBug are organized into directories. At Basis we used the following
|
||||
naming conventions:
|
||||
|
||||
<product>-bugs Open bugs
|
||||
<product>-requests Open Feature Requests
|
||||
<product>-resolved Bugs/Features marked fixed by engineering, but not verified
|
||||
<product>-verified Resolved defects that have been verified by QA
|
||||
|
||||
where <product> is either:
|
||||
|
||||
<product-name>
|
||||
|
||||
or
|
||||
|
||||
<product-name>-<version>
|
||||
"""
|
||||
|
||||
def process_notes_file(current, fname):
|
||||
try:
|
||||
new_note = {}
|
||||
notes = open(fname, "r")
|
||||
s = os.fstat(notes.fileno())
|
||||
|
||||
new_note['text'] = notes.read()
|
||||
new_note['timestamp'] = time.gmtime(s[stat.ST_MTIME])
|
||||
|
||||
notes.close()
|
||||
|
||||
current['notes'].append(new_note)
|
||||
|
||||
except IOError:
|
||||
pass
|
||||
|
||||
def process_reply_file(current, fname):
|
||||
new_note = {}
|
||||
reply = open(fname, "r")
|
||||
msg = rfc822.Message(reply)
|
||||
new_note['text'] = "%s\n%s" % (msg['From'], msg.fp.read())
|
||||
new_note['timestamp'] = rfc822.parsedate_tz(msg['Date'])
|
||||
current["notes"].append(new_note)
|
||||
|
||||
def add_notes(current):
|
||||
"""Add any notes that have been recorded for the current bug."""
|
||||
process_notes_file(current, "%d.notes" % current['number'])
|
||||
|
||||
for f in glob.glob("%d.reply.*" % current['number']):
|
||||
process_reply_file(current, f)
|
||||
|
||||
for f in glob.glob("%d.followup.*" % current['number']):
|
||||
process_reply_file(current, f)
|
||||
|
||||
def maybe_add_attachment(current, file, submsg):
|
||||
"""Adds the attachment to the current record"""
|
||||
cd = submsg["Content-Disposition"]
|
||||
m = re.search(r'filename="([^"]+)"', cd)
|
||||
if m == None:
|
||||
return
|
||||
attachment_filename = m.group(1)
|
||||
if (submsg.gettype() == 'application/octet-stream'):
|
||||
# try get a more specific content-type for this attachment
|
||||
type, encoding = mimetypes.guess_type(m.group(1))
|
||||
if type == None:
|
||||
type = submsg.gettype()
|
||||
else:
|
||||
type = submsg.gettype()
|
||||
|
||||
try:
|
||||
data = StringIO.StringIO()
|
||||
mimetools.decode(file, data, submsg.getencoding())
|
||||
except:
|
||||
return
|
||||
|
||||
current['attachments'].append( ( attachment_filename, type, data.getvalue() ) )
|
||||
|
||||
def process_mime_body(current, file, submsg):
|
||||
data = StringIO.StringIO()
|
||||
mimetools.decode(file, data, submsg.getencoding())
|
||||
current['description'] = data.getvalue()
|
||||
|
||||
|
||||
|
||||
def process_text_plain(msg, current):
|
||||
print "Processing: %d" % current['number']
|
||||
current['description'] = msg.fp.read()
|
||||
|
||||
def process_multi_part(file, msg, current):
|
||||
print "Processing: %d" % current['number']
|
||||
mf = multifile.MultiFile(file)
|
||||
mf.push(msg.getparam("boundary"))
|
||||
while mf.next():
|
||||
submsg = mimetools.Message(file)
|
||||
if submsg.has_key("Content-Disposition"):
|
||||
maybe_add_attachment(current, mf, submsg)
|
||||
else:
|
||||
# This is the message body itself (always?), so process
|
||||
# accordingly
|
||||
process_mime_body(current, mf, submsg)
|
||||
|
||||
def process_jitterbug(filename):
|
||||
current = {}
|
||||
current['number'] = int(filename)
|
||||
current['notes'] = []
|
||||
current['attachments'] = []
|
||||
current['description'] = ''
|
||||
current['date-reported'] = ()
|
||||
current['short-description'] = ''
|
||||
|
||||
file = open(filename, "r")
|
||||
msg = mimetools.Message(file)
|
||||
|
||||
msgtype = msg.gettype()
|
||||
|
||||
add_notes(current)
|
||||
current['date-reported'] = rfc822.parsedate_tz(msg['Date'])
|
||||
current['short-description'] = msg['Subject']
|
||||
|
||||
if msgtype[:5] == 'text/':
|
||||
process_text_plain(msg, current)
|
||||
elif msgtype[:10] == "multipart/":
|
||||
process_multi_part(file, msg, current)
|
||||
else:
|
||||
# Huh? This should never happen.
|
||||
print "Unknown content-type: %s" % msgtype
|
||||
sys.exit(1)
|
||||
|
||||
# At this point we have processed the message: we have all of the notes and
|
||||
# attachments stored, so it's time to add things to the database.
|
||||
# The schema for JitterBug 2.14 can be found at:
|
||||
#
|
||||
# http://www.trilobyte.net/barnsons/html/dbschema.html
|
||||
#
|
||||
# The following fields need to be provided by the user:
|
||||
#
|
||||
# bug_status
|
||||
# product
|
||||
# version
|
||||
# reporter
|
||||
# component
|
||||
# resolution
|
||||
|
||||
# change this to the user_id of the Bugzilla user who is blessed with the
|
||||
# imported defects
|
||||
reporter=6
|
||||
|
||||
# the resolution will need to be set manually
|
||||
resolution=""
|
||||
|
||||
db = MySQLdb.connect(db='bugs',user='root',host='localhost')
|
||||
cursor = db.cursor()
|
||||
|
||||
cursor.execute( "INSERT INTO bugs SET " \
|
||||
"bug_id=%s," \
|
||||
"bug_severity='normal'," \
|
||||
"bug_status=%s," \
|
||||
"creation_ts=%s," \
|
||||
"delta_ts=%s," \
|
||||
"short_desc=%s," \
|
||||
"product=%s," \
|
||||
"rep_platform='All'," \
|
||||
"assigned_to=%s,"
|
||||
"reporter=%s," \
|
||||
"version=%s," \
|
||||
"component=%s," \
|
||||
"resolution=%s",
|
||||
[ current['number'],
|
||||
bug_status,
|
||||
time.strftime("%Y-%m-%d %H:%M:%S", current['date-reported'][:9]),
|
||||
time.strftime("%Y-%m-%d %H:%M:%S", current['date-reported'][:9]),
|
||||
current['short-description'],
|
||||
product,
|
||||
reporter,
|
||||
reporter,
|
||||
version,
|
||||
component,
|
||||
resolution] )
|
||||
|
||||
# This is the initial long description associated with the bug report
|
||||
cursor.execute( "INSERT INTO longdescs VALUES (%s,%s,%s,%s)",
|
||||
[ current['number'],
|
||||
reporter,
|
||||
time.strftime("%Y-%m-%d %H:%M:%S", current['date-reported'][:9]),
|
||||
current['description'] ] )
|
||||
|
||||
# Add whatever notes are associated with this defect
|
||||
for n in current['notes']:
|
||||
cursor.execute( "INSERT INTO longdescs VALUES (%s,%s,%s,%s)",
|
||||
[current['number'],
|
||||
reporter,
|
||||
time.strftime("%Y-%m-%d %H:%M:%S", n['timestamp'][:9]),
|
||||
n['text']])
|
||||
|
||||
# add attachments associated with this defect
|
||||
for a in current['attachments']:
|
||||
cursor.execute( "INSERT INTO attachments SET " \
|
||||
"bug_id=%s, creation_ts=%s, description='', mimetype=%s," \
|
||||
"filename=%s, submitter_id=%s",
|
||||
[ current['number'],
|
||||
time.strftime("%Y-%m-%d %H:%M:%S", current['date-reported'][:9]),
|
||||
a[1], a[0], reporter ])
|
||||
cursor.execute( "INSERT INTO attach_data SET " \
|
||||
"id=LAST_INSERT_ID(), thedata=%s",
|
||||
[ a[2] ])
|
||||
|
||||
cursor.close()
|
||||
db.close()
|
||||
|
||||
def usage():
|
||||
print """Usage: jb2bz.py [OPTIONS] Product
|
||||
|
||||
Where OPTIONS are one or more of the following:
|
||||
|
||||
-h This help information.
|
||||
-s STATUS One of UNCONFIRMED, NEW, ASSIGNED, REOPENED, RESOLVED, VERIFIED, CLOSED
|
||||
(default is NEW)
|
||||
-c COMPONENT The component to attach to each bug as it is important. This should be
|
||||
valid component for the Product.
|
||||
-v VERSION Version to assign to these defects.
|
||||
|
||||
Product is the Product to assign these defects to.
|
||||
|
||||
All of the JitterBugs in the current directory are imported, including replies, notes,
|
||||
attachments, and similar noise.
|
||||
"""
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
global bug_status, component, version, product
|
||||
opts, args = getopt.getopt(sys.argv[1:], "hs:c:v:")
|
||||
|
||||
for o,a in opts:
|
||||
if o == "-s":
|
||||
if a in ('UNCONFIRMED','NEW','ASSIGNED','REOPENED','RESOLVED','VERIFIED','CLOSED'):
|
||||
bug_status = a
|
||||
elif o == '-c':
|
||||
component = a
|
||||
elif o == '-v':
|
||||
version = a
|
||||
elif o == '-h':
|
||||
usage()
|
||||
|
||||
if len(args) != 1:
|
||||
sys.stderr.write("Must specify the Product.\n")
|
||||
sys.exit(1)
|
||||
|
||||
product = args[0]
|
||||
|
||||
for bug in filter(lambda x: re.match(r"\d+$", x), glob.glob("*")):
|
||||
process_jitterbug(bug)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,245 +0,0 @@
|
||||
#!/usr/bin/perl -wT
|
||||
# -*- 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): Myk Melez <myk@mozilla.org>
|
||||
# Frédéric Buclin <LpSolit@gmail.com>
|
||||
|
||||
use strict;
|
||||
|
||||
=head1 NAME
|
||||
|
||||
merge-users.pl - Merge two user accounts.
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
This script moves activity from one user account to another.
|
||||
Specify the two accounts on the command line, e.g.:
|
||||
|
||||
./merge-users.pl old_account@foo.com new_account@bar.com
|
||||
or:
|
||||
./merge-users.pl id:old_userid id:new_userid
|
||||
or:
|
||||
./merge-users.pl id:old_userid new_account@bar.com
|
||||
|
||||
Notes: - the new account must already exist.
|
||||
- the id:old_userid syntax permits you to migrate
|
||||
activity from a deleted account to an existing one.
|
||||
|
||||
=cut
|
||||
|
||||
use lib qw(.);
|
||||
|
||||
use Bugzilla;
|
||||
use Bugzilla::Config qw(:DEFAULT);
|
||||
use Bugzilla::Util;
|
||||
|
||||
use Getopt::Long;
|
||||
use Pod::Usage;
|
||||
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
# Display the help if called with --help or -?.
|
||||
my $help = 0;
|
||||
my $result = GetOptions("help|?" => \$help);
|
||||
pod2usage(0) if $help;
|
||||
|
||||
|
||||
# We require Bugzilla 2.20 or higher (including 2.22+).
|
||||
my $current_version = $Bugzilla::Config::VERSION;
|
||||
if ($current_version =~ /^2\.2[0123]/) {
|
||||
print "OK, you are using Bugzilla $current_version\n"
|
||||
}
|
||||
else {
|
||||
die "You are using Bugzilla $current_version but Bugzilla " .
|
||||
"2.20 - 2.23 is required.\n";
|
||||
}
|
||||
|
||||
|
||||
# Make sure accounts were specified on the command line and exist.
|
||||
my $old = $ARGV[0] || die "You must specify an old user account.\n";
|
||||
my $old_id;
|
||||
if ($old =~ /^id:(\d+)$/) {
|
||||
# As the old user account may be a deleted one, we don't
|
||||
# check whether this user ID is valid or not.
|
||||
# If it never existed, no damage will be done.
|
||||
$old_id = $1;
|
||||
}
|
||||
else {
|
||||
trick_taint($old);
|
||||
$old_id = $dbh->selectrow_array('SELECT userid FROM profiles
|
||||
WHERE login_name = ?',
|
||||
undef, $old);
|
||||
}
|
||||
if ($old_id) {
|
||||
print "OK, old user account $old found; user ID: $old_id.\n";
|
||||
}
|
||||
else {
|
||||
die "The old user account $old does not exist.\n";
|
||||
}
|
||||
|
||||
my $new = $ARGV[1] || die "You must specify a new user account.\n";
|
||||
my $new_id;
|
||||
if ($new =~ /^id:(\d+)$/) {
|
||||
$new_id = $1;
|
||||
# Make sure this user ID exists.
|
||||
$new_id = $dbh->selectrow_array('SELECT userid FROM profiles
|
||||
WHERE userid = ?',
|
||||
undef, $new_id);
|
||||
}
|
||||
else {
|
||||
trick_taint($new);
|
||||
$new_id = $dbh->selectrow_array('SELECT userid FROM profiles
|
||||
WHERE login_name = ?',
|
||||
undef, $new);
|
||||
}
|
||||
if ($new_id) {
|
||||
print "OK, new user account $new found; user ID: $new_id.\n";
|
||||
}
|
||||
else {
|
||||
die "The new user account $new does not exist.\n";
|
||||
}
|
||||
|
||||
# Make sure the old and new accounts are different.
|
||||
if ($old_id == $new_id) {
|
||||
die "\nBoth accounts are identical. There is nothing to migrate.\n";
|
||||
}
|
||||
|
||||
|
||||
# A list of tables and columns to be changed:
|
||||
# - keys of the hash are table names to be locked/altered;
|
||||
# - values of the hash contain column names to be updated
|
||||
# as well as the columns they depend on:
|
||||
# = each array is of the form:
|
||||
# ['foo1 bar11 bar12 bar13', 'foo2 bar21 bar22', 'foo3 bar31 bar32']
|
||||
# where fooN is the column to update, and barN1, barN2, ... are
|
||||
# the columns to take into account to avoid duplicated entries.
|
||||
# Note that the barNM columns are optional.
|
||||
my $changes = {
|
||||
# Tables affecting bugs.
|
||||
bugs => ['assigned_to', 'reporter', 'qa_contact'],
|
||||
bugs_activity => ['who'],
|
||||
attachments => ['submitter_id'],
|
||||
flags => ['setter_id', 'requestee_id'],
|
||||
cc => ['who bug_id'],
|
||||
longdescs => ['who'],
|
||||
votes => ['who'],
|
||||
# Tables affecting global behavior / other users.
|
||||
components => ['initialowner', 'initialqacontact'],
|
||||
quips => ['userid'],
|
||||
series => ['creator'],
|
||||
whine_events => ['owner_userid'],
|
||||
watch => ['watcher watched', 'watched watcher'],
|
||||
# Tables affecting the user directly.
|
||||
namedqueries => ['userid name'],
|
||||
user_group_map => ['user_id group_id isbless grant_type'],
|
||||
email_setting => ['user_id relationship event'],
|
||||
profile_setting => ['user_id setting_name'],
|
||||
profiles_activity => ['userid', 'who'], # Should activity be migrated?
|
||||
|
||||
# Only do it if mailto_type = 0, i.e is pointing to a user account!
|
||||
# This requires to be done separately due to this condition.
|
||||
whine_schedules => [], # ['mailto'],
|
||||
|
||||
# Delete all old records for these tables; no migration.
|
||||
logincookies => [], # ['userid'],
|
||||
tokens => [], # ['userid'],
|
||||
profiles => [], # ['userid'],
|
||||
};
|
||||
|
||||
# Lock tables
|
||||
my @locked_tables = map {"$_ WRITE"} keys(%$changes);
|
||||
$dbh->bz_lock_tables(@locked_tables);
|
||||
|
||||
# Delete old records from logincookies and tokens tables.
|
||||
$dbh->do('DELETE FROM logincookies WHERE userid = ?', undef, $old_id);
|
||||
$dbh->do('DELETE FROM tokens WHERE userid = ?', undef, $old_id);
|
||||
|
||||
# Migrate records from old user to new user.
|
||||
foreach my $table (keys(%$changes)) {
|
||||
foreach my $column_list (@{$changes->{$table}}) {
|
||||
# Get all columns to consider. There is always at least
|
||||
# one column given: the one to update.
|
||||
my @columns = split(/[\s]+/, $column_list);
|
||||
my $cols_to_check = join(' AND ', map {"$_ = ?"} @columns);
|
||||
# The first column of the list is the one to update.
|
||||
my $col_to_update = shift @columns;
|
||||
|
||||
# Will be used to migrate the old user account to the new one.
|
||||
my $sth_update = $dbh->prepare("UPDATE $table
|
||||
SET $col_to_update = ?
|
||||
WHERE $cols_to_check");
|
||||
|
||||
# Do we have additional columns to take care of?
|
||||
if (scalar(@columns)) {
|
||||
my $cols_to_query = join(', ', @columns);
|
||||
|
||||
# Get existing entries for the old user account.
|
||||
my $old_entries =
|
||||
$dbh->selectall_arrayref("SELECT $cols_to_query
|
||||
FROM $table
|
||||
WHERE $col_to_update = ?",
|
||||
undef, $old_id);
|
||||
|
||||
# Will be used to check whether the same entry exists
|
||||
# for the new user account.
|
||||
my $sth_select = $dbh->prepare("SELECT COUNT(*)
|
||||
FROM $table
|
||||
WHERE $cols_to_check");
|
||||
|
||||
# Will be used to delete duplicated entries.
|
||||
my $sth_delete = $dbh->prepare("DELETE FROM $table
|
||||
WHERE $cols_to_check");
|
||||
|
||||
foreach my $entry (@$old_entries) {
|
||||
my $exists = $dbh->selectrow_array($sth_select, undef,
|
||||
($new_id, @$entry));
|
||||
|
||||
if ($exists) {
|
||||
$sth_delete->execute($old_id, @$entry);
|
||||
}
|
||||
else {
|
||||
$sth_update->execute($new_id, $old_id, @$entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
# No check required. Update the column directly.
|
||||
else {
|
||||
$sth_update->execute($new_id, $old_id);
|
||||
}
|
||||
print "OK, records in the '$col_to_update' column of the '$table' table\n" .
|
||||
"have been migrated to the new user account.\n";
|
||||
}
|
||||
}
|
||||
|
||||
# Only update 'whine_schedules' if mailto_type = 0.
|
||||
# (i.e. is pointing to a user ID).
|
||||
$dbh->do('UPDATE whine_schedules SET mailto = ?
|
||||
WHERE mailto = ? AND mailto_type = ?',
|
||||
undef, ($new_id, $old_id, 0));
|
||||
print "OK, records in the 'mailto' column of the 'whine_schedules' table\n" .
|
||||
"have been migrated to the new user account.\n";
|
||||
|
||||
# Delete the old record from the profiles table.
|
||||
$dbh->do('DELETE FROM profiles WHERE userid = ?', undef, $old_id);
|
||||
|
||||
# Unlock tables
|
||||
$dbh->bz_unlock_tables();
|
||||
|
||||
print "Done.\n";
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user