Compare commits

..

2 Commits

Author SHA1 Message Date
sdv%sparc.spb.su
53866ece4f workaround for bug=30927
git-svn-id: svn://10.0.0.236/branches/M15-patch@72213 18797224-902f-48f8-a5cc-f745e15eee43
2000-06-14 11:34:36 +00:00
(no author)
350be55313 This commit was manufactured by cvs2svn to create branch 'M15-patch'.
git-svn-id: svn://10.0.0.236/branches/M15-patch@52901 18797224-902f-48f8-a5cc-f745e15eee43
1999-11-06 02:47:16 +00:00
592 changed files with 105 additions and 133909 deletions

View File

@@ -0,0 +1,105 @@
/*
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 mozilla.org code.
The Initial Developer of the Original Code is Sun Microsystems,
Inc. Portions created by Sun are
Copyright (C) 1999 Sun Microsystems, Inc. All
Rights Reserved.
Contributor(s):
*/
#ifndef __JavaDOMGlobals_h__
#define __JavaDOMGlobals_h__
#include "jni.h"
#include "prclist.h"
#include "nsError.h"
#ifdef ERROR
#undef ERROR
#endif
class nsISupports;
class nsIDOMNode;
struct PRLogModuleInfo;
struct PRLock;
class JavaDOMGlobals {
public:
static jclass attrClass;
static jclass cDataSectionClass;
static jclass commentClass;
static jclass documentClass;
static jclass documentFragmentClass;
static jclass documentTypeClass;
static jclass domImplementationClass;
static jclass elementClass;
static jclass entityClass;
static jclass entityReferenceClass;
static jclass namedNodeMapClass;
static jclass nodeClass;
static jclass nodeListClass;
static jclass notationClass;
static jclass processingInstructionClass;
static jclass textClass;
static jfieldID nodePtrFID;
static jfieldID nodeListPtrFID;
static jfieldID domImplementationPtrFID;
static jfieldID nodeTypeAttributeFID;
static jfieldID nodeTypeCDataSectionFID;
static jfieldID nodeTypeCommentFID;
static jfieldID nodeTypeDocumentFragmentFID;
static jfieldID nodeTypeDocumentFID;
static jfieldID nodeTypeDocumentTypeFID;
static jfieldID nodeTypeElementFID;
static jfieldID nodeTypeEntityFID;
static jfieldID nodeTypeEntityReferenceFID;
static jfieldID nodeTypeNotationFID;
static jfieldID nodeTypeProcessingInstructionFID;
static jfieldID nodeTypeTextFID;
static jclass domExceptionClass;
static jmethodID domExceptionInitMID;
static jclass runtimeExceptionClass;
static jmethodID runtimeExceptionInitMID;
static const char* const DOM_EXCEPTION_MESSAGE[];
typedef enum ExceptionType { EXCEPTION_RUNTIME,
EXCEPTION_DOM } ExceptionType;
static PRLogModuleInfo* log;
static PRCList garbage;
static PRLock* garbageLock;
static PRInt32 javaMaxInt;
static void Initialize(JNIEnv *env);
static void Destroy(JNIEnv *env);
static jobject CreateNodeSubtype(JNIEnv *env,
nsIDOMNode *node);
static void AddToGarbage(nsISupports* domObject);
static void TakeOutGarbage();
static void ThrowException(JNIEnv *env,
const char * message = NULL,
nsresult rv = NS_OK,
ExceptionType exceptionType = EXCEPTION_RUNTIME);
};
#endif /* __JavaDOMGlobals_h__ */

View File

@@ -1,749 +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>
# Marc Schumann <wurblzap@gmail.com>
package Bugzilla;
use strict;
# We want any compile errors to get to the browser, if possible.
BEGIN {
# This makes sure we're in a CGI.
if ($ENV{SERVER_SOFTWARE} && !$ENV{MOD_PERL}) {
require CGI::Carp;
CGI::Carp->import('fatalsToBrowser');
}
}
use Bugzilla::Config;
use Bugzilla::Constants;
use Bugzilla::Auth;
use Bugzilla::Auth::Persist::Cookie;
use Bugzilla::CGI;
use Bugzilla::DB;
use Bugzilla::Install::Localconfig qw(read_localconfig);
use Bugzilla::JobQueue;
use Bugzilla::Template;
use Bugzilla::User;
use Bugzilla::Error;
use Bugzilla::Util;
use Bugzilla::Field;
use Bugzilla::Flag;
use File::Basename;
use File::Spec::Functions;
use DateTime::TimeZone;
use Safe;
# This creates the request cache for non-mod_perl installations.
our $_request_cache = {};
#####################################################################
# Constants
#####################################################################
# Scripts that are not stopped by shutdownhtml being in effect.
use constant SHUTDOWNHTML_EXEMPT => [
'editparams.cgi',
'checksetup.pl',
'recode.pl',
];
# Non-cgi scripts that should silently exit.
use constant SHUTDOWNHTML_EXIT_SILENTLY => [
'whine.pl'
];
#####################################################################
# Global Code
#####################################################################
# $::SIG{__DIE__} = i_am_cgi() ? \&CGI::Carp::confess : \&Carp::confess;
# Note that this is a raw subroutine, not a method, so $class isn't available.
sub init_page {
(binmode STDOUT, ':utf8') if Bugzilla->params->{'utf8'};
if (${^TAINT}) {
# Some environment variables are not taint safe
delete @::ENV{'PATH', 'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
# Some modules throw undefined errors (notably File::Spec::Win32) if
# PATH is undefined.
$ENV{'PATH'} = '';
}
# IIS prints out warnings to the webpage, so ignore them, or log them
# to a file if the file exists.
if ($ENV{SERVER_SOFTWARE} && $ENV{SERVER_SOFTWARE} =~ /microsoft-iis/i) {
$SIG{__WARN__} = sub {
my ($msg) = @_;
my $datadir = bz_locations()->{'datadir'};
if (-w "$datadir/errorlog") {
my $warning_log = new IO::File(">>$datadir/errorlog");
print $warning_log $msg;
$warning_log->close();
}
};
}
# If Bugzilla is shut down, do not allow anything to run, just display a
# message to the user about the downtime and log out. Scripts listed in
# SHUTDOWNHTML_EXEMPT are exempt from this message.
#
# Because this is code which is run live from perl "use" commands of other
# scripts, we're skipping this part if we get here during a perl syntax
# check -- runtests.pl compiles scripts without running them, so we
# need to make sure that this check doesn't apply to 'perl -c' calls.
#
# This code must go here. It cannot go anywhere in Bugzilla::CGI, because
# it uses Template, and that causes various dependency loops.
if (!$^C && Bugzilla->params->{"shutdownhtml"}
&& 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;
eval { $user = Bugzilla->login(LOGIN_OPTIONAL); };
if ($@) {
# The DB is not accessible. Use the default user object.
$user = Bugzilla->user;
$user->{settings} = {};
}
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;
}
}
init_page() if !$ENV{MOD_PERL};
#####################################################################
# Subroutines and Methods
#####################################################################
sub template {
my $class = shift;
$class->request_cache->{language} = "";
$class->request_cache->{template} ||= Bugzilla::Template->create();
return $class->request_cache->{template};
}
sub template_inner {
my ($class, $lang) = @_;
$lang = defined($lang) ? $lang : ($class->request_cache->{language} || "");
$class->request_cache->{language} = $lang;
$class->request_cache->{"template_inner_$lang"}
||= Bugzilla::Template->create();
return $class->request_cache->{"template_inner_$lang"};
}
sub cgi {
my $class = shift;
$class->request_cache->{cgi} ||= new Bugzilla::CGI();
return $class->request_cache->{cgi};
}
sub localconfig {
my $class = shift;
$class->request_cache->{localconfig} ||= read_localconfig();
return $class->request_cache->{localconfig};
}
sub params {
my $class = shift;
$class->request_cache->{params} ||= Bugzilla::Config::read_param_file();
return $class->request_cache->{params};
}
sub user {
my $class = shift;
$class->request_cache->{user} ||= new Bugzilla::User;
return $class->request_cache->{user};
}
sub set_user {
my ($class, $user) = @_;
$class->request_cache->{user} = $user;
}
sub sudoer {
my $class = shift;
return $class->request_cache->{sudoer};
}
sub sudo_request {
my ($class, $new_user, $new_sudoer) = @_;
$class->request_cache->{user} = $new_user;
$class->request_cache->{sudoer} = $new_sudoer;
# NOTE: If you want to log the start of an sudo session, do it here.
}
sub page_requires_login {
return $_[0]->request_cache->{page_requires_login};
}
sub login {
my ($class, $type) = @_;
return $class->user if $class->user->id;
my $authorizer = new Bugzilla::Auth();
$type = LOGIN_REQUIRED if $class->cgi->param('GoAheadAndLogIn');
if (!defined $type || $type == LOGIN_NORMAL) {
$type = $class->params->{'requirelogin'} ? LOGIN_REQUIRED : LOGIN_NORMAL;
}
# Allow templates to know that we're in a page that always requires
# login.
if ($type == LOGIN_REQUIRED) {
$class->request_cache->{page_requires_login} = 1;
}
my $authenticated_user = $authorizer->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'))
)
{
$class->set_user($sudo_target);
$class->request_cache->{sudoer} = $authenticated_user;
# And make sure that both users have the same Auth object,
# since we never call Auth::login for the sudo target.
$sudo_target->set_authorizer($authenticated_user->authorizer);
# NOTE: If you want to do any special logging, do it here.
}
else {
$class->set_user($authenticated_user);
}
# We run after the login has completed since
# some of the checks in ssl_require_redirect
# look for Bugzilla->user->id to determine
# if redirection is required.
if (i_am_cgi() && ssl_require_redirect()) {
$class->cgi->require_https($class->params->{'sslbase'});
}
return $class->user;
}
sub logout {
my ($class, $option) = @_;
# If we're not logged in, go away
return unless $class->user->id;
$option = LOGOUT_CURRENT unless defined $option;
Bugzilla::Auth::Persist::Cookie->logout({type => $option});
$class->logout_request() unless $option eq LOGOUT_KEEP_CURRENT;
}
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::Persist::Cookie->logout({user => $user});
}
# 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 {
my $class = shift;
delete $class->request_cache->{user};
delete $class->request_cache->{sudoer};
# We can't delete from $cgi->cookie, so logincookie data will remain
# there. Don't rely on it: use Bugzilla->user->login instead!
}
sub job_queue {
my $class = shift;
$class->request_cache->{job_queue} ||= Bugzilla::JobQueue->new();
return $class->request_cache->{job_queue};
}
sub dbh {
my $class = shift;
# If we're not connected, then we must want the main db
$class->request_cache->{dbh} ||= $class->request_cache->{dbh_main}
= Bugzilla::DB::connect_main();
return $class->request_cache->{dbh};
}
sub languages {
my $class = shift;
return $class->request_cache->{languages}
if $class->request_cache->{languages};
my @files = glob(catdir(bz_locations->{'templatedir'}, '*'));
my @languages;
foreach my $dir_entry (@files) {
# It's a language directory only if it contains "default" or
# "custom". This auto-excludes CVS directories as well.
next unless (-d catdir($dir_entry, 'default')
|| -d catdir($dir_entry, 'custom'));
$dir_entry = basename($dir_entry);
# Check for language tag format conforming to RFC 1766.
next unless $dir_entry =~ /^[a-zA-Z]{1,8}(-[a-zA-Z]{1,8})?$/;
push(@languages, $dir_entry);
}
return $class->request_cache->{languages} = \@languages;
}
sub error_mode {
my ($class, $newval) = @_;
if (defined $newval) {
$class->request_cache->{error_mode} = $newval;
}
return $class->request_cache->{error_mode}
|| (i_am_cgi() ? ERROR_MODE_WEBPAGE : ERROR_MODE_DIE);
}
sub usage_mode {
my ($class, $newval) = @_;
if (defined $newval) {
if ($newval == USAGE_MODE_BROWSER) {
$class->error_mode(ERROR_MODE_WEBPAGE);
}
elsif ($newval == USAGE_MODE_CMDLINE) {
$class->error_mode(ERROR_MODE_DIE);
}
elsif ($newval == USAGE_MODE_WEBSERVICE) {
$class->error_mode(ERROR_MODE_DIE_SOAP_FAULT);
}
elsif ($newval == USAGE_MODE_EMAIL) {
$class->error_mode(ERROR_MODE_DIE);
}
else {
ThrowCodeError('usage_mode_invalid',
{'invalid_usage_mode', $newval});
}
$class->request_cache->{usage_mode} = $newval;
}
return $class->request_cache->{usage_mode}
|| (i_am_cgi()? USAGE_MODE_BROWSER : USAGE_MODE_CMDLINE);
}
sub installation_mode {
my ($class, $newval) = @_;
($class->request_cache->{installation_mode} = $newval) if defined $newval;
return $class->request_cache->{installation_mode}
|| INSTALLATION_MODE_INTERACTIVE;
}
sub installation_answers {
my ($class, $filename) = @_;
if ($filename) {
my $s = new Safe;
$s->rdo($filename);
die "Error reading $filename: $!" if $!;
die "Error evaluating $filename: $@" if $@;
# Now read the param back out from the sandbox
$class->request_cache->{installation_answers} = $s->varglob('answer');
}
return $class->request_cache->{installation_answers} || {};
}
sub switch_to_shadow_db {
my $class = shift;
if (!$class->request_cache->{dbh_shadow}) {
if ($class->params->{'shadowdb'}) {
$class->request_cache->{dbh_shadow} = Bugzilla::DB::connect_shadow();
} else {
$class->request_cache->{dbh_shadow} = request_cache()->{dbh_main};
}
}
$class->request_cache->{dbh} = $class->request_cache->{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;
$class->request_cache->{dbh} = $class->request_cache->{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;
}
sub get_fields {
my $class = shift;
my $criteria = shift;
# This function may be called during installation, and Field::match
# may fail at that time. so we want to return an empty list in that
# case.
my $fields = eval { Bugzilla::Field->match($criteria) } || [];
return @$fields;
}
sub active_custom_fields {
my $class = shift;
if (!exists $class->request_cache->{active_custom_fields}) {
$class->request_cache->{active_custom_fields} =
Bugzilla::Field->match({ custom => 1, obsolete => 0 });
}
return @{$class->request_cache->{active_custom_fields}};
}
sub has_flags {
my $class = shift;
if (!defined $class->request_cache->{has_flags}) {
$class->request_cache->{has_flags} = Bugzilla::Flag::has_flags();
}
return $class->request_cache->{has_flags};
}
sub hook_args {
my ($class, $args) = @_;
$class->request_cache->{hook_args} = $args if $args;
return $class->request_cache->{hook_args};
}
sub local_timezone {
my $class = shift;
if (!defined $class->request_cache->{local_timezone}) {
$class->request_cache->{local_timezone} =
DateTime::TimeZone->new(name => 'local');
}
return $class->request_cache->{local_timezone};
}
sub request_cache {
if ($ENV{MOD_PERL}) {
require Apache2::RequestUtil;
return Apache2::RequestUtil->request->pnotes();
}
return $_request_cache;
}
# Private methods
# Per-process cleanup. Note that this is a plain subroutine, not a method,
# so we don't have $class available.
sub _cleanup {
my $main = Bugzilla->request_cache->{dbh_main};
my $shadow = Bugzilla->request_cache->{dbh_shadow};
foreach my $dbh ($main, $shadow) {
next if !$dbh;
$dbh->bz_rollback_transaction() if $dbh->bz_in_transaction;
$dbh->disconnect;
}
undef $_request_cache;
}
sub END {
# Bugzilla.pm cannot compile in mod_perl.pl if this runs.
_cleanup() unless $ENV{MOD_PERL};
}
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 it's 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<template_inner>
If you ever need a L<Bugzilla::Template> object while you're already
processing a template, use this. Also use it if you want to specify
the language to use. If no argument is passed, it uses the last
language set. If the argument is "" (empty string), the language is
reset to the current one (the one used by Bugzilla->template).
=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<set_user>
Allows you to directly set what L</user> will return. You can use this
if you want to bypass L</login> for some reason and directly "log in"
a specific L<Bugzilla::User>. Be careful with it, though!
=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<page_requires_login>
If the current page always requires the user to log in (for example,
C<enter_bug.cgi> or any page called with C<?GoAheadAndLogIn=1>) then
this will return something true. Otherwise it will return false. (This is
set when you call L</login>.)
=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<error_mode>
Call either C<Bugzilla->error_mode(Bugzilla::Constants::ERROR_MODE_DIE)>
or C<Bugzilla->error_mode(Bugzilla::Constants::ERROR_MODE_DIE_SOAP_FAULT)> to
change this flag's default of C<Bugzilla::Constants::ERROR_MODE_WEBPAGE> and to
indicate that errors should be passed to error mode specific error handlers
rather than being sent to a browser and finished with an exit().
This is useful, for example, to keep C<eval> blocks from producing wild HTML
on errors, making it easier for you to catch them.
(Remember to reset the error mode to its previous value afterwards, though.)
C<Bugzilla->error_mode> will return the current state of this flag.
Note that C<Bugzilla->error_mode> is being called by C<Bugzilla->usage_mode> on
usage mode changes.
=item C<usage_mode>
Call either C<Bugzilla->usage_mode(Bugzilla::Constants::USAGE_MODE_CMDLINE)>
or C<Bugzilla->usage_mode(Bugzilla::Constants::USAGE_MODE_WEBSERVICE)> near the
beginning of your script to change this flag's default of
C<Bugzilla::Constants::USAGE_MODE_BROWSER> and to indicate that Bugzilla is
being called in a non-interactive manner.
This influences error handling because on usage mode changes, C<usage_mode>
calls C<Bugzilla->error_mode> to set an error mode which makes sense for the
usage mode.
C<Bugzilla->usage_mode> will return the current state of this flag.
=item C<installation_mode>
Determines whether or not installation should be silent. See
L<Bugzilla::Constants> for the C<INSTALLATION_MODE> constants.
=item C<installation_answers>
Returns a hashref representing any "answers" file passed to F<checksetup.pl>,
used to automatically answer or skip prompts.
=item C<dbh>
The current database handle. See L<DBI>.
=item C<languages>
Currently installed languages.
Returns a reference to a list of RFC 1766 language tags of installed languages.
=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.
=item C<params>
The current Parameters of Bugzilla, as a hashref. If C<data/params>
does not exist, then we return an empty hashref. If C<data/params>
is unreadable or is not valid perl, we C<die>.
=item C<hook_args>
If you are running inside a code hook (see L<Bugzilla::Hook>) this
is how you get the arguments passed to the hook.
=item C<local_timezone>
Returns the local timezone of the Bugzilla installation,
as a DateTime::TimeZone object. This detection is very time
consuming, so we cache this information for future references.
=item C<job_queue>
Returns a L<Bugzilla::JobQueue> that you can use for queueing jobs.
Will throw an error if job queueing is not correctly configured on
this Bugzilla installation.
=back

View File

@@ -1,988 +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>
# Marc Schumann <wurblzap@gmail.com>
# Frédéric Buclin <LpSolit@gmail.com>
use strict;
package Bugzilla::Attachment;
=head1 NAME
Bugzilla::Attachment - Bugzilla attachment class.
=head1 SYNOPSIS
use Bugzilla::Attachment;
# Get the attachment with the given ID.
my $attachment = new Bugzilla::Attachment($attach_id);
# Get the attachments with the given IDs.
my $attachments = Bugzilla::Attachment->new_from_list($attach_ids);
=head1 DESCRIPTION
Attachment.pm represents an attachment object. It is an implementation
of L<Bugzilla::Object>, and thus provides all methods that
L<Bugzilla::Object> provides.
The methods that are specific to C<Bugzilla::Attachment> are listed
below.
=cut
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Flag;
use Bugzilla::User;
use Bugzilla::Util;
use Bugzilla::Field;
use base qw(Bugzilla::Object);
###############################
#### Initialization ####
###############################
use constant DB_TABLE => 'attachments';
use constant ID_FIELD => 'attach_id';
use constant LIST_ORDER => ID_FIELD;
sub DB_COLUMNS {
my $dbh = Bugzilla->dbh;
return qw(
attach_id
bug_id
description
filename
isobsolete
ispatch
isprivate
isurl
mimetype
modification_time
submitter_id),
$dbh->sql_date_format('attachments.creation_ts', '%Y.%m.%d %H:%i') . ' AS creation_ts';
}
###############################
#### Accessors ######
###############################
=pod
=head2 Instance Properties
=over
=item C<bug_id>
the ID of the bug to which the attachment is attached
=back
=cut
sub bug_id {
my $self = shift;
return $self->{bug_id};
}
=over
=item C<bug>
the bug object to which the attachment is attached
=back
=cut
sub bug {
my $self = shift;
require Bugzilla::Bug;
$self->{bug} = Bugzilla::Bug->new($self->bug_id);
return $self->{bug};
}
=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->{mimetype};
}
=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->{submitter_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->{creation_ts};
}
=over
=item C<modification_time>
the date and time on which the attachment was last modified.
=back
=cut
sub modification_time {
my $self = shift;
return $self->{modification_time};
}
=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<is_viewable>
Returns 1 if the attachment has a content-type viewable in this browser.
Note that we don't use $cgi->Accept()'s ability to check if a content-type
matches, because this will return a value even if it's matched by the generic
*/* which most browsers add to the end of their Accept: headers.
=back
=cut
sub is_viewable {
my $self = shift;
my $contenttype = $self->contenttype;
my $cgi = Bugzilla->cgi;
# We assume we can view all text and image types.
return 1 if ($contenttype =~ /^(text|image)\//);
# Mozilla can view XUL. Note the trailing slash on the Gecko detection to
# avoid sending XUL to Safari.
return 1 if (($contenttype =~ /^application\/vnd\.mozilla\./)
&& ($cgi->user_agent() =~ /Gecko\//));
# If it's not one of the above types, we check the Accept: header for any
# types mentioned explicitly.
my $accept = join(",", $cgi->Accept());
return 1 if ($accept =~ /^(.*,)?\Q$contenttype\E(,.*)?$/);
return 0;
}
=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) || 0;
# If there's no attachment data in the database, either the attachment
# is stored in a local file, and so retrieve its size from the file,
# or the attachment has been deleted.
unless ($self->{datasize}) {
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 });
return $self->{flags};
}
=over
=item C<flag_types>
Return all flag types available for this attachment as well as flags
already set, grouped by flag type.
=back
=cut
sub flag_types {
my $self = shift;
return $self->{flag_types} if exists $self->{flag_types};
my $vars = { target_type => 'attachment',
product_id => $self->bug->product_id,
component_id => $self->bug->component_id,
attach_id => $self->id };
$self->{flag_types} = Bugzilla::Flag::_flag_types($vars);
return $self->{flag_types};
}
###############################
#### Validators ######
###############################
# Instance methods; no POD documentation here yet because the only ones so far
# are private.
sub _get_local_filename {
my $self = shift;
my $hash = ($self->id % 100) + 100;
$hash =~ s/.*(\d\d)$/group.$1/;
return bz_locations()->{'attachdir'} . "/$hash/attachment." . $self->id;
}
sub _validate_filename {
my ($throw_error) = @_;
my $cgi = Bugzilla->cgi;
defined $cgi->upload('data')
|| ($throw_error ? ThrowUserError("file_not_specified") : return 0);
my $filename = $cgi->upload('data');
# Remove path info (if any) from the file name. The browser should do this
# for us, but some are buggy. This may not work on Mac file names and could
# mess up file names with slashes in them, but them's the breaks. We only
# use this as a hint to users downloading attachments anyway, so it's not
# a big deal if it munges incorrectly occasionally.
$filename =~ s/^.*[\/\\]//;
# Truncate the filename to 100 characters, counting from the end of the
# string to make sure we keep the filename extension.
$filename = substr($filename, -100, 100);
return $filename;
}
sub _validate_data {
my ($throw_error, $hr_vars) = @_;
my $cgi = Bugzilla->cgi;
my $fh;
# Skip uploading into a local variable if the user wants to upload huge
# attachments into local files.
if (!$cgi->param('bigfile')) {
$fh = $cgi->upload('data');
}
my $data;
# We could get away with reading only as much as required, except that then
# we wouldn't have a size to print to the error handler below.
if (!$cgi->param('bigfile')) {
# enable 'slurp' mode
local $/;
$data = <$fh>;
}
$data
|| ($cgi->param('bigfile'))
|| ($throw_error ? ThrowUserError("zero_length_file") : return 0);
# Windows screenshots are usually uncompressed BMP files which
# makes for a quick way to eat up disk space. Let's compress them.
# We do this before we check the size since the uncompressed version
# could easily be greater than maxattachmentsize.
if (Bugzilla->params->{'convert_uncompressed_images'}
&& $cgi->param('contenttype') eq 'image/bmp') {
require Image::Magick;
my $img = Image::Magick->new(magick=>'bmp');
$img->BlobToImage($data);
$img->set(magick=>'png');
my $imgdata = $img->ImageToBlob();
$data = $imgdata;
$cgi->param('contenttype', 'image/png');
$hr_vars->{'convertedbmp'} = 1;
}
# Make sure the attachment does not exceed the maximum permitted size
my $maxsize = Bugzilla->params->{'maxattachmentsize'} * 1024; # Convert from K
my $len = $data ? length($data) : 0;
if ($maxsize && $len > $maxsize) {
my $vars = { filesize => sprintf("%.0f", $len/1024) };
if ($cgi->param('ispatch')) {
$throw_error ? ThrowUserError("patch_too_large", $vars) : return 0;
}
else {
$throw_error ? ThrowUserError("file_too_large", $vars) : return 0;
}
}
return $data || '';
}
=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.
=cut
sub get_attachments_by_bug {
my ($class, $bug_id, $vars) = @_;
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->new_from_list($attach_ids);
# To avoid $attachment->flags to run SQL queries itself for each
# attachment listed here, we collect all the data at once and
# populate $attachment->{flags} ourselves.
if ($vars->{preload}) {
$_->{flags} = [] foreach @$attachments;
my %att = map { $_->id => $_ } @$attachments;
my $flags = Bugzilla::Flag->match({ bug_id => $bug_id,
target_type => 'attachment' });
# Exclude flags for private attachments you cannot see.
@$flags = grep {exists $att{$_->attach_id}} @$flags;
push(@{$att{$_->attach_id}->{flags}}, $_) foreach @$flags;
$attachments = [sort {$a->id <=> $b->id} values %att];
}
return $attachments;
}
=pod
=item C<validate_is_patch()>
Description: validates the "patch" flag passed in by CGI.
Returns: 1 on success.
=cut
sub validate_is_patch {
my ($class, $throw_error) = @_;
my $cgi = Bugzilla->cgi;
# Set the ispatch flag to zero if it is undefined, since the UI uses
# an HTML checkbox to represent this flag, and unchecked HTML checkboxes
# do not get sent in HTML requests.
$cgi->param('ispatch', $cgi->param('ispatch') ? 1 : 0);
# Set the content type to text/plain if the attachment is a patch.
$cgi->param('contenttype', 'text/plain') if $cgi->param('ispatch');
return 1;
}
=pod
=item C<validate_description()>
Description: validates the description passed in by CGI.
Returns: 1 on success.
=cut
sub validate_description {
my ($class, $throw_error) = @_;
my $cgi = Bugzilla->cgi;
$cgi->param('description')
|| ($throw_error ? ThrowUserError("missing_attachment_description") : return 0);
return 1;
}
=pod
=item C<validate_content_type()>
Description: validates the content type passed in by CGI.
Returns: 1 on success.
=cut
sub validate_content_type {
my ($class, $throw_error) = @_;
my $cgi = Bugzilla->cgi;
if (!defined $cgi->param('contenttypemethod')) {
$throw_error ? ThrowUserError("missing_content_type_method") : return 0;
}
elsif ($cgi->param('contenttypemethod') eq 'autodetect') {
my $contenttype =
$cgi->uploadInfo($cgi->param('data'))->{'Content-Type'};
# The user asked us to auto-detect the content type, so use the type
# specified in the HTTP request headers.
if ( !$contenttype ) {
$throw_error ? ThrowUserError("missing_content_type") : return 0;
}
$cgi->param('contenttype', $contenttype);
}
elsif ($cgi->param('contenttypemethod') eq 'list') {
# The user selected a content type from the list, so use their
# selection.
$cgi->param('contenttype', $cgi->param('contenttypeselection'));
}
elsif ($cgi->param('contenttypemethod') eq 'manual') {
# The user entered a content type manually, so use their entry.
$cgi->param('contenttype', $cgi->param('contenttypeentry'));
}
else {
$throw_error ?
ThrowCodeError("illegal_content_type_method",
{ contenttypemethod => $cgi->param('contenttypemethod') }) :
return 0;
}
if ( $cgi->param('contenttype') !~
/^(application|audio|image|message|model|multipart|text|video)\/.+$/ ) {
$throw_error ?
ThrowUserError("invalid_content_type",
{ contenttype => $cgi->param('contenttype') }) :
return 0;
}
return 1;
}
=pod
=item C<validate_can_edit($attachment, $product_id)>
Description: validates if the user is allowed to view and edit the attachment.
Only the submitter or someone with editbugs privs can edit it.
Only the submitter and users in the insider group can view
private attachments.
Params: $attachment - the attachment object being edited.
$product_id - the product ID the attachment belongs to.
Returns: 1 on success. Else an error is thrown.
=cut
sub validate_can_edit {
my ($attachment, $product_id) = @_;
my $user = Bugzilla->user;
# The submitter can edit their attachments.
return 1 if ($attachment->attacher->id == $user->id
|| ((!$attachment->isprivate || $user->is_insider)
&& $user->in_group('editbugs', $product_id)));
# If we come here, then this attachment cannot be seen by the user.
ThrowUserError('illegal_attachment_edit', { attach_id => $attachment->id });
}
=item C<validate_obsolete($bug)>
Description: validates if attachments the user wants to mark as obsolete
really belong to the given bug and are not already obsolete.
Moreover, a user cannot mark an attachment as obsolete if
he cannot view it (due to restrictions on it).
Params: $bug - The bug object obsolete attachments should belong to.
Returns: 1 on success. Else an error is thrown.
=cut
sub validate_obsolete {
my ($class, $bug) = @_;
my $cgi = Bugzilla->cgi;
# Make sure the attachment id is valid and the user has permissions to view
# the bug to which it is attached. Make sure also that the user can view
# the attachment itself.
my @obsolete_attachments;
foreach my $attachid ($cgi->param('obsolete')) {
my $vars = {};
$vars->{'attach_id'} = $attachid;
detaint_natural($attachid)
|| ThrowCodeError('invalid_attach_id_to_obsolete', $vars);
# Make sure the attachment exists in the database.
my $attachment = new Bugzilla::Attachment($attachid)
|| ThrowUserError('invalid_attach_id', $vars);
# Check that the user can view and edit this attachment.
$attachment->validate_can_edit($bug->product_id);
$vars->{'description'} = $attachment->description;
if ($attachment->bug_id != $bug->bug_id) {
$vars->{'my_bug_id'} = $bug->bug_id;
$vars->{'attach_bug_id'} = $attachment->bug_id;
ThrowCodeError('mismatched_bug_ids_on_obsolete', $vars);
}
if ($attachment->isobsolete) {
ThrowCodeError('attachment_already_obsolete', $vars);
}
push(@obsolete_attachments, $attachment);
}
return @obsolete_attachments;
}
###############################
#### Constructors #####
###############################
=pod
=item C<create($throw_error, $bug, $user, $timestamp, $hr_vars)>
Description: inserts an attachment from CGI input for the given bug.
Params: C<$bug> - Bugzilla::Bug object - the bug for which to insert
the attachment.
C<$user> - Bugzilla::User object - the user we're inserting an
attachment for.
C<$timestamp> - scalar - timestamp of the insert as returned
by SELECT NOW().
C<$hr_vars> - hash reference - reference to a hash of template
variables.
Returns: the ID of the new attachment.
=cut
# FIXME: needs to follow the way Object->create() works.
sub create {
my ($class, $throw_error, $bug, $user, $timestamp, $hr_vars) = @_;
my $cgi = Bugzilla->cgi;
my $dbh = Bugzilla->dbh;
my $attachurl = $cgi->param('attachurl') || '';
my $data;
my $filename;
my $contenttype;
my $isurl;
$class->validate_is_patch($throw_error) || return;
$class->validate_description($throw_error) || return;
if (Bugzilla->params->{'allow_attach_url'}
&& ($attachurl =~ /^(http|https|ftp):\/\/\S+/)
&& !defined $cgi->upload('data'))
{
$filename = '';
$data = $attachurl;
$isurl = 1;
$contenttype = 'text/plain';
$cgi->param('ispatch', 0);
$cgi->delete('bigfile');
}
else {
$filename = _validate_filename($throw_error) || return;
# need to validate content type before data as
# we now check the content type for image/bmp in _validate_data()
unless ($cgi->param('ispatch')) {
$class->validate_content_type($throw_error) || return;
# Set the ispatch flag to 1 if we're set to autodetect
# and the content type is text/x-diff or text/x-patch
if ($cgi->param('contenttypemethod') eq 'autodetect'
&& $cgi->param('contenttype') =~ m{text/x-(?:diff|patch)})
{
$cgi->param('ispatch', 1);
$cgi->param('contenttype', 'text/plain');
}
}
$data = _validate_data($throw_error, $hr_vars);
# If the attachment is stored locally, $data eq ''.
# If an error is thrown, $data eq '0'.
($data ne '0') || return;
$contenttype = $cgi->param('contenttype');
# These are inserted using placeholders so no need to panic
trick_taint($filename);
trick_taint($contenttype);
$isurl = 0;
}
# Check attachments the user tries to mark as obsolete.
my @obsolete_attachments;
if ($cgi->param('obsolete')) {
@obsolete_attachments = $class->validate_obsolete($bug);
}
# The order of these function calls is important, as Flag::validate
# assumes User::match_field has ensured that the
# values in the requestee fields are legitimate user email addresses.
my $match_status = Bugzilla::User::match_field($cgi, {
'^requestee(_type)?-(\d+)$' => { 'type' => 'multi' },
}, MATCH_SKIP_CONFIRM);
$hr_vars->{'match_field'} = 'requestee';
if ($match_status == USER_MATCH_FAILED) {
$hr_vars->{'message'} = 'user_match_failed';
}
elsif ($match_status == USER_MATCH_MULTIPLE) {
$hr_vars->{'message'} = 'user_match_multiple';
}
# Escape characters in strings that will be used in SQL statements.
my $description = $cgi->param('description');
trick_taint($description);
my $isprivate = $cgi->param('isprivate') ? 1 : 0;
# Insert the attachment into the database.
my $sth = $dbh->do(
"INSERT INTO attachments
(bug_id, creation_ts, modification_time, filename, description,
mimetype, ispatch, isurl, isprivate, submitter_id)
VALUES (?,?,?,?,?,?,?,?,?,?)", undef, ($bug->bug_id, $timestamp, $timestamp,
$filename, $description, $contenttype, $cgi->param('ispatch'),
$isurl, $isprivate, $user->id));
# Retrieve the ID of the newly created attachment record.
my $attachid = $dbh->bz_last_key('attachments', 'attach_id');
# We only use $data here in this INSERT with a placeholder,
# so it's safe.
$sth = $dbh->prepare("INSERT INTO attach_data
(id, thedata) VALUES ($attachid, ?)");
trick_taint($data);
$sth->bind_param(1, $data, $dbh->BLOB_TYPE);
$sth->execute();
# If the file is to be stored locally, stream the file from the web server
# to the local file without reading it into a local variable.
if ($cgi->param('bigfile')) {
my $attachdir = bz_locations()->{'attachdir'};
my $fh = $cgi->upload('data');
my $hash = ($attachid % 100) + 100;
$hash =~ s/.*(\d\d)$/group.$1/;
mkdir "$attachdir/$hash", 0770;
chmod 0770, "$attachdir/$hash";
open(AH, ">$attachdir/$hash/attachment.$attachid");
binmode AH;
my $sizecount = 0;
my $limit = (Bugzilla->params->{"maxlocalattachment"} * 1048576);
while (<$fh>) {
print AH $_;
$sizecount += length($_);
if ($sizecount > $limit) {
close AH;
close $fh;
unlink "$attachdir/$hash/attachment.$attachid";
$throw_error ? ThrowUserError("local_file_too_large") : return;
}
}
close AH;
close $fh;
}
# Make existing attachments obsolete.
my $fieldid = get_field_id('attachments.isobsolete');
foreach my $obsolete_attachment (@obsolete_attachments) {
# If the obsolete attachment has request flags, cancel them.
# This call must be done before updating the 'attachments' table.
Bugzilla::Flag->CancelRequests($bug, $obsolete_attachment, $timestamp);
$dbh->do('UPDATE attachments SET isobsolete = 1, modification_time = ?
WHERE attach_id = ?',
undef, ($timestamp, $obsolete_attachment->id));
$dbh->do('INSERT INTO bugs_activity (bug_id, attach_id, who, bug_when,
fieldid, removed, added)
VALUES (?,?,?,?,?,?,?)',
undef, ($bug->bug_id, $obsolete_attachment->id, $user->id,
$timestamp, $fieldid, 0, 1));
}
my $attachment = new Bugzilla::Attachment($attachid);
# 1. Add flags, if any. To avoid dying if something goes wrong
# while processing flags, we will eval() flag validation.
# This requires errors to die().
# XXX: this can go away as soon as flag validation is able to
# fail without dying.
#
# 2. Flag::validate() should not detect any reference to existing flags
# when creating a new attachment. Setting the third param to -1 will
# force this function to check this point.
my $error_mode_cache = Bugzilla->error_mode;
Bugzilla->error_mode(ERROR_MODE_DIE);
eval {
Bugzilla::Flag::validate($bug->bug_id, -1, SKIP_REQUESTEE_ON_ERROR);
Bugzilla::Flag->process($bug, $attachment, $timestamp, $hr_vars);
};
Bugzilla->error_mode($error_mode_cache);
if ($@) {
$hr_vars->{'message'} = 'flag_creation_failed';
$hr_vars->{'flag_creation_error'} = $@;
}
# Return the new attachment object.
return $attachment;
}
=pod
=item C<remove_from_db()>
Description: removes an attachment from the DB.
Params: none
Returns: nothing
=back
=cut
sub remove_from_db {
my $self = shift;
my $dbh = Bugzilla->dbh;
$dbh->bz_start_transaction();
$dbh->do('DELETE FROM flags WHERE attach_id = ?', undef, $self->id);
$dbh->do('DELETE FROM attach_data WHERE id = ?', undef, $self->id);
$dbh->do('UPDATE attachments SET mimetype = ?, ispatch = ?, isurl = ?, isobsolete = ?
WHERE attach_id = ?', undef, ('text/plain', 0, 0, 1, $self->id));
$dbh->bz_commit_transaction();
}
1;

View File

@@ -1,295 +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): John Keiser <john@johnkeiser.com>
# Frédéric Buclin <LpSolit@gmail.com>
use strict;
package Bugzilla::Attachment::PatchReader;
use Bugzilla::Error;
use Bugzilla::Attachment;
use Bugzilla::Util;
sub process_diff {
my ($attachment, $format, $context) = @_;
my $dbh = Bugzilla->dbh;
my $cgi = Bugzilla->cgi;
my $lc = Bugzilla->localconfig;
my $vars = {};
my ($reader, $last_reader) = setup_patch_readers(undef, $context);
if ($format eq 'raw') {
require PatchReader::DiffPrinter::raw;
$last_reader->sends_data_to(new PatchReader::DiffPrinter::raw());
# Actually print out the patch.
print $cgi->header(-type => 'text/plain',
-expires => '+3M');
disable_utf8();
$reader->iterate_string('Attachment ' . $attachment->id, $attachment->data);
}
else {
my @other_patches = ();
if ($lc->{interdiffbin} && $lc->{diffpath}) {
# Get the list of attachments that the user can view in this bug.
my @attachments =
@{Bugzilla::Attachment->get_attachments_by_bug($attachment->bug_id)};
# Extract patches only.
@attachments = grep {$_->ispatch == 1} @attachments;
# We want them sorted from newer to older.
@attachments = sort { $b->id <=> $a->id } @attachments;
# Ignore the current patch, but select the one right before it
# chronologically.
my $select_next_patch = 0;
foreach my $attach (@attachments) {
if ($attach->id == $attachment->id) {
$select_next_patch = 1;
}
else {
push(@other_patches, { 'id' => $attach->id,
'desc' => $attach->description,
'selected' => $select_next_patch });
$select_next_patch = 0;
}
}
}
$vars->{'bugid'} = $attachment->bug_id;
$vars->{'attachid'} = $attachment->id;
$vars->{'description'} = $attachment->description;
$vars->{'other_patches'} = \@other_patches;
setup_template_patch_reader($last_reader, $format, $context, $vars);
# The patch is going to be displayed in a HTML page and if the utf8
# param is enabled, we have to encode attachment data as utf8.
if (Bugzilla->params->{'utf8'}) {
$attachment->data; # Populate ->{data}
utf8::decode($attachment->{data});
}
$reader->iterate_string('Attachment ' . $attachment->id, $attachment->data);
}
}
sub process_interdiff {
my ($old_attachment, $new_attachment, $format, $context) = @_;
my $cgi = Bugzilla->cgi;
my $lc = Bugzilla->localconfig;
my $vars = {};
# Encode attachment data as utf8 if it's going to be displayed in a HTML
# page using the UTF-8 encoding.
if ($format ne 'raw' && Bugzilla->params->{'utf8'}) {
$old_attachment->data; # Populate ->{data}
utf8::decode($old_attachment->{data});
$new_attachment->data; # Populate ->{data}
utf8::decode($new_attachment->{data});
}
# Get old patch data.
my ($old_filename, $old_file_list) = get_unified_diff($old_attachment, $format);
# Get new patch data.
my ($new_filename, $new_file_list) = get_unified_diff($new_attachment, $format);
my $warning = warn_if_interdiff_might_fail($old_file_list, $new_file_list);
# Send through interdiff, send output directly to template.
# Must hack path so that interdiff will work.
$ENV{'PATH'} = $lc->{diffpath};
open my $interdiff_fh, "$lc->{interdiffbin} $old_filename $new_filename|";
binmode $interdiff_fh;
my ($reader, $last_reader) = setup_patch_readers("", $context);
if ($format eq 'raw') {
require PatchReader::DiffPrinter::raw;
$last_reader->sends_data_to(new PatchReader::DiffPrinter::raw());
# Actually print out the patch.
print $cgi->header(-type => 'text/plain',
-expires => '+3M');
disable_utf8();
}
else {
# In case the HTML page is displayed with the UTF-8 encoding.
binmode $interdiff_fh, ':utf8' if Bugzilla->params->{'utf8'};
$vars->{'warning'} = $warning if $warning;
$vars->{'bugid'} = $new_attachment->bug_id;
$vars->{'oldid'} = $old_attachment->id;
$vars->{'old_desc'} = $old_attachment->description;
$vars->{'newid'} = $new_attachment->id;
$vars->{'new_desc'} = $new_attachment->description;
setup_template_patch_reader($last_reader, $format, $context, $vars);
}
$reader->iterate_fh($interdiff_fh, 'interdiff #' . $old_attachment->id .
' #' . $new_attachment->id);
close $interdiff_fh;
$ENV{'PATH'} = '';
# Delete temporary files.
unlink($old_filename) or warn "Could not unlink $old_filename: $!";
unlink($new_filename) or warn "Could not unlink $new_filename: $!";
}
######################
# Internal routines
######################
sub get_unified_diff {
my ($attachment, $format) = @_;
# Bring in the modules we need.
require PatchReader::Raw;
require PatchReader::FixPatchRoot;
require PatchReader::DiffPrinter::raw;
require PatchReader::PatchInfoGrabber;
require File::Temp;
$attachment->ispatch
|| ThrowCodeError('must_be_patch', { 'attach_id' => $attachment->id });
# Reads in the patch, converting to unified diff in a temp file.
my $reader = new PatchReader::Raw;
my $last_reader = $reader;
# Fixes patch root (makes canonical if possible).
if (Bugzilla->params->{'cvsroot'}) {
my $fix_patch_root =
new PatchReader::FixPatchRoot(Bugzilla->params->{'cvsroot'});
$last_reader->sends_data_to($fix_patch_root);
$last_reader = $fix_patch_root;
}
# Grabs the patch file info.
my $patch_info_grabber = new PatchReader::PatchInfoGrabber();
$last_reader->sends_data_to($patch_info_grabber);
$last_reader = $patch_info_grabber;
# Prints out to temporary file.
my ($fh, $filename) = File::Temp::tempfile();
if ($format ne 'raw' && Bugzilla->params->{'utf8'}) {
# The HTML page will be displayed with the UTF-8 encoding.
binmode $fh, ':utf8';
}
my $raw_printer = new PatchReader::DiffPrinter::raw($fh);
$last_reader->sends_data_to($raw_printer);
$last_reader = $raw_printer;
# Iterate!
$reader->iterate_string($attachment->id, $attachment->data);
return ($filename, $patch_info_grabber->patch_info()->{files});
}
sub warn_if_interdiff_might_fail {
my ($old_file_list, $new_file_list) = @_;
# Verify that the list of files diffed is the same.
my @old_files = sort keys %{$old_file_list};
my @new_files = sort keys %{$new_file_list};
if (@old_files != @new_files
|| join(' ', @old_files) ne join(' ', @new_files))
{
return 'interdiff1';
}
# Verify that the revisions in the files are the same.
foreach my $file (keys %{$old_file_list}) {
if ($old_file_list->{$file}{old_revision} ne
$new_file_list->{$file}{old_revision})
{
return 'interdiff2';
}
}
return undef;
}
sub setup_patch_readers {
my ($diff_root, $context) = @_;
# Parameters:
# format=raw|html
# context=patch|file|0-n
# collapsed=0|1
# headers=0|1
# Define the patch readers.
# The reader that reads the patch in (whatever its format).
require PatchReader::Raw;
my $reader = new PatchReader::Raw;
my $last_reader = $reader;
# Fix the patch root if we have a cvs root.
if (Bugzilla->params->{'cvsroot'}) {
require PatchReader::FixPatchRoot;
$last_reader->sends_data_to(new PatchReader::FixPatchRoot(Bugzilla->params->{'cvsroot'}));
$last_reader->sends_data_to->diff_root($diff_root) if defined($diff_root);
$last_reader = $last_reader->sends_data_to;
}
# Add in cvs context if we have the necessary info to do it
if ($context ne 'patch' && Bugzilla->localconfig->{cvsbin}
&& Bugzilla->params->{'cvsroot_get'})
{
require PatchReader::AddCVSContext;
# We need to set $cvsbin as global, because PatchReader::CVSClient
# needs it in order to find 'cvs'.
$main::cvsbin = Bugzilla->localconfig->{cvsbin};
$last_reader->sends_data_to(
new PatchReader::AddCVSContext($context, Bugzilla->params->{'cvsroot_get'}));
$last_reader = $last_reader->sends_data_to;
}
return ($reader, $last_reader);
}
sub setup_template_patch_reader {
my ($last_reader, $format, $context, $vars) = @_;
my $cgi = Bugzilla->cgi;
my $template = Bugzilla->template;
require PatchReader::DiffPrinter::template;
# Define the vars for templates.
if (defined $cgi->param('headers')) {
$vars->{'headers'} = $cgi->param('headers');
}
else {
$vars->{'headers'} = 1;
}
$vars->{'collapsed'} = $cgi->param('collapsed');
$vars->{'context'} = $context;
$vars->{'do_context'} = Bugzilla->localconfig->{cvsbin}
&& Bugzilla->params->{'cvsroot_get'} && !$vars->{'newid'};
# Print everything out.
print $cgi->header(-type => 'text/html',
-expires => '+3M');
$last_reader->sends_data_to(new PatchReader::DiffPrinter::template($template,
"attachment/diff-header.$format.tmpl",
"attachment/diff-file.$format.tmpl",
"attachment/diff-footer.$format.tmpl",
{ %{$vars},
bonsai_url => Bugzilla->params->{'bonsai_url'},
lxr_url => Bugzilla->params->{'lxr_url'},
lxr_root => Bugzilla->params->{'lxr_root'},
}));
}
1;
__END__

View File

@@ -1,460 +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>
# Max Kanat-Alexander <mkanat@bugzilla.org>
package Bugzilla::Auth;
use strict;
use fields qw(
_info_getter
_verifier
_persister
);
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Auth::Login::Stack;
use Bugzilla::Auth::Verify::Stack;
use Bugzilla::Auth::Persist::Cookie;
sub new {
my ($class, $params) = @_;
my $self = fields::new($class);
$params ||= {};
$params->{Login} ||= Bugzilla->params->{'user_info_class'} . ',Cookie';
$params->{Verify} ||= Bugzilla->params->{'user_verify_class'};
$self->{_info_getter} = new Bugzilla::Auth::Login::Stack($params->{Login});
$self->{_verifier} = new Bugzilla::Auth::Verify::Stack($params->{Verify});
# If we ever have any other login persistence methods besides cookies,
# this could become more configurable.
$self->{_persister} = new Bugzilla::Auth::Persist::Cookie();
return $self;
}
sub login {
my ($self, $type) = @_;
my $dbh = Bugzilla->dbh;
# Get login info from the cookie, form, environment variables, etc.
my $login_info = $self->{_info_getter}->get_login_info();
if ($login_info->{failure}) {
return $self->_handle_login_result($login_info, $type);
}
# Now verify his username and password against the DB, LDAP, etc.
if ($self->{_info_getter}->{successful}->requires_verification) {
$login_info = $self->{_verifier}->check_credentials($login_info);
if ($login_info->{failure}) {
return $self->_handle_login_result($login_info, $type);
}
$login_info =
$self->{_verifier}->{successful}->create_or_update_user($login_info);
}
else {
$login_info = $self->{_verifier}->create_or_update_user($login_info);
}
if ($login_info->{failure}) {
return $self->_handle_login_result($login_info, $type);
}
# Make sure the user isn't disabled.
my $user = $login_info->{user};
if ($user->disabledtext) {
return $self->_handle_login_result({ failure => AUTH_DISABLED,
user => $user }, $type);
}
$user->set_authorizer($self);
return $self->_handle_login_result($login_info, $type);
}
sub can_change_password {
my ($self) = @_;
my $verifier = $self->{_verifier}->{successful};
$verifier ||= $self->{_verifier};
my $getter = $self->{_info_getter}->{successful};
$getter = $self->{_info_getter}
if (!$getter || $getter->isa('Bugzilla::Auth::Login::Cookie'));
return $verifier->can_change_password &&
$getter->user_can_create_account;
}
sub can_login {
my ($self) = @_;
my $getter = $self->{_info_getter}->{successful};
$getter = $self->{_info_getter}
if (!$getter || $getter->isa('Bugzilla::Auth::Login::Cookie'));
return $getter->can_login;
}
sub can_logout {
my ($self) = @_;
my $getter = $self->{_info_getter}->{successful};
# If there's no successful getter, we're not logged in, so of
# course we can't log out!
return 0 unless $getter;
return $getter->can_logout;
}
sub user_can_create_account {
my ($self) = @_;
my $verifier = $self->{_verifier}->{successful};
$verifier ||= $self->{_verifier};
my $getter = $self->{_info_getter}->{successful};
$getter = $self->{_info_getter}
if (!$getter || $getter->isa('Bugzilla::Auth::Login::Cookie'));
return $verifier->user_can_create_account
&& $getter->user_can_create_account;
}
sub can_change_email {
return $_[0]->user_can_create_account;
}
sub _handle_login_result {
my ($self, $result, $login_type) = @_;
my $dbh = Bugzilla->dbh;
my $user = $result->{user};
my $fail_code = $result->{failure};
if (!$fail_code) {
if ($self->{_info_getter}->{successful}->requires_persistence) {
$self->{_persister}->persist_login($user);
}
}
elsif ($fail_code == AUTH_ERROR) {
ThrowCodeError($result->{error}, $result->{details});
}
elsif ($fail_code == AUTH_NODATA) {
$self->{_info_getter}->fail_nodata($self)
if $login_type == LOGIN_REQUIRED;
# If we're not LOGIN_REQUIRED, we just return the default user.
$user = Bugzilla->user;
}
# 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)
elsif ($fail_code == AUTH_LOGINFAILED or $fail_code == AUTH_NO_SUCH_USER) {
ThrowUserError("invalid_username_or_password");
}
# The account may be disabled
elsif ($fail_code == AUTH_DISABLED) {
$self->{_persister}->logout();
# XXX This is NOT a good way to do this, architecturally.
$self->{_persister}->clear_browser_cookies();
# and throw a user error
ThrowUserError("account_disabled",
{'disabled_reason' => $result->{user}->disabledtext});
}
# If we get here, then we've run out of options, which shouldn't happen.
else {
ThrowCodeError("authres_unhandled", { value => $fail_code });
}
return $user;
}
1;
__END__
=head1 NAME
Bugzilla::Auth - An object that authenticates the login credentials for
a user.
=head1 DESCRIPTION
Handles authentication for Bugzilla users.
Authentication from Bugzilla involves two sets of modules. One set is
used to obtain the username/password (from CGI, email, etc), and the
other set uses this data to authenticate against the datasource
(the Bugzilla DB, LDAP, PAM, etc.).
Modules for obtaining the username/password are subclasses of
L<Bugzilla::Auth::Login>, and modules for authenticating are subclasses
of L<Bugzilla::Auth::Verify>.
=head1 AUTHENTICATION ERROR CODES
Whenever a method in the C<Bugzilla::Auth> family fails in some way,
it will return a hashref containing at least a single key called C<failure>.
C<failure> will point to an integer error code, and depending on the error
code the hashref may contain more data.
The error codes are explained here below.
=head2 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.
=head2 C<AUTH_ERROR>
An error occurred when trying to use the login mechanism.
The hashref will also contain an C<error> element, which is the name
of an error from C<template/en/default/global/code-error.html> --
the same type of error that would be thrown by
L<Bugzilla::Error::ThrowCodeError>.
The hashref *may* contain an element called C<details>, which is a hashref
that should be passed to L<Bugzilla::Error::ThrowCodeError> as the
various fields to be used in the error message.
=head2 C<AUTH_LOGINFAILED>
An incorrect username or password was given.
=head2 C<AUTH_NO_SUCH_USER>
This is an optional more-specific version of C<AUTH_LOGINFAILED>.
Modules should throw this error when they discover that the
requested user account actually does not exist, according to them.
That is, for example, L<Bugzilla::Auth::Verify::LDAP> would throw
this if the user didn't exist in LDAP.
The difference between C<AUTH_NO_SUCH_USER> and C<AUTH_LOGINFAILED>
should never be communicated to the user, for security reasons.
=head2 C<AUTH_DISABLED>
The user successfully logged in, but their account has been disabled.
Usually this is throw only by C<Bugzilla::Auth::login>.
=head1 LOGIN TYPES
The C<login> function (below) can do different types of login, depending
on what constant you pass into it:
=head2 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.
=head2 C<LOGIN_NORMAL>
A login may or may not be required, depending on the setting of the
I<requirelogin> parameter. This is the default if you don't specify a
type.
=head2 C<LOGIN_REQUIRED>
A login is always required to access this data.
=head1 METHODS
These are methods that can be called on a C<Bugzilla::Auth> object
itself.
=head2 Login
=over 4
=item C<login($type)>
Description: Logs a user in. For more details on how this works
internally, see the section entitled "STRUCTURE."
Params: $type - One of the Login Types from above.
Returns: An authenticated C<Bugzilla::User>. Or, if the type was
not C<LOGIN_REQUIRED>, then we return an
empty C<Bugzilla::User> if no login data was passed in.
=back
=head2 Info Methods
These are methods that give information about the Bugzilla::Auth object.
=over 4
=item C<can_change_password>
Description: Tells you whether or not the current login system allows
changing passwords.
Params: None
Returns: C<true> if users and administrators should be allowed to
change passwords, C<false> otherwise.
=item C<can_login>
Description: Tells you whether or not the current login system allows
users to log in through the web interface.
Params: None
Returns: C<true> if users can log in through the web interface,
C<false> otherwise.
=item C<can_logout>
Description: Tells you whether or not the current login system allows
users to log themselves out.
Params: None
Returns: C<true> if users can log themselves out, C<false> otherwise.
If a user isn't logged in, we always return C<false>.
=item C<user_can_create_account>
Description: Tells you whether or not users are allowed to manually create
their own accounts, based on the current login system in use.
Note that this doesn't check the C<createemailregexp>
parameter--you have to do that by yourself in your code.
Params: None
Returns: C<true> if users are allowed to create new Bugzilla accounts,
C<false> otherwise.
=item C<can_change_email>
Description: Whether or not the current login system allows users to
change their own email address.
Params: None
Returns: C<true> if users can change their own email address,
C<false> otherwise.
=back
=head1 STRUCTURE
This section is mostly interesting to developers who want to implement
a new authentication type. It describes the general structure of the
Bugzilla::Auth family, and how the C<login> function works.
A C<Bugzilla::Auth> object is essentially a collection of a few other
objects: the "Info Getter," the "Verifier," and the "Persistence
Mechanism."
They are used inside the C<login> function in the following order:
=head2 The Info Getter
This is a C<Bugzilla::Auth::Login> object. Basically, it gets the
username and password from the user, somehow. Or, it just gets enough
information to uniquely identify a user, and passes that on down the line.
(For example, a C<user_id> is enough to uniquely identify a user,
even without a username and password.)
Some Info Getters don't require any verification. For example, if we got
the C<user_id> from a Cookie, we don't need to check the username and
password.
If an Info Getter returns only a C<user_id> and no username/password,
then it MUST NOT require verification. If an Info Getter requires
verfication, then it MUST return at least a C<username>.
=head2 The Verifier
This verifies that the username and password are valid.
It's possible that some methods of verification don't require a password.
=head2 The Persistence Mechanism
This makes it so that the user doesn't have to log in on every page.
Normally this object just sends a cookie to the user's web browser,
as that's the most common method of "login persistence."
=head2 Other Things We Do
After we verify the username and password, sometimes we automatically
create an account in the Bugzilla database, for certain authentication
types. We use the "Account Source" to get data about the user, and
create them in the database. (Or, if their data has changed since the
last time they logged in, their data gets updated.)
=head2 The C<$login_data> Hash
All of the C<Bugzilla::Auth::Login> and C<Bugzilla::Auth::Verify>
methods take an argument called C<$login_data>. This is basically
a hash that becomes more and more populated as we go through the
C<login> function.
All C<Bugzilla::Auth::Login> and C<Bugzilla::Auth::Verify> methods
also *return* the C<$login_data> structure, when they succeed. They
may have added new data to it.
For all C<Bugzilla::Auth::Login> and C<Bugzilla::Auth::Verify> methods,
the rule is "you must return the same hashref you were passed in." You can
modify the hashref all you want, but you can't create a new one. The only
time you can return a new one is if you're returning some error code
instead of the C<$login_data> structure.
Each C<Bugzilla::Auth::Login> or C<Bugzilla::Auth::Verify> method
explains in its documentation which C<$login_data> elements are
required by it, and which are set by it.
Here are all of the elements that *may* be in C<$login_data>:
=over 4
=item C<user_id>
A Bugzilla C<user_id> that uniquely identifies a user.
=item C<username>
The username that was provided by the user.
=item C<bz_username>
The username of this user inside of Bugzilla. Sometimes this differs from
C<username>.
=item C<password>
The password provided by the user.
=item C<realname>
The real name of the user.
=item C<extern_id>
Some string that uniquely identifies the user in an external account
source. If this C<extern_id> already exists in the database with
a different username, the username will be *changed* to be the
username specified in this C<$login_data>.
That is, let's my extern_id is C<mkanat>. I already have an account
in Bugzilla with the username of C<mkanat@foo.com>. But this time,
when I log in, I have an extern_id of C<mkanat> and a C<username>
of C<mkanat@bar.org>. So now, Bugzilla will automatically change my
username to C<mkanat@bar.org> instead of C<mkanat@foo.com>.
=item C<user>
A L<Bugzilla::User> object representing the authenticated user.
Note that C<Bugzilla::Auth::login> may modify this object at various points.
=back

View File

@@ -1,125 +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): Max Kanat-Alexander <mkanat@bugzilla.org>
package Bugzilla::Auth::Login;
use strict;
use fields qw();
# Determines whether or not a user can logout. It's really a subroutine,
# but we implement it here as a constant. Override it in subclasses if
# that particular type of login method cannot log out.
use constant can_logout => 1;
use constant can_login => 1;
use constant requires_persistence => 1;
use constant requires_verification => 1;
use constant user_can_create_account => 0;
sub new {
my ($class) = @_;
my $self = fields::new($class);
return $self;
}
1;
__END__
=head1 NAME
Bugzilla::Auth::Login - Gets username/password data from the user.
=head1 DESCRIPTION
Bugzilla::Auth::Login is used to get information that uniquely identifies
a user and allows us to authorize their Bugzilla access.
It is mostly an abstract class, requiring subclasses to implement
most methods.
Note that callers outside of the C<Bugzilla::Auth> package should never
create this object directly. Just create a C<Bugzilla::Auth> object
and call C<login> on it.
=head1 LOGIN METHODS
These are methods that have to do with getting the actual login data
from the user or handling a login somehow.
These methods are abstract -- they MUST be implemented by a subclass.
=over 4
=item C<get_login_info()>
Description: Gets a username/password from the user, or some other
information that uniquely identifies them.
Params: None
Returns: A C<$login_data> hashref. (See L<Bugzilla::Auth> for details.)
The hashref MUST contain: C<user_id> *or* C<username>
If this is a login method that requires verification,
the hashref MUST contain C<password>.
The hashref MAY contain C<realname> and C<extern_id>.
=item C<fail_nodata()>
Description: This function is called when Bugzilla doesn't get
a username/password and the login type is C<LOGIN_REQUIRED>
(See L<Bugzilla::Auth> for a description of C<LOGIN_REQUIRED>).
That is, this handles C<AUTH_NODATA> in that situation.
This function MUST stop CGI execution when it is complete.
That is, it must call C<exit> or C<ThrowUserError> or some
such thing.
Params: None
Returns: Never Returns.
=back
=head1 INFO METHODS
These are methods that describe the capabilities of this
C<Bugzilla::Auth::Login> object. These are all no-parameter
methods that return either C<true> or C<false>.
=over 4
=item C<can_logout>
Whether or not users can log out if they logged in using this
object. Defaults to C<true>.
=item C<can_login>
Whether or not users can log in through the web interface using
this object. Defaults to C<true>.
=item C<requires_persistence>
Whether or not we should send the user a cookie if they logged in with
this method. Defaults to C<true>.
=item C<requires_verification>
Whether or not we should check the username/password that we
got from this login method. Defaults to C<true>.
=item C<user_can_create_account>
Whether or not users can create accounts, if this login method is
currently being used by the system. Defaults to C<false>.
=back

View File

@@ -1,86 +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>
# Max Kanat-Alexander <mkanat@bugzilla.org>
package Bugzilla::Auth::Login::CGI;
use strict;
use base qw(Bugzilla::Auth::Login);
use constant user_can_create_account => 1;
use Bugzilla::Constants;
use Bugzilla::WebService::Constants;
use Bugzilla::Util;
use Bugzilla::Error;
sub get_login_info {
my ($self) = @_;
my $cgi = Bugzilla->cgi;
my $username = trim($cgi->param("Bugzilla_login"));
my $password = $cgi->param("Bugzilla_password");
$cgi->delete('Bugzilla_login', 'Bugzilla_password');
if (!defined $username || !defined $password) {
return { failure => AUTH_NODATA };
}
return { username => $username, password => $password };
}
sub fail_nodata {
my ($self) = @_;
my $cgi = Bugzilla->cgi;
my $template = Bugzilla->template;
if (Bugzilla->error_mode == Bugzilla::Constants::ERROR_MODE_DIE_SOAP_FAULT) {
die SOAP::Fault
->faultcode(ERROR_AUTH_NODATA)
->faultstring('Login Required');
}
# If system is not configured to never require SSL connections
# we want to always redirect to SSL since passing usernames and
# passwords over an unprotected connection is a bad idea. If we
# get here then a login form will be provided to the user so we
# want this to be protected if possible.
if ($cgi->protocol ne 'https' && Bugzilla->params->{'sslbase'} ne ''
&& Bugzilla->params->{'ssl'} ne 'never')
{
$cgi->require_https(Bugzilla->params->{'sslbase'});
}
print $cgi->header();
$template->process("account/auth/login.html.tmpl",
{ 'target' => $cgi->url(-relative=>1) })
|| ThrowTemplateError($template->error());
exit;
}
1;

View File

@@ -1,96 +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): Bradley Baetz <bbaetz@acm.org>
# Max Kanat-Alexander <mkanat@bugzilla.org>
package Bugzilla::Auth::Login::Cookie;
use strict;
use base qw(Bugzilla::Auth::Login);
use Bugzilla::Constants;
use Bugzilla::Util;
use List::Util qw(first);
use constant requires_persistence => 0;
use constant requires_verification => 0;
use constant can_login => 0;
# Note that Cookie never consults the Verifier, it always assumes
# it has a valid DB account or it fails.
sub get_login_info {
my ($self) = @_;
my $cgi = Bugzilla->cgi;
my $dbh = Bugzilla->dbh;
my $ip_addr = $cgi->remote_addr();
my $net_addr = get_netaddr($ip_addr);
my $login_cookie = $cgi->cookie("Bugzilla_logincookie");
my $user_id = $cgi->cookie("Bugzilla_login");
# If cookies cannot be found, this could mean that they haven't
# been made available yet. In this case, look at Bugzilla_cookie_list.
unless ($login_cookie) {
my $cookie = first {$_->name eq 'Bugzilla_logincookie'}
@{$cgi->{'Bugzilla_cookie_list'}};
$login_cookie = $cookie->value if $cookie;
}
unless ($user_id) {
my $cookie = first {$_->name eq 'Bugzilla_login'}
@{$cgi->{'Bugzilla_cookie_list'}};
$user_id = $cookie->value if $cookie;
}
if ($login_cookie && $user_id) {
# Anything goes for these params - they're just strings which
# we're going to verify against the db
trick_taint($ip_addr);
trick_taint($login_cookie);
detaint_natural($user_id);
my $query = "SELECT userid
FROM logincookies
WHERE logincookies.cookie = ?
AND logincookies.userid = ?
AND (logincookies.ipaddr = ?";
# If we have a network block that's allowed to use this cookie,
# as opposed to just a single IP.
my @params = ($login_cookie, $user_id, $ip_addr);
if (defined $net_addr) {
trick_taint($net_addr);
$query .= " OR logincookies.ipaddr = ?";
push(@params, $net_addr);
}
$query .= ")";
# If the cookie is valid, return a valid username.
if ($dbh->selectrow_array($query, undef, @params)) {
# 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 { user_id => $user_id };
}
}
# Either the he cookie is invalid, or we got no cookie. We don't want
# to ever return AUTH_LOGINFAILED, because we don't want Bugzilla to
# actually throw an error when it gets a bad cookie. It should just
# look like there was no cookie to begin with.
return { failure => AUTH_NODATA };
}
1;

View File

@@ -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): Erik Stambaugh <erik@dasbistro.com>
# Max Kanat-Alexander <mkanat@bugzilla.org>
package Bugzilla::Auth::Login::Env;
use strict;
use base qw(Bugzilla::Auth::Login);
use Bugzilla::Constants;
use Bugzilla::Error;
use constant can_logout => 0;
use constant can_login => 0;
use constant requires_verification => 0;
sub get_login_info {
my ($self) = @_;
my $dbh = Bugzilla->dbh;
my $env_id = $ENV{Bugzilla->params->{"auth_env_id"}} || '';
my $env_email = $ENV{Bugzilla->params->{"auth_env_email"}} || '';
my $env_realname = $ENV{Bugzilla->params->{"auth_env_realname"}} || '';
return { failure => AUTH_NODATA } if !$env_email;
return { username => $env_email, extern_id => $env_id,
realname => $env_realname };
}
sub fail_nodata {
ThrowCodeError('env_no_email');
}
1;

View File

@@ -1,95 +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): Max Kanat-Alexander <mkanat@bugzilla.org>
package Bugzilla::Auth::Login::Stack;
use strict;
use base qw(Bugzilla::Auth::Login);
use fields qw(
_stack
successful
);
use Hash::Util qw(lock_keys);
use Bugzilla::Hook;
sub new {
my $class = shift;
my $self = $class->SUPER::new(@_);
my $list = shift;
my %methods = map { $_ => "Bugzilla/Auth/Login/$_.pm" } split(',', $list);
lock_keys(%methods);
Bugzilla::Hook::process('auth-login_methods', { modules => \%methods });
$self->{_stack} = [];
foreach my $login_method (split(',', $list)) {
my $module = $methods{$login_method};
require $module;
$module =~ s|/|::|g;
$module =~ s/.pm$//;
push(@{$self->{_stack}}, $module->new(@_));
}
return $self;
}
sub get_login_info {
my $self = shift;
my $result;
foreach my $object (@{$self->{_stack}}) {
$result = $object->get_login_info(@_);
$self->{successful} = $object;
last if !$result->{failure};
# So that if none of them succeed, it's undef.
$self->{successful} = undef;
}
return $result;
}
sub fail_nodata {
my $self = shift;
# We fail from the bottom of the stack.
my @reverse_stack = reverse @{$self->{_stack}};
foreach my $object (@reverse_stack) {
# We pick the first object that actually has the method
# implemented.
if ($object->can('fail_nodata')) {
$object->fail_nodata(@_);
}
}
}
sub can_login {
my ($self) = @_;
# We return true if any method can log in.
foreach my $object (@{$self->{_stack}}) {
return 1 if $object->can_login;
}
return 0;
}
sub user_can_create_account {
my ($self) = @_;
# We return true if any method allows users to create accounts.
foreach my $object (@{$self->{_stack}}) {
return 1 if $object->user_can_create_account;
}
return 0;
}
1;

View File

@@ -1,167 +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>
# Max Kanat-Alexander <mkanat@bugzilla.org>
package Bugzilla::Auth::Persist::Cookie;
use strict;
use fields qw();
use Bugzilla::Constants;
use Bugzilla::Util;
use Bugzilla::Token;
use List::Util qw(first);
sub new {
my ($class) = @_;
my $self = fields::new($class);
return $self;
}
sub persist_login {
my ($self, $user) = @_;
my $dbh = Bugzilla->dbh;
my $cgi = Bugzilla->cgi;
my $ip_addr = $cgi->remote_addr;
unless ($cgi->param('Bugzilla_restrictlogin') ||
Bugzilla->params->{'loginnetmask'} == 32)
{
$ip_addr = get_netaddr($ip_addr);
}
# The IP address is valid, at least for comparing with itself in a
# subsequent login
trick_taint($ip_addr);
$dbh->bz_start_transaction();
my $login_cookie =
Bugzilla::Token::GenerateUniqueToken('logincookies', 'cookie');
$dbh->do("INSERT INTO logincookies (cookie, userid, ipaddr, lastused)
VALUES (?, ?, ?, NOW())",
undef, $login_cookie, $user->id, $ip_addr);
# Issuing a new cookie is a good time to clean up the old
# cookies.
$dbh->do("DELETE FROM logincookies WHERE lastused < LOCALTIMESTAMP(0) - "
. $dbh->sql_interval(MAX_LOGINCOOKIE_AGE, 'DAY'));
$dbh->bz_commit_transaction();
# Prevent JavaScript from accessing login cookies.
my %cookieargs = ('-httponly' => 1);
# Remember cookie only if admin has told so
# or admin didn't forbid it and user told to remember.
if ( Bugzilla->params->{'rememberlogin'} eq 'on' ||
(Bugzilla->params->{'rememberlogin'} ne 'off' &&
$cgi->param('Bugzilla_remember') &&
$cgi->param('Bugzilla_remember') eq 'on') )
{
# Not a session cookie, so set an infinite expiry
$cookieargs{'-expires'} = 'Fri, 01-Jan-2038 00:00:00 GMT';
}
if (Bugzilla->params->{'ssl'} ne 'never'
&& Bugzilla->params->{'sslbase'} ne '')
{
# Bugzilla->login will automatically redirect to https://,
# so it's safe to turn on the 'secure' bit.
$cookieargs{'-secure'} = 1;
}
$cgi->send_cookie(-name => 'Bugzilla_login',
-value => $user->id,
%cookieargs);
$cgi->send_cookie(-name => 'Bugzilla_logincookie',
-value => $login_cookie,
%cookieargs);
}
sub logout {
my ($self, $param) = @_;
my $dbh = Bugzilla->dbh;
my $cgi = Bugzilla->cgi;
$param = {} unless $param;
my $user = $param->{user} || Bugzilla->user;
my $type = $param->{type} || LOGOUT_ALL;
if ($type == 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 = first {$_->name eq 'Bugzilla_logincookie'}
@{$cgi->{'Bugzilla_cookie_list'}};
my $login_cookie;
if ($cookie) {
$login_cookie = $cookie->value;
}
else {
$login_cookie = $cgi->cookie("Bugzilla_logincookie");
}
trick_taint($login_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 ($type == LOGOUT_KEEP_CURRENT) {
$dbh->do("DELETE FROM logincookies WHERE cookie != ? AND userid = ?",
undef, $login_cookie, $user->id);
} elsif ($type == LOGOUT_CURRENT) {
$dbh->do("DELETE FROM logincookies WHERE cookie = ? AND userid = ?",
undef, $login_cookie, $user->id);
} else {
die("Invalid type $type supplied to logout()");
}
if ($type != LOGOUT_KEEP_CURRENT) {
clear_browser_cookies();
}
}
sub clear_browser_cookies {
my $cgi = Bugzilla->cgi;
$cgi->remove_cookie('Bugzilla_login');
$cgi->remove_cookie('Bugzilla_logincookie');
$cgi->remove_cookie('sudo');
}
1;

View File

@@ -1,235 +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): Max Kanat-Alexander <mkanat@bugzilla.org>
package Bugzilla::Auth::Verify;
use strict;
use fields qw();
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::User;
use Bugzilla::Util;
use constant user_can_create_account => 1;
sub new {
my ($class, $login_type) = @_;
my $self = fields::new($class);
return $self;
}
sub can_change_password {
return $_[0]->can('change_password');
}
sub create_or_update_user {
my ($self, $params) = @_;
my $dbh = Bugzilla->dbh;
my $extern_id = $params->{extern_id};
my $username = $params->{bz_username} || $params->{username};
my $password = $params->{password} || '*';
my $real_name = $params->{realname} || '';
my $user_id = $params->{user_id};
# A passed-in user_id always overrides anything else, for determining
# what account we should return.
if (!$user_id) {
my $username_user_id = login_to_id($username || '');
my $extern_user_id;
if ($extern_id) {
trick_taint($extern_id);
$extern_user_id = $dbh->selectrow_array('SELECT userid
FROM profiles WHERE extern_id = ?', undef, $extern_id);
}
# If we have both a valid extern_id and a valid username, and they are
# not the same id, then we have a conflict.
if ($username_user_id && $extern_user_id
&& $username_user_id ne $extern_user_id)
{
my $extern_name = Bugzilla::User->new($extern_user_id)->login;
return { failure => AUTH_ERROR, error => "extern_id_conflict",
details => {extern_id => $extern_id,
extern_user => $extern_name,
username => $username} };
}
# If we have a valid username, but no valid id,
# then we have to create the user. This happens when we're
# passed only a username, and that username doesn't exist already.
if ($username && !$username_user_id && !$extern_user_id) {
validate_email_syntax($username)
|| return { failure => AUTH_ERROR,
error => 'auth_invalid_email',
details => {addr => $username} };
# Usually we'd call validate_password, but external authentication
# systems might follow different standards than ours. So in this
# place here, we call trick_taint without checks.
trick_taint($password);
# XXX Theoretically this could fail with an error, but the fix for
# that is too involved to be done right now.
my $user = Bugzilla::User->create({
login_name => $username,
cryptpassword => $password,
realname => $real_name});
$username_user_id = $user->id;
}
# If we have a valid username id and an extern_id, but no valid
# extern_user_id, then we have to set the user's extern_id.
if ($extern_id && $username_user_id && !$extern_user_id) {
$dbh->do('UPDATE profiles SET extern_id = ? WHERE userid = ?',
undef, $extern_id, $username_user_id);
}
# Finally, at this point, one of these will give us a valid user id.
$user_id = $extern_user_id || $username_user_id;
}
# If we still don't have a valid user_id, then we weren't passed
# enough information in $params, and we should die right here.
ThrowCodeError('bad_arg', {argument => 'params', function =>
'Bugzilla::Auth::Verify::create_or_update_user'})
unless $user_id;
my $user = new Bugzilla::User($user_id);
# Now that we have a valid User, we need to see if any data has to be
# updated.
if ($username && lc($user->login) ne lc($username)) {
validate_email_syntax($username)
|| return { failure => AUTH_ERROR, error => 'auth_invalid_email',
details => {addr => $username} };
$user->set_login($username);
}
if ($real_name && $user->name ne $real_name) {
# $real_name is more than likely tainted, but we only use it
# in a placeholder and we never use it after this.
trick_taint($real_name);
$user->set_name($real_name);
}
$user->update();
return { user => $user };
}
1;
__END__
=head1 NAME
Bugzilla::Auth::Verify - An object that verifies usernames and passwords.
=head1 DESCRIPTION
Bugzilla::Auth::Verify provides the "Verifier" part of the Bugzilla
login process. (For details, see the "STRUCTURE" section of
L<Bugzilla::Auth>.)
It is mostly an abstract class, requiring subclasses to implement
most methods.
Note that callers outside of the C<Bugzilla::Auth> package should never
create this object directly. Just create a C<Bugzilla::Auth> object
and call C<login> on it.
=head1 VERIFICATION METHODS
These are the methods that have to do with the actual verification.
Subclasses MUST implement these methods.
=over 4
=item C<check_credentials($login_data)>
Description: Checks whether or not a username is valid.
Params: $login_data - A C<$login_data> hashref, as described in
L<Bugzilla::Auth>.
This C<$login_data> hashref MUST contain
C<username>, and SHOULD also contain
C<password>.
Returns: A C<$login_data> hashref with C<bz_username> set. This
method may also set C<realname>. It must avoid changing
anything that is already set.
=back
=head1 MODIFICATION METHODS
These are methods that change data in the actual authentication backend.
These methods are optional, they do not have to be implemented by
subclasses.
=over 4
=item C<create_or_update_user($login_data)>
Description: Automatically creates a user account in the database
if it doesn't already exist, or updates the account
data if C<$login_data> contains newer information.
Params: $login_data - A C<$login_data> hashref, as described in
L<Bugzilla::Auth>.
This C<$login_data> hashref MUST contain
either C<user_id>, C<bz_username>, or
C<username>. If both C<username> and C<bz_username>
are specified, C<bz_username> is used as the
login name of the user to create in the database.
It MAY also contain C<extern_id>, in which
case it still MUST contain C<bz_username> or
C<username>.
It MAY contain C<password> and C<realname>.
Returns: A hashref with one element, C<user>, which is a
L<Bugzilla::User> object. May also return a login error
as described in L<Bugzilla::Auth>.
Note: This method is not abstract, it is actually implemented
and creates accounts in the Bugzilla database. Subclasses
should probably all call the C<Bugzilla::Auth::Verify>
version of this function at the end of their own
C<create_or_update_user>.
=item C<change_password($user, $password)>
Description: Modifies the user's password in the authentication backend.
Params: $user - A L<Bugzilla::User> object representing the user
whose password we want to change.
$password - The user's new password.
Returns: Nothing.
=back
=head1 INFO METHODS
These are methods that describe the capabilities of this object.
These are all no-parameter methods that return either C<true> or
C<false>.
=over 4
=item C<user_can_create_account>
Whether or not users can manually create accounts in this type of
account source. Defaults to C<true>.
=back

View File

@@ -1,88 +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 base qw(Bugzilla::Auth::Verify);
use Bugzilla::Constants;
use Bugzilla::Token;
use Bugzilla::Util;
use Bugzilla::User;
sub check_credentials {
my ($self, $login_data) = @_;
my $dbh = Bugzilla->dbh;
my $username = $login_data->{username};
my $user_id = login_to_id($username);
return { failure => AUTH_NO_SUCH_USER } unless $user_id;
$login_data->{bz_username} = $username;
my $password = $login_data->{password};
trick_taint($username);
my ($real_password_crypted) = $dbh->selectrow_array(
"SELECT cryptpassword FROM profiles WHERE userid = ?",
undef, $user_id);
# Using the internal crypted password as the salt,
# crypt the password the user entered.
my $entered_password_crypted = bz_crypt($password, $real_password_crypted);
return { failure => AUTH_LOGINFAILED }
if $entered_password_crypted ne $real_password_crypted;
# The user's credentials are okay, so delete any outstanding
# password tokens they may have generated.
Bugzilla::Token::DeletePasswordTokens($user_id, "user_logged_in");
# If their old password was using crypt() or some different hash
# than we're using now, convert the stored password to using
# whatever hashing system we're using now.
my $current_algorithm = PASSWORD_DIGEST_ALGORITHM;
if ($real_password_crypted !~ /{\Q$current_algorithm\E}$/) {
my $new_crypted = bz_crypt($password);
$dbh->do('UPDATE profiles SET cryptpassword = ? WHERE userid = ?',
undef, $new_crypted, $user_id);
}
return $login_data;
}
sub change_password {
my ($self, $user, $password) = @_;
my $dbh = Bugzilla->dbh;
my $cryptpassword = bz_crypt($password);
$dbh->do("UPDATE profiles SET cryptpassword = ? WHERE userid = ?",
undef, $cryptpassword, $user->id);
}
1;

View File

@@ -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>
# 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>
# Max Kanat-Alexander <mkanat@bugzilla.org>
package Bugzilla::Auth::Verify::LDAP;
use strict;
use base qw(Bugzilla::Auth::Verify);
use fields qw(
ldap
);
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::User;
use Bugzilla::Util;
use Net::LDAP;
use constant admin_can_create_account => 0;
use constant user_can_create_account => 0;
sub check_credentials {
my ($self, $params) = @_;
my $dbh = Bugzilla->dbh;
# 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.
$self->_bind_ldap_anonymously();
# Now, we verify that the user exists, and get a LDAP Distinguished
# Name for the user.
my $username = $params->{username};
my $dn_result = $self->ldap->search(_bz_search_params($username),
attrs => ['dn']);
return { failure => AUTH_ERROR, error => "ldap_search_error",
details => {errstr => $dn_result->error, username => $username}
} if $dn_result->code;
return { failure => AUTH_NO_SUCH_USER } if !$dn_result->count;
my $dn = $dn_result->shift_entry->dn;
# Check the password.
my $pw_result = $self->ldap->bind($dn, password => $params->{password});
return { failure => AUTH_LOGINFAILED } if $pw_result->code;
# And now we fill in the user's details.
my $detail_result = $self->ldap->search(_bz_search_params($username));
return { failure => AUTH_ERROR, error => "ldap_search_error",
details => {errstr => $detail_result->error, username => $username}
} if $detail_result->code;
my $user_entry = $detail_result->shift_entry;
my $mail_attr = Bugzilla->params->{"LDAPmailattribute"};
if ($mail_attr) {
if (!$user_entry->exists($mail_attr)) {
return { failure => AUTH_ERROR,
error => "ldap_cannot_retreive_attr",
details => {attr => $mail_attr} };
}
my @emails = $user_entry->get_value($mail_attr);
# Default to the first email address returned.
$params->{bz_username} = $emails[0];
if (@emails > 1) {
# Cycle through the adresses and check if they're Bugzilla logins.
# Use the first one that returns a valid id.
foreach my $email (@emails) {
if ( login_to_id($email) ) {
$params->{bz_username} = $email;
last;
}
}
}
} else {
$params->{bz_username} = $username;
}
$params->{realname} ||= $user_entry->get_value("displayName");
$params->{realname} ||= $user_entry->get_value("cn");
$params->{extern_id} = $username;
return $params;
}
sub _bz_search_params {
my ($username) = @_;
return (base => Bugzilla->params->{"LDAPBaseDN"},
scope => "sub",
filter => '(&(' . Bugzilla->params->{"LDAPuidattribute"}
. "=$username)"
. Bugzilla->params->{"LDAPfilter"} . ')');
}
sub _bind_ldap_anonymously {
my ($self) = @_;
my $bind_result;
if (Bugzilla->params->{"LDAPbinddn"}) {
my ($LDAPbinddn,$LDAPbindpass) =
split(":",Bugzilla->params->{"LDAPbinddn"});
$bind_result =
$self->ldap->bind($LDAPbinddn, password => $LDAPbindpass);
}
else {
$bind_result = $self->ldap->bind();
}
ThrowCodeError("ldap_bind_failed", {errstr => $bind_result->error})
if $bind_result->code;
}
# We can't just do this in new(), because we're not allowed to throw any
# error from anywhere under Bugzilla::Auth::new -- otherwise we
# could create a situation where the admin couldn't get to editparams
# to fix his mistake. (Because Bugzilla->login always calls
# Bugzilla::Auth->new, and almost every page calls Bugzilla->login.)
sub ldap {
my ($self) = @_;
return $self->{ldap} if $self->{ldap};
my @servers = split(/[\s,]+/, Bugzilla->params->{"LDAPserver"});
ThrowCodeError("ldap_server_not_defined") unless @servers;
foreach (@servers) {
$self->{ldap} = new Net::LDAP(trim($_));
last if $self->{ldap};
}
ThrowCodeError("ldap_connect_failed", { server => join(", ", @servers) })
unless $self->{ldap};
# try to start TLS if needed
if (Bugzilla->params->{"LDAPstarttls"}) {
my $mesg = $self->{ldap}->start_tls();
ThrowCodeError("ldap_start_tls_failed", { error => $mesg->error() })
if $mesg->code();
}
return $self->{ldap};
}
1;

View File

@@ -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 Marc Schumann.
# Portions created by Marc Schumann are Copyright (c) 2007 Marc Schumann.
# All rights reserved.
#
# Contributor(s): Marc Schumann <wurblzap@gmail.com>
package Bugzilla::Auth::Verify::RADIUS;
use strict;
use base qw(Bugzilla::Auth::Verify);
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Util;
use Authen::Radius;
use constant admin_can_create_account => 0;
use constant user_can_create_account => 0;
sub check_credentials {
my ($self, $params) = @_;
my $dbh = Bugzilla->dbh;
my $address_suffix = Bugzilla->params->{'RADIUS_email_suffix'};
my $username = $params->{username};
# If we're using RADIUS_email_suffix, we may need to cut it off from
# the login name.
if ($address_suffix) {
$username =~ s/\Q$address_suffix\E$//i;
}
# Create RADIUS object.
my $radius =
new Authen::Radius(Host => Bugzilla->params->{'RADIUS_server'},
Secret => Bugzilla->params->{'RADIUS_secret'})
|| return { failure => AUTH_ERROR, error => 'radius_preparation_error',
details => {errstr => Authen::Radius::strerror() } };
# Check the password.
$radius->check_pwd($username, $params->{password},
Bugzilla->params->{'RADIUS_NAS_IP'} || undef)
|| return { failure => AUTH_LOGINFAILED };
# Build the user account's e-mail address.
$params->{bz_username} = $username . $address_suffix;
return $params;
}
1;

View File

@@ -1,89 +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): Max Kanat-Alexander <mkanat@bugzilla.org>
package Bugzilla::Auth::Verify::Stack;
use strict;
use base qw(Bugzilla::Auth::Verify);
use fields qw(
_stack
successful
);
use Hash::Util qw(lock_keys);
use Bugzilla::Hook;
sub new {
my $class = shift;
my $list = shift;
my $self = $class->SUPER::new(@_);
my %methods = map { $_ => "Bugzilla/Auth/Verify/$_.pm" } split(',', $list);
lock_keys(%methods);
Bugzilla::Hook::process('auth-verify_methods', { modules => \%methods });
$self->{_stack} = [];
foreach my $verify_method (split(',', $list)) {
my $module = $methods{$verify_method};
require $module;
$module =~ s|/|::|g;
$module =~ s/.pm$//;
push(@{$self->{_stack}}, $module->new(@_));
}
return $self;
}
sub can_change_password {
my ($self) = @_;
# We return true if any method can change passwords.
foreach my $object (@{$self->{_stack}}) {
return 1 if $object->can_change_password;
}
return 0;
}
sub check_credentials {
my $self = shift;
my $result;
foreach my $object (@{$self->{_stack}}) {
$result = $object->check_credentials(@_);
$self->{successful} = $object;
last if !$result->{failure};
# So that if none of them succeed, it's undef.
$self->{successful} = undef;
}
# Returns the result at the bottom of the stack if they all fail.
return $result;
}
sub create_or_update_user {
my $self = shift;
my $result;
foreach my $object (@{$self->{_stack}}) {
$result = $object->create_or_update_user(@_);
last if !$result->{failure};
}
# Returns the result at the bottom of the stack if they all fail.
return $result;
}
sub user_can_create_account {
my ($self) = @_;
# We return true if any method allows the user to create an account.
foreach my $object (@{$self->{_stack}}) {
return 1 if $object->user_can_create_account;
}
return 0;
}
1;

File diff suppressed because it is too large Load Diff

View File

@@ -1,700 +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::Error;
use Bugzilla::User;
use Bugzilla::Constants;
use Bugzilla::Util;
use Bugzilla::Bug;
use Bugzilla::Classification;
use Bugzilla::Product;
use Bugzilla::Component;
use Bugzilla::Status;
use Bugzilla::Mailer;
use Date::Parse;
use Date::Format;
use constant FORMAT_TRIPLE => "%19s|%-28s|%-28s";
use constant FORMAT_3_SIZE => [19,28,28];
use constant FORMAT_DOUBLE => "%19s %-55s";
use constant FORMAT_2_SIZE => [19,55];
use constant BIT_DIRECT => 1;
use constant BIT_WATCHING => 2;
# We need these strings for the X-Bugzilla-Reasons header
# Note: this hash uses "," rather than "=>" to avoid auto-quoting of the LHS.
use constant REL_NAMES => {
REL_ASSIGNEE , "AssignedTo",
REL_REPORTER , "Reporter",
REL_QA , "QAcontact",
REL_CC , "CC",
REL_VOTER , "Voter",
REL_GLOBAL_WATCHER, "GlobalWatcher"
};
# We use this instead of format because format doesn't deal well with
# multi-byte languages.
sub multiline_sprintf {
my ($format, $args, $sizes) = @_;
my @parts;
my @my_sizes = @$sizes; # Copy this so we don't modify the input array.
foreach my $string (@$args) {
my $size = shift @my_sizes;
my @pieces = split("\n", wrap_hard($string, $size));
push(@parts, \@pieces);
}
my $formatted;
while (1) {
# Get the first item of each part.
my @line = map { shift @$_ } @parts;
# If they're all undef, we're done.
last if !grep { defined $_ } @line;
# Make any single undef item into ''
@line = map { defined $_ ? $_ : '' } @line;
# And append a formatted line
$formatted .= sprintf($format, @line);
# Remove trailing spaces, or they become lots of =20's in
# quoted-printable emails.
$formatted =~ s/\s+$//;
$formatted .= "\n";
}
return $formatted;
}
sub three_columns {
return multiline_sprintf(FORMAT_TRIPLE, \@_, FORMAT_3_SIZE);
}
# 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) = (@_);
my @headerlist;
my %defmailhead;
my %fielddescription;
my $msg = "";
my $dbh = Bugzilla->dbh;
# XXX - These variables below are useless. We could use field object
# methods directly. But we first have to implement a cache in
# Bugzilla->get_fields to avoid querying the DB all the time.
foreach my $field (Bugzilla->get_fields({obsolete => 0})) {
push(@headerlist, $field->name);
$defmailhead{$field->name} = $field->in_new_bugmail;
$fielddescription{$field->name} = $field->description;
}
my %values = %{$dbh->selectrow_hashref(
'SELECT ' . join(',', editable_bug_fields()) . ', reporter,
lastdiffed AS start_time, LOCALTIMESTAMP(0) AS end_time
FROM bugs WHERE bug_id = ?',
undef, $id)};
my $product = new Bugzilla::Product($values{product_id});
$values{product} = $product->name;
if (Bugzilla->params->{'useclassification'}) {
$values{classification} = Bugzilla::Classification->new($product->classification_id)->name;
}
my $component = new Bugzilla::Component($values{component_id});
$values{component} = $component->name;
my ($start, $end) = ($values{start_time}, $values{end_time});
# 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, login_to_id($forced->{'owner'}, THROW_ERROR));
}
if ($forced->{'qacontact'}) {
push (@qa_contacts, login_to_id($forced->{'qacontact'}, THROW_ERROR));
}
if ($forced->{'cc'}) {
foreach my $cc (@{$forced->{'cc'}}) {
push(@ccs, login_to_id($cc, THROW_ERROR));
}
}
# Convert to names, for later display
$values{'changer'} = $changer;
# If no changer is specified, then it has no name.
if ($changer) {
$values{'changername'} = Bugzilla::User->new({name => $changer})->name;
}
$values{'assigned_to'} = user_id_to_login($values{'assigned_to'});
$values{'reporter'} = user_id_to_login($values{'reporter'});
if ($values{'qa_contact'}) {
$values{'qa_contact'} = user_id_to_login($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 $grouplist = $dbh->selectcol_arrayref(
' SELECT name FROM groups
INNER JOIN bug_group_map
ON groups.id = bug_group_map.group_id
AND bug_group_map.bug_id = ?',
undef, ($id));
$values{'bug_group'} = join(', ', @$grouplist);
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, profiles.realname, 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.id = 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 @new_depbugs;
my $difftext = "";
my $diffheader = "";
my @diffparts;
my $lastwho = "";
my $fullwho;
my @changedfields;
foreach my $ref (@$diffs) {
my ($who, $whoname, $what, $when, $old, $new, $attachid, $fieldname) = (@$ref);
my $diffpart = {};
if ($who ne $lastwho) {
$lastwho = $who;
$fullwho = $whoname ? "$whoname <$who>" : $who;
$diffheader = "\n$fullwho changed:\n\n";
$diffheader .= three_columns("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 ($fieldname eq 'dependson') {
push(@new_depbugs, grep {$_ =~ /^\d+$/} split(/[\s,]+/, $new));
}
if ($attachid) {
($diffpart->{'isprivate'}) = $dbh->selectrow_array(
'SELECT isprivate FROM attachments WHERE attach_id = ?',
undef, ($attachid));
}
$difftext = three_columns($what, $old, $new);
$diffpart->{'header'} = $diffheader;
$diffpart->{'fieldname'} = $fieldname;
$diffpart->{'text'} = $difftext;
push(@diffparts, $diffpart);
push(@changedfields, $what);
}
$values{'changed_fields'} = join(' ', @changedfields);
my @depbugs;
my $deptext = "";
# Do not include data about dependent bugs when they have just been added.
# Completely skip checking for dependent bugs on bug creation as all
# dependencies bugs will just have been added.
if ($start) {
my $dep_restriction = "";
if (scalar @new_depbugs) {
$dep_restriction = "AND bugs_activity.bug_id NOT IN (" .
join(", ", @new_depbugs) . ")";
}
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.id = bugs_activity.fieldid
WHERE dependencies.blocked = ?
AND (fielddefs.name = 'bug_status'
OR fielddefs.name = 'resolution')
$when_restriction
$dep_restriction
ORDER BY bugs_activity.bug_when, bugs.bug_id", undef, @args);
my $thisdiff = "";
my $lastbug = "";
my $interestingchange = 0;
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 = Bugzilla->params->{"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 .= three_columns("What ", "Old Value", "New Value");
$thisdiff .= ('-' x 76) . "\n";
$interestingchange = 0;
}
$thisdiff .= three_columns($fielddescription{$what}, $old, $new);
if ($what eq 'bug_status'
&& is_open_state($old) ne is_open_state($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 ($comments, $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;
my %watching;
# 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));
$recipients{$_}->{+REL_VOTER} = BIT_DIRECT foreach (@$voters);
# CCs
$recipients{$_}->{+REL_CC} = BIT_DIRECT foreach (@ccs);
# Reporter (there's only ever one)
$recipients{$reporter}->{+REL_REPORTER} = BIT_DIRECT;
# QA Contact
if (Bugzilla->params->{'useqacontact'}) {
foreach (@qa_contacts) {
# QA Contact can be blank; ignore it if so.
$recipients{$_}->{+REL_QA} = BIT_DIRECT if $_;
}
}
# Assignee
$recipients{$_}->{+REL_ASSIGNEE} = BIT_DIRECT 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, $whoname, $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);
$recipients{$uid}->{+REL_CC} = BIT_DIRECT if $uid;
}
}
elsif ($what eq "QAContact") {
my $uid = login_to_id($old);
$recipients{$uid}->{+REL_QA} = BIT_DIRECT if $uid;
}
elsif ($what eq "AssignedTo") {
my $uid = login_to_id($old);
$recipients{$uid}->{+REL_ASSIGNEE} = BIT_DIRECT if $uid;
}
}
}
# 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)");
# Mark these people as having the role of the person they are watching
foreach my $watch (@$userwatchers) {
while (my ($role, $bits) = each %{$recipients{$watch->[1]}}) {
$recipients{$watch->[0]}->{$role} |= BIT_WATCHING
if $bits & BIT_DIRECT;
}
push(@{$watching{$watch->[0]}}, $watch->[1]);
}
# Global watcher
my @watchers = split(/[,\s]+/, Bugzilla->params->{'globalwatchers'});
foreach (@watchers) {
my $watcher_id = login_to_id($_);
next unless $watcher_id;
$recipients{$watcher_id}->{+REL_GLOBAL_WATCHER} = BIT_DIRECT;
}
# 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 (keys %{$recipients{$user_id}}) {
if ($user->wants_bug_mail($id,
$relationship,
$diffs,
$comments,
$deptext,
$changer,
!$start))
{
$rels_which_want{$relationship} =
$recipients{$user_id}->{$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 $anyprivate && !$user->is_insider;
# We shouldn't send mail if this is a dependency mail (i.e. there
# is something in @depbugs), and any of the depending bugs are not
# visible to the user. This is to avoid leaking the summaries of
# 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 ($user->email_enabled &&
$insider_ok &&
$dep_ok)
{
# OK, OK, if we must. Email the user.
$sent_mail = sendMail($user,
\@headerlist,
\%rels_which_want,
\%values,
\%defmailhead,
\%fielddescription,
\@diffparts,
$comments,
$anyprivate,
! $start,
$id,
exists $watching{$user_id} ?
$watching{$user_id} : undef);
}
}
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, $isnew,
$id, $watchingRef) = @_;
my %values = %$valueRef;
my @headerlist = @$hlRef;
my %mailhead = %$dmhRef;
my %fielddescription = %$fdRef;
my @diffparts = @$diffRef;
# 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'))
{
$add_diff = 1 if $user->is_timetracker;
} elsif ($diff->{'isprivate'}
&& !$user->is_insider)
{
$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 "" && !$isnew) {
# Whoops, no differences!
return 0;
}
# 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 =
Bugzilla->params->{'urlbase'} . "attachment.cgi?id=";
$newcomments =~ s/(Created an attachment \(id=([0-9]+)\))/$1\n --> \(${showattachurlbase}$2\)/g;
}
my $diffs = $difftext;
if ($isnew) {
my $head = "";
foreach my $f (@headerlist) {
next unless $mailhead{$f};
my $value = $values{$f};
# If there isn't anything to show, don't include this header.
next unless $value;
# Only send estimated_time if it is enabled and the user is in the group.
if (($f ne 'estimated_time' && $f ne 'deadline') || $user->is_timetracker) {
my $desc = $fielddescription{$f};
$head .= multiline_sprintf(FORMAT_DOUBLE, ["$desc:", $value],
FORMAT_2_SIZE);
}
}
$diffs = $head . ($difftext ? "\n\n" : "") . $diffs;
}
my (@reasons, @reasons_watch);
while (my ($relationship, $bits) = each %{$relRef}) {
push(@reasons, $relationship) if ($bits & BIT_DIRECT);
push(@reasons_watch, $relationship) if ($bits & BIT_WATCHING);
}
my @headerrel = map { REL_NAMES->{$_} } @reasons;
my @watchingrel = map { REL_NAMES->{$_} } @reasons_watch;
push(@headerrel, 'None') unless @headerrel;
push(@watchingrel, 'None') unless @watchingrel;
push @watchingrel, map { user_id_to_login($_) } @$watchingRef;
my $vars = {
isnew => $isnew,
to => $user->email,
bugid => $id,
alias => Bugzilla->params->{'usebugaliases'} ? $values{'alias'} : "",
classification => $values{'classification'},
product => $values{'product'},
comp => $values{'component'},
keywords => $values{'keywords'},
severity => $values{'bug_severity'},
status => $values{'bug_status'},
priority => $values{'priority'},
assignedto => $values{'assigned_to'},
assignedtoname => Bugzilla::User->new({name => $values{'assigned_to'}})->name,
targetmilestone => $values{'target_milestone'},
changedfields => $values{'changed_fields'},
summary => $values{'short_desc'},
reasons => \@reasons,
reasons_watch => \@reasons_watch,
reasonsheader => join(" ", @headerrel),
reasonswatchheader => join(" ", @watchingrel),
changer => $values{'changer'},
changername => $values{'changername'},
reporter => $values{'reporter'},
reportername => Bugzilla::User->new({name => $values{'reporter'}})->name,
diffs => $diffs,
new_comments => $newcomments,
threadingmarker => build_thread_marker($id, $user->id, $isnew),
};
my $msg;
my $template = Bugzilla->template_inner($user->settings->{'lang'}->{'value'});
$template->process("email/newchangedmail.txt.tmpl", $vars, \$msg)
|| ThrowTemplateError($template->error());
Bugzilla->template_inner("");
MessageToMTA($msg);
return 1;
}
# Get bug comments for the given period.
sub get_comments_by_bug {
my ($id, $start, $end) = @_;
my $dbh = Bugzilla->dbh;
my $result = "";
my $count = 0;
my $anyprivate = 0;
# $start will be undef for new bugs, and defined for pre-existing bugs.
if ($start) {
# If $start is not NULL, obtain the count-index
# of this comment for the leading "Comment #xxx" line.
$count = $dbh->selectrow_array('SELECT COUNT(*) FROM longdescs
WHERE bug_id = ? AND bug_when <= ?',
undef, ($id, $start));
}
my $raw = 1; # Do not format comments which are not of type CMT_NORMAL.
my $comments = Bugzilla::Bug::GetComments($id, "oldest_to_newest", $start, $end, $raw);
foreach my $comment (@$comments) {
$comment->{count} = $count++;
}
if (Bugzilla->params->{'insidergroup'}) {
$anyprivate = 1 if scalar(grep {$_->{'isprivate'} > 0} @$comments);
}
return ($comments, $anyprivate);
}
1;

View File

@@ -1,479 +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::Constants;
use Bugzilla::Error;
use Bugzilla::Util;
# We need to disable output buffering - see bug 179174
$| = 1;
# Ignore SIGTERM and SIGPIPE - this prevents DB corruption. If the user closes
# their browser window while a script is running, the web server sends these
# signals, and we don't want to die half way through a write.
$::SIG{TERM} = 'IGNORE';
$::SIG{PIPE} = 'IGNORE';
# 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);
# Make sure our outgoing cookie list is empty on each invocation
$self->{Bugzilla_cookie_list} = [];
# Send appropriate charset
$self->charset(Bugzilla->params->{'utf8'} ? 'UTF-8' : '');
# Redirect to urlbase/sslbase if we are not viewing an attachment.
if (use_attachbase() && i_am_cgi()) {
my $cgi_file = $self->url('-path_info' => 0, '-query' => 0, '-relative' => 1);
$cgi_file =~ s/\?$//;
my $urlbase = Bugzilla->params->{'urlbase'};
my $sslbase = Bugzilla->params->{'sslbase'};
my $path_regexp = $sslbase ? qr/^(\Q$urlbase\E|\Q$sslbase\E)/ : qr/^\Q$urlbase\E/;
if ($cgi_file ne 'attachment.cgi' && $self->self_url !~ /$path_regexp/) {
$self->redirect_to_urlbase;
}
}
# 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) {
# 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;
# Remove the Boolean Charts for standard query.cgi fields
# They are listed in the query URL already
next if $key =~ /^(field|type|value)(-\d+){3}$/;
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);
}
sub clean_search_url {
my $self = shift;
# Delete any empty URL parameter.
my @cgi_params = $self->param;
foreach my $param (@cgi_params) {
if (defined $self->param($param) && $self->param($param) eq '') {
$self->delete($param);
$self->delete("${param}_type");
}
# Boolean Chart stuff is empty if it's "noop"
if ($param =~ /\d-\d-\d/ && defined $self->param($param)
&& $self->param($param) eq 'noop')
{
$self->delete($param);
}
}
# Delete certain parameters if the associated parameter is empty.
$self->delete('bugidtype') if !$self->param('bug_id');
# Delete leftovers from the login form
$self->delete('Bugzilla_remember', 'GoAheadAndLogIn');
foreach my $num (1,2) {
# If there's no value in the email field, delete the related fields.
if (!$self->param("email$num")) {
foreach my $field qw(type assigned_to reporter qa_contact
cc longdesc)
{
$self->delete("email$field$num");
}
}
}
# chfieldto is set to "Now" by default in query.cgi. But if none
# of the other chfield parameters are set, it's meaningless.
if (!defined $self->param('chfieldfrom') && !$self->param('chfield')
&& !defined $self->param('chfieldvalue'))
{
$self->delete('chfieldto');
}
# cmdtype "doit" is the default from query.cgi, but it's only meaningful
# if there's a remtype parameter.
if (defined $self->param('cmdtype') && $self->param('cmdtype') eq 'doit'
&& !defined $self->param('remtype'))
{
$self->delete('cmdtype');
}
# "Reuse same sort as last time" is actually the default, so we don't
# need it in the URL.
if ($self->param('order')
&& $self->param('order') eq 'Reuse same sort as last time')
{
$self->delete('order');
}
# And now finally, if query_format is our only parameter, that
# really means we have no parameters, so we should delete query_format.
if ($self->param('query_format') && scalar($self->param()) == 1) {
$self->delete('query_format');
}
}
# 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.
return $self->header(
%param,
) . "WARNING: YOUR BROWSER DOESN'T SUPPORT THIS SERVER-PUSH TECHNOLOGY." . $self->multipart_end;
}
# Have to add the cookies in.
sub multipart_start {
my $self = shift;
my %args = @_;
# CGI.pm::multipart_start doesn't honour its own charset information, so
# we do it ourselves here
if (defined $self->charset() && defined $args{-type}) {
# Remove any existing charset specifier
$args{-type} =~ s/;.*$//;
# and add the specified one
$args{-type} .= '; charset=' . $self->charset();
}
my $headers = $self->SUPER::multipart_start(%args);
# Eliminate the one extra CRLF at the end.
$headers =~ s/$CGI::CRLF$//;
# Add the cookies. We have to do it this way instead of
# passing them to multpart_start, because CGI.pm's multipart_start
# doesn't understand a '-cookie' argument pointing to an arrayref.
foreach my $cookie (@{$self->{Bugzilla_cookie_list}}) {
$headers .= "Set-Cookie: ${cookie}${CGI::CRLF}";
}
$headers .= $CGI::CRLF;
return $headers;
}
# 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(@_) || "";
}
sub param {
my $self = shift;
# When we are just requesting the value of a parameter...
if (scalar(@_) == 1) {
my @result = $self->SUPER::param(@_);
# Also look at the URL parameters, after we look at the POST
# parameters. This is to allow things like login-form submissions
# with URL parameters in the form's "target" attribute.
if (!scalar(@result)
&& $self->request_method && $self->request_method eq 'POST')
{
@result = $self->SUPER::url_param(@_);
}
# Fix UTF-8-ness of input parameters.
if (Bugzilla->params->{'utf8'}) {
@result = map { _fix_utf8($_) } @result;
}
return wantarray ? @result : $result[0];
}
# And for various other functions in CGI.pm, we need to correctly
# return the URL parameters in addition to the POST parameters when
# asked for the list of parameters.
elsif (!scalar(@_) && $self->request_method
&& $self->request_method eq 'POST')
{
my @post_params = $self->SUPER::param;
my @url_params = $self->url_param;
my %params = map { $_ => 1 } (@post_params, @url_params);
return keys %params;
}
return $self->SUPER::param(@_);
}
sub _fix_utf8 {
my $input = shift;
# The is_utf8 is here in case CGI gets smart about utf8 someday.
utf8::decode($input) if defined $input && !utf8::is_utf8($input);
return $input;
}
# 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'} = Bugzilla->params->{'cookiepath'};
$paramhash{'-domain'} = Bugzilla->params->{'cookiedomain'}
if Bugzilla->params->{'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, $url) = @_;
# Do not create query string if data submitted via XMLRPC
# since we want the data to be resubmitted over POST method.
my $query = Bugzilla->usage_mode == USAGE_MODE_WEBSERVICE ? 0 : 1;
# XMLRPC clients (SOAP::Lite at least) requires 301 to redirect properly
# and do not work with 302.
my $status = Bugzilla->usage_mode == USAGE_MODE_WEBSERVICE ? 301 : 302;
if (defined $url) {
$url .= $self->url('-path_info' => 1, '-query' => $query, '-relative' => 1);
} else {
$url = $self->self_url;
$url =~ s/^http:/https:/i;
}
print $self->redirect(-location => $url, -status => $status);
# When using XML-RPC with mod_perl, we need the headers sent immediately.
$self->r->rflush if $ENV{MOD_PERL};
exit;
}
# Redirect to the urlbase version of the current URL.
sub redirect_to_urlbase {
my $self = shift;
my $path = $self->url('-path_info' => 1, '-query' => 1, '-relative' => 1);
print $self->redirect('-location' => correct_urlbase() . $path);
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 redirects the client to a different location using the https protocol.
If the client is using XMLRPC, it will not retain the QUERY_STRING since XMLRPC uses POST.
It takes an optional argument which will be used as the base URL. If $baseurl
is not provided, the current URL is used.
=item C<redirect_to_urlbase>
Redirects from the current URL to one prefixed by the urlbase parameter.
=back
=head1 SEE ALSO
L<CGI|CGI>, L<CGI::Cookie|CGI::Cookie>

View File

@@ -1,445 +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;
# 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::Error;
use Bugzilla::Util;
use Bugzilla::Series;
use Date::Format;
use Date::Parse;
use List::Util qw(max);
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...
# &gt=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'} = max(@processed_datediff);
}
else {
$self->{'y_max_value'} = 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;
my $grouplist = Bugzilla->user->groups_as_string;
# 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;

View File

@@ -1,216 +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>
# Frédéric Buclin <LpSolit@gmail.com>
use strict;
package Bugzilla::Classification;
use Bugzilla::Constants;
use Bugzilla::Util;
use Bugzilla::Error;
use Bugzilla::Product;
use base qw(Bugzilla::Object);
###############################
#### Initialization ####
###############################
use constant DB_TABLE => 'classifications';
use constant LIST_ORDER => 'sortkey, name';
use constant DB_COLUMNS => qw(
id
name
description
sortkey
);
use constant REQUIRED_CREATE_FIELDS => qw(
name
);
use constant UPDATE_COLUMNS => qw(
name
description
sortkey
);
use constant VALIDATORS => {
name => \&_check_name,
description => \&_check_description,
sortkey => \&_check_sortkey,
};
###############################
#### Constructors #####
###############################
sub remove_from_db {
my $self = shift;
my $dbh = Bugzilla->dbh;
ThrowUserError("classification_not_deletable") if ($self->id == 1);
$dbh->bz_start_transaction();
# Reclassify products to the default classification, if needed.
$dbh->do("UPDATE products SET classification_id = 1
WHERE classification_id = ?", undef, $self->id);
$dbh->do("DELETE FROM classifications WHERE id = ?", undef, $self->id);
$dbh->bz_commit_transaction();
}
###############################
#### Validators ####
###############################
sub _check_name {
my ($invocant, $name) = @_;
$name = trim($name);
$name || ThrowUserError('classification_not_specified');
if (length($name) > MAX_CLASSIFICATION_SIZE) {
ThrowUserError('classification_name_too_long', {'name' => $name});
}
my $classification = new Bugzilla::Classification({name => $name});
if ($classification && (!ref $invocant || $classification->id != $invocant->id)) {
ThrowUserError("classification_already_exists", { name => $classification->name });
}
return $name;
}
sub _check_description {
my ($invocant, $description) = @_;
$description = trim($description || '');
return $description;
}
sub _check_sortkey {
my ($invocant, $sortkey) = @_;
$sortkey ||= 0;
my $stored_sortkey = $sortkey;
if (!detaint_natural($sortkey) || $sortkey > MAX_SMALLINT) {
ThrowUserError('classification_invalid_sortkey', { 'sortkey' => $stored_sortkey });
}
return $sortkey;
}
###############################
#### Methods ####
###############################
sub set_name { $_[0]->set('name', $_[1]); }
sub set_description { $_[0]->set('description', $_[1]); }
sub set_sortkey { $_[0]->set('sortkey', $_[1]); }
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);
$self->{'products'} = Bugzilla::Product->new_from_list($product_ids);
}
return $self->{'products'};
}
###############################
#### Accessors ####
###############################
sub description { return $_[0]->{'description'}; }
sub sortkey { return $_[0]->{'sortkey'}; }
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 $sortkey = $classification->sortkey;
my $product_count = $classification->product_count;
my $products = $classification->products;
=head1 DESCRIPTION
Classification.pm represents a classification object. It is an
implementation of L<Bugzilla::Object>, and thus provides all methods
that L<Bugzilla::Object> provides.
The methods that are specific to C<Bugzilla::Classification> are listed
below.
A Classification is a higher-level grouping of Products.
=head1 METHODS
=over
=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
=cut

View File

@@ -1,647 +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>
# Frédéric Buclin <LpSolit@gmail.com>
# Max Kanat-Alexander <mkanat@bugzilla.org>
# Akamai Technologies <bugzilla-dev@akamai.com>
use strict;
package Bugzilla::Component;
use base qw(Bugzilla::Object);
use Bugzilla::Constants;
use Bugzilla::Util;
use Bugzilla::Error;
use Bugzilla::User;
use Bugzilla::FlagType;
use Bugzilla::Series;
###############################
#### Initialization ####
###############################
use constant DB_TABLE => 'components';
use constant DB_COLUMNS => qw(
id
name
product_id
initialowner
initialqacontact
description
);
use constant REQUIRED_CREATE_FIELDS => qw(
name
product
initialowner
description
);
use constant UPDATE_COLUMNS => qw(
name
initialowner
initialqacontact
description
);
use constant VALIDATORS => {
product => \&_check_product,
initialowner => \&_check_initialowner,
initialqacontact => \&_check_initialqacontact,
description => \&_check_description,
initial_cc => \&_check_cc_list,
};
use constant UPDATE_VALIDATORS => {
name => \&_check_name,
};
###############################
sub new {
my $class = shift;
my $param = shift;
my $dbh = Bugzilla->dbh;
my $product;
if (ref $param) {
$product = $param->{product};
my $name = $param->{name};
if (!defined $product) {
ThrowCodeError('bad_arg',
{argument => 'product',
function => "${class}::new"});
}
if (!defined $name) {
ThrowCodeError('bad_arg',
{argument => 'name',
function => "${class}::new"});
}
my $condition = 'product_id = ? AND name = ?';
my @values = ($product->id, $name);
$param = { condition => $condition, values => \@values };
}
unshift @_, $param;
my $component = $class->SUPER::new(@_);
# Add the product object as attribute only if the component exists.
$component->{product} = $product if ($component && $product);
return $component;
}
sub create {
my $class = shift;
my $dbh = Bugzilla->dbh;
$dbh->bz_start_transaction();
$class->check_required_create_fields(@_);
my $params = $class->run_create_validators(@_);
my $cc_list = delete $params->{initial_cc};
my $component = $class->insert_create_data($params);
# We still have to fill the component_cc table.
$component->_update_cc_list($cc_list);
# Create series for the new component.
$component->_create_series();
$dbh->bz_commit_transaction();
return $component;
}
sub run_create_validators {
my $class = shift;
my $params = $class->SUPER::run_create_validators(@_);
my $product = delete $params->{product};
$params->{product_id} = $product->id;
$params->{name} = $class->_check_name($params->{name}, $product);
return $params;
}
sub update {
my $self = shift;
my $changes = $self->SUPER::update(@_);
# Update the component_cc table if necessary.
if (defined $self->{cc_ids}) {
my $diff = $self->_update_cc_list($self->{cc_ids});
$changes->{cc_list} = $diff if defined $diff;
}
return $changes;
}
sub remove_from_db {
my $self = shift;
my $dbh = Bugzilla->dbh;
$dbh->bz_start_transaction();
if ($self->bug_count) {
if (Bugzilla->params->{'allowbugdeletion'}) {
require Bugzilla::Bug;
foreach my $bug_id (@{$self->bug_ids}) {
# Note: We allow admins to delete bugs even if they can't
# see them, as long as they can see the product.
my $bug = new Bugzilla::Bug($bug_id);
$bug->remove_from_db();
}
} else {
ThrowUserError('component_has_bugs', {nb => $self->bug_count});
}
}
$dbh->do('DELETE FROM flaginclusions WHERE component_id = ?',
undef, $self->id);
$dbh->do('DELETE FROM flagexclusions WHERE component_id = ?',
undef, $self->id);
$dbh->do('DELETE FROM component_cc WHERE component_id = ?',
undef, $self->id);
$dbh->do('DELETE FROM components WHERE id = ?', undef, $self->id);
$dbh->bz_commit_transaction();
}
################################
# Validators
################################
sub _check_name {
my ($invocant, $name, $product) = @_;
$name = trim($name);
$name || ThrowUserError('component_blank_name');
if (length($name) > MAX_COMPONENT_SIZE) {
ThrowUserError('component_name_too_long', {'name' => $name});
}
$product = $invocant->product if (ref $invocant);
my $component = new Bugzilla::Component({product => $product, name => $name});
if ($component && (!ref $invocant || $component->id != $invocant->id)) {
ThrowUserError('component_already_exists', { name => $component->name,
product => $product });
}
return $name;
}
sub _check_description {
my ($invocant, $description) = @_;
$description = trim($description);
$description || ThrowUserError('component_blank_description');
return $description;
}
sub _check_initialowner {
my ($invocant, $owner) = @_;
$owner || ThrowUserError('component_need_initialowner');
my $owner_id = Bugzilla::User->check($owner)->id;
return $owner_id;
}
sub _check_initialqacontact {
my ($invocant, $qa_contact) = @_;
my $qa_contact_id;
if (Bugzilla->params->{'useqacontact'}) {
$qa_contact_id = Bugzilla::User->check($qa_contact)->id if $qa_contact;
}
elsif (ref $invocant) {
$qa_contact_id = $invocant->{initialqacontact};
}
return $qa_contact_id;
}
sub _check_product {
my ($invocant, $product) = @_;
return Bugzilla->user->check_can_admin_product($product->name);
}
sub _check_cc_list {
my ($invocant, $cc_list) = @_;
my %cc_ids;
foreach my $cc (@$cc_list) {
my $id = login_to_id($cc, THROW_ERROR);
$cc_ids{$id} = 1;
}
return [keys %cc_ids];
}
###############################
#### Methods ####
###############################
sub _update_cc_list {
my ($self, $cc_list) = @_;
my $dbh = Bugzilla->dbh;
my $old_cc_list =
$dbh->selectcol_arrayref('SELECT user_id FROM component_cc
WHERE component_id = ?', undef, $self->id);
my ($removed, $added) = diff_arrays($old_cc_list, $cc_list);
my $diff;
if (scalar @$removed || scalar @$added) {
$diff = [join(', ', @$removed), join(', ', @$added)];
}
$dbh->do('DELETE FROM component_cc WHERE component_id = ?', undef, $self->id);
my $sth = $dbh->prepare('INSERT INTO component_cc
(user_id, component_id) VALUES (?, ?)');
$sth->execute($_, $self->id) foreach (@$cc_list);
return $diff;
}
sub _create_series {
my $self = shift;
# Insert default charting queries for this product.
# If they aren't using charting, this won't do any harm.
my $prodcomp = "&product=" . url_quote($self->product->name) .
"&component=" . url_quote($self->name);
my $open_query = 'field0-0-0=resolution&type0-0-0=notregexp&value0-0-0=.' .
$prodcomp;
my $nonopen_query = 'field0-0-0=resolution&type0-0-0=regexp&value0-0-0=.' .
$prodcomp;
my @series = ([get_text('series_all_open'), $open_query],
[get_text('series_all_closed'), $nonopen_query]);
foreach my $sdata (@series) {
my $series = new Bugzilla::Series(undef, $self->product->name,
$self->name, $sdata->[0],
Bugzilla->user->id, 1, $sdata->[1], 1);
$series->writeToDatabase();
}
}
sub set_name { $_[0]->set('name', $_[1]); }
sub set_description { $_[0]->set('description', $_[1]); }
sub set_default_assignee {
my ($self, $owner) = @_;
$self->set('initialowner', $owner);
# Reset the default owner object.
delete $self->{default_assignee};
}
sub set_default_qa_contact {
my ($self, $qa_contact) = @_;
$self->set('initialqacontact', $qa_contact);
# Reset the default QA contact object.
delete $self->{default_qa_contact};
}
sub set_cc_list {
my ($self, $cc_list) = @_;
$self->{cc_ids} = $self->_check_cc_list($cc_list);
# Reset the list of CC user objects.
delete $self->{initial_cc};
}
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'};
}
sub flag_types {
my $self = shift;
if (!defined $self->{'flag_types'}) {
$self->{'flag_types'} = {};
$self->{'flag_types'}->{'bug'} =
Bugzilla::FlagType::match({ 'target_type' => 'bug',
'product_id' => $self->product_id,
'component_id' => $self->id });
$self->{'flag_types'}->{'attachment'} =
Bugzilla::FlagType::match({ 'target_type' => 'attachment',
'product_id' => $self->product_id,
'component_id' => $self->id });
}
return $self->{'flag_types'};
}
sub initial_cc {
my $self = shift;
my $dbh = Bugzilla->dbh;
if (!defined $self->{'initial_cc'}) {
# If set_cc_list() has been called but data are not yet written
# into the DB, we want the new values defined by it.
my $cc_ids = $self->{cc_ids}
|| $dbh->selectcol_arrayref('SELECT user_id FROM component_cc
WHERE component_id = ?',
undef, $self->id);
$self->{'initial_cc'} = Bugzilla::User->new_from_list($cc_ids);
}
return $self->{'initial_cc'};
}
sub product {
my $self = shift;
if (!defined $self->{'product'}) {
require Bugzilla::Product; # We cannot |use| it.
$self->{'product'} = new Bugzilla::Product($self->product_id);
}
return $self->{'product'};
}
###############################
#### Accessors ####
###############################
sub id { return $_[0]->{'id'}; }
sub name { return $_[0]->{'name'}; }
sub description { return $_[0]->{'description'}; }
sub product_id { return $_[0]->{'product_id'}; }
###############################
#### Subroutines ####
###############################
1;
__END__
=head1 NAME
Bugzilla::Component - Bugzilla product component class.
=head1 SYNOPSIS
use Bugzilla::Component;
my $component = new Bugzilla::Component($comp_id);
my $component = new Bugzilla::Component({ product => $product, name => $name });
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 $initial_cc = $component->initial_cc;
my $product = $component->product;
my $bug_flag_types = $component->flag_types->{'bug'};
my $attach_flag_types = $component->flag_types->{'attachment'};
my $component = Bugzilla::Component->check({ product => $product, name => $name });
my $component =
Bugzilla::Component->create({ name => $name,
product => $product,
initialowner => $user_login1,
initialqacontact => $user_login2,
description => $description});
$component->set_name($new_name);
$component->set_description($new_description);
$component->set_default_assignee($new_login_name);
$component->set_default_qa_contact($new_login_name);
$component->set_cc_list(\@new_login_names);
$component->update();
$component->remove_from_db;
=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
object the component belongs to 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 the 'name'
and 'product' keys, then the value of the name
key is the name of a component being in the given
product.
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.
=item C<initial_cc>
Description: Returns a list of user objects representing users being
in the initial CC list.
Params: none.
Returns: An arrayref of L<Bugzilla::User> objects.
=item C<flag_types()>
Description: Returns all bug and attachment flagtypes available for
the component.
Params: none.
Returns: Two references to an array of flagtype objects.
=item C<product()>
Description: Returns the product the component belongs to.
Params: none.
Returns: A Bugzilla::Product object.
=item C<set_name($new_name)>
Description: Changes the name of the component.
Params: $new_name - new name of the component (string). This name
must be unique within the product.
Returns: Nothing.
=item C<set_description($new_desc)>
Description: Changes the description of the component.
Params: $new_desc - new description of the component (string).
Returns: Nothing.
=item C<set_default_assignee($new_assignee)>
Description: Changes the default assignee of the component.
Params: $new_owner - login name of the new default assignee of
the component (string). This user account
must already exist.
Returns: Nothing.
=item C<set_default_qa_contact($new_qa_contact)>
Description: Changes the default QA contact of the component.
Params: $new_qa_contact - login name of the new QA contact of
the component (string). This user
account must already exist.
Returns: Nothing.
=item C<set_cc_list(\@cc_list)>
Description: Changes the list of users being in the CC list by default.
Params: \@cc_list - list of login names (string). All the user
accounts must already exist.
Returns: Nothing.
=item C<update()>
Description: Write changes made to the component into the DB.
Params: none.
Returns: A hashref with changes made to the component object.
=item C<remove_from_db()>
Description: Deletes the current component from the DB. The object itself
is not destroyed.
Params: none.
Returns: Nothing.
=back
=head1 CLASS METHODS
=over
=item C<create(\%params)>
Description: Create a new component for the given product.
Params: The hashref must have the following keys:
name - name of the new component (string). This name
must be unique within the product.
product - a Bugzilla::Product object to which
the Component is being added.
description - description of the new component (string).
initialowner - login name of the default assignee (string).
The following keys are optional:
initiaqacontact - login name of the default QA contact (string),
or an empty string to clear it.
initial_cc - an arrayref of login names to add to the
CC list by default.
Returns: A Bugzilla::Component object.
=back
=cut

View File

@@ -1,412 +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>
package Bugzilla::Config;
use strict;
use base qw(Exporter);
use Bugzilla::Constants;
use Bugzilla::Hook;
use Data::Dumper;
use File::Temp;
# 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
%Bugzilla::Config::EXPORT_TAGS =
(
admin => [qw(update_params SetParam write_params)],
);
Exporter::export_ok_tags('admin');
use vars qw(@param_list);
# INITIALISATION CODE
# Perl throws a warning if we use bz_locations() directly after do.
our %params;
# Load in the param definitions
sub _load_params {
my $panels = param_panels();
my %hook_panels;
foreach my $panel (keys %$panels) {
my $module = $panels->{$panel};
eval("require $module") || die $@;
my @new_param_list = $module->get_param_list();
$hook_panels{lc($panel)} = { params => \@new_param_list };
foreach my $item (@new_param_list) {
$params{$item->{'name'}} = $item;
}
push(@param_list, @new_param_list);
}
# This hook is also called in editparams.cgi. This call here is required
# to make SetParam work.
Bugzilla::Hook::process('config-modify_panels',
{ panels => \%hook_panels });
}
# END INIT CODE
# Subroutines go here
sub param_panels {
my $param_panels = {};
my $libpath = bz_locations()->{'libpath'};
foreach my $item ((glob "$libpath/Bugzilla/Config/*.pm")) {
$item =~ m#/([^/]+)\.pm$#;
my $module = $1;
$param_panels->{$module} = "Bugzilla::Config::$module" unless $module eq 'Common';
}
# Now check for any hooked params
Bugzilla::Hook::process('config-add_panels',
{ panel_modules => $param_panels });
return $param_panels;
}
sub SetParam {
my ($name, $value) = @_;
_load_params unless %params;
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 '';
}
Bugzilla->params->{$name} = $value;
}
sub update_params {
my ($params) = @_;
my $answer = Bugzilla->installation_answers;
my $param = read_param_file();
# If we didn't return any param values, then this is a new installation.
my $new_install = !(keys %$param);
# --- UPDATE OLD PARAMS ---
# 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'};
}
# Old mail_delivery_method choices contained no uppercase characters
if (exists $param->{'mail_delivery_method'}
&& $param->{'mail_delivery_method'} !~ /[A-Z]/) {
my $method = $param->{'mail_delivery_method'};
my %translation = (
'sendmail' => 'Sendmail',
'smtp' => 'SMTP',
'qmail' => 'Qmail',
'testfile' => 'Test',
'none' => 'None');
$param->{'mail_delivery_method'} = $translation{$method};
}
# --- DEFAULTS FOR NEW PARAMS ---
_load_params unless %params;
foreach my $item (@param_list) {
my $name = $item->{'name'};
unless (exists $param->{$name}) {
print "New parameter: $name\n" unless $new_install;
$param->{$name} = $answer->{$name} || $item->{'default'};
}
}
$param->{'utf8'} = 1 if $new_install;
# --- REMOVE OLD PARAMS ---
my %oldparams;
# Remove any old params, put them in old-params.txt
foreach my $item (keys %$param) {
if (!grep($_ eq $item, map ($_->{'name'}, @param_list))) {
$oldparams{$item} = $param->{$item};
delete $param->{$item};
}
}
if (scalar(keys %oldparams)) {
my $op_file = new IO::File('old-params.txt', '>>', 0600)
|| die "old-params.txt: $!";
print "The following parameters are no longer used in Bugzilla,",
" and so have been\nmoved from your parameters file into",
" old-params.txt:\n";
local $Data::Dumper::Terse = 1;
local $Data::Dumper::Indent = 0;
my $comma = "";
foreach my $item (keys %oldparams) {
print $op_file "\n\n$item:\n" . Data::Dumper->Dump([$oldparams{$item}]) . "\n";
print "${comma}$item";
$comma = ", ";
}
print "\n";
$op_file->close;
}
if (ON_WINDOWS && !-e SENDMAIL_EXE
&& $param->{'mail_delivery_method'} eq 'Sendmail')
{
my $smtp = $answer->{'SMTP_SERVER'};
if (!$smtp) {
print "\nBugzilla requires an SMTP server to function on",
" Windows.\nPlease enter your SMTP server's hostname: ";
$smtp = <STDIN>;
chomp $smtp;
if ($smtp) {
$param->{'smtpserver'} = $smtp;
}
else {
print "\nWarning: No SMTP Server provided, defaulting to",
" localhost\n";
}
}
$param->{'mail_delivery_method'} = 'SMTP';
}
write_params($param);
# Return deleted params and values so that checksetup.pl has a chance
# to convert old params to new data.
return %oldparams;
}
sub write_params {
my ($param_data) = @_;
$param_data ||= Bugzilla->params;
my $datadir = bz_locations()->{'datadir'};
my $param_file = "$datadir/params";
local $Data::Dumper::Sortkeys = 1;
my ($fh, $tmpname) = File::Temp::tempfile('params.XXXXX',
DIR => $datadir );
print $fh (Data::Dumper->Dump([$param_data], ['*param']))
|| die "Can't write param file: $!";
close $fh;
rename $tmpname, $param_file
or die "Can't rename $tmpname to $param_file: $!";
ChmodDataFile($param_file, 0666);
# And now we have to reset the params cache so that Bugzilla will re-read
# them.
delete Bugzilla->request_cache->{params};
}
# Some files in the data directory must be world readable if and only if
# we don't have a webserver group. Call this function to do this.
# This will become a private function once all the datafile handling stuff
# moves into this package
# This sub is not perldoc'd for that reason - noone should know about it
sub ChmodDataFile {
my ($file, $mask) = @_;
my $perm = 0770;
if ((stat(bz_locations()->{'datadir'}))[2] & 0002) {
$perm = 0777;
}
$perm = $perm & $mask;
chmod $perm,$file;
}
sub read_param_file {
my %params;
my $datadir = bz_locations()->{'datadir'};
if (-e "$datadir/params") {
# 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
%params = %{$s->varglob('param')};
}
elsif ($ENV{'SERVER_SOFTWARE'}) {
# We're in a CGI, but the params file doesn't exist. We can't
# Template Toolkit, or even install_string, since checksetup
# might not have thrown an error. Bugzilla::CGI->new
# hasn't even been called yet, so we manually use CGI::Carp here
# so that the user sees the error.
require CGI::Carp;
CGI::Carp->import('fatalsToBrowser');
die "The $datadir/params file does not exist."
. ' You probably need to run checksetup.pl.',
}
return \%params;
}
1;
__END__
=head1 NAME
Bugzilla::Config - Configuration parameters for Bugzilla
=head1 SYNOPSIS
# Administration functions
use Bugzilla::Config qw(:admin);
update_params();
SetParam($param, $value);
write_params();
=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<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<update_params()>
Updates the parameters, by transitioning old params to new formats, setting
defaults for new params, and removing obsolete ones. Used by F<checksetup.pl>
in the process of an installation or upgrade.
Prints out information about what it's doing, if it makes any changes.
May prompt the user for input, if certain required parameters are not
specified.
=item C<write_params($params)>
Description: Writes the parameters to disk.
Params: C<$params> (optional) - A hashref to write to the disk
instead of C<Bugzilla->params>. Used only by
C<update_params>.
Returns: nothing
=item C<read_param_file()>
Description: Most callers should never need this. This is used
by C<Bugzilla->params> to directly read C<$datadir/params>
and load it into memory. Use C<Bugzilla->params> instead.
Params: none
Returns: A hashref containing the current params in C<$datadir/params>.
=back

View File

@@ -1,63 +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 => 1
},
{
name => 'allowuserdeletion',
type => 'b',
default => 0
});
return @param_list;
}
1;

View File

@@ -1,98 +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 => 'attachment_base',
type => 't',
default => '',
checker => \&check_urlbase
},
{
name => 'allow_attachment_deletion',
type => 'b',
default => 0
},
{
name => 'allow_attach_url',
type => 'b',
default => 0
},
{
name => 'maxattachmentsize',
type => 't',
default => '1000',
checker => \&check_maxattachmentsize
},
# 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;

View File

@@ -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 => 'o',
choices => [ 'DB', 'RADIUS', 'LDAP' ],
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 => '0',
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;

View File

@@ -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::BugChange;
use strict;
use Bugzilla::Config::Common;
use Bugzilla::Status;
$Bugzilla::Config::BugChange::sortkey = "03";
sub get_param_list {
my $class = shift;
# Hardcoded bug statuses which existed before Bugzilla 3.1.
my @closed_bug_statuses = ('RESOLVED', 'VERIFIED', 'CLOSED');
# If we are upgrading from 3.0 or older, bug statuses are not customisable
# and bug_status.is_open is not yet defined (hence the eval), so we use
# the bug statuses above as they are still hardcoded.
eval {
my @current_closed_states = map {$_->name} closed_bug_statuses();
# If no closed state was found, use the default list above.
@closed_bug_statuses = @current_closed_states if scalar(@current_closed_states);
};
my @param_list = (
{
name => 'duplicate_or_move_bug_status',
type => 's',
choices => \@closed_bug_statuses,
default => $closed_bug_statuses[0],
checker => \&check_bug_status
},
{
name => 'letsubmitterchoosepriority',
type => 'b',
default => 1
},
{
name => 'letsubmitterchoosemilestone',
type => 'b',
default => 1
},
{
name => 'musthavemilestoneonaccept',
type => 'b',
default => 0
},
{
name => 'commentonchange_resolution',
type => 'b',
default => 0
},
{
name => 'commentonduplicate',
type => 'b',
default => 0
},
{
name => 'noresolveonopenblockers',
type => 'b',
default => 0,
} );
return @param_list;
}
1;

View File

@@ -1,126 +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;
use Bugzilla::Field;
$Bugzilla::Config::BugFields::sortkey = "04";
sub get_param_list {
my $class = shift;
my @legal_priorities = @{get_legal_field_values('priority')};
my @legal_severities = @{get_legal_field_values('bug_severity')};
my @legal_platforms = @{get_legal_field_values('rep_platform')};
my @legal_OS = @{get_legal_field_values('op_sys')};
my @param_list = (
{
name => 'useclassification',
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 => 0
},
{
name => 'usebugaliases',
type => 'b',
default => 0
},
{
name => 'use_see_also',
type => 'b',
default => 1
},
{
name => 'defaultpriority',
type => 's',
choices => \@legal_priorities,
default => $legal_priorities[-1],
checker => \&check_priority
},
{
name => 'defaultseverity',
type => 's',
choices => \@legal_severities,
default => $legal_severities[-1],
checker => \&check_severity
},
{
name => 'defaultplatform',
type => 's',
choices => ['', @legal_platforms],
default => '',
checker => \&check_platform
},
{
name => 'defaultopsys',
type => 's',
choices => ['', @legal_OS],
default => '',
checker => \&check_opsys
} );
return @param_list;
}
1;

View File

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

View File

@@ -1,462 +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>
# Marc Schumann <wurblzap@gmail.com>
#
package Bugzilla::Config::Common;
use strict;
use Socket;
use Bugzilla::Util;
use Bugzilla::Constants;
use Bugzilla::Field;
use Bugzilla::Group;
use Bugzilla::Status;
use base qw(Exporter);
@Bugzilla::Config::Common::EXPORT =
qw(check_multi check_numeric check_regexp check_url check_group
check_sslbase check_priority check_severity check_platform
check_opsys check_shadowdb check_urlbase check_webdotbase
check_netmask check_user_verify_class check_image_converter
check_mail_delivery_method check_notification check_utf8
check_bug_status check_smtp_auth check_theschwartz_available
check_maxattachmentsize
);
# 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' || $param->{'type'} eq 'o') {
foreach my $chkParam (split(',', $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;
# Fall back to port 443 if for some reason getservbyname() fails.
my $port = getservbyname('https', 'tcp') || 443;
if ($host =~ /^(.+):(\d+)$/) {
$host = $1;
$port = $2;
}
local *SOCK;
my $proto = getprotobyname('tcp');
socket(SOCK, PF_INET, SOCK_STREAM, $proto);
my $iaddr = inet_aton($host) || return "The host $host cannot be resolved";
my $sin = sockaddr_in($port, $iaddr);
if (!connect(SOCK, $sin)) {
return "Failed to connect to $host:$port; unable to enable SSL";
}
close(SOCK);
}
return "";
}
sub check_utf8 {
my $utf8 = shift;
# You cannot turn off the UTF-8 parameter if you've already converted
# your tables to utf-8.
my $dbh = Bugzilla->dbh;
if ($dbh->isa('Bugzilla::DB::Mysql') && $dbh->bz_db_is_utf8 && !$utf8) {
return "You cannot disable UTF-8 support, because your MySQL database"
. " is encoded in UTF-8";
}
return "";
}
sub check_priority {
my ($value) = (@_);
my $legal_priorities = get_legal_field_values('priority');
if (lsearch($legal_priorities, $value) < 0) {
return "Must be a legal priority value: one of " .
join(", ", @$legal_priorities);
}
return "";
}
sub check_severity {
my ($value) = (@_);
my $legal_severities = get_legal_field_values('bug_severity');
if (lsearch($legal_severities, $value) < 0) {
return "Must be a legal severity value: one of " .
join(", ", @$legal_severities);
}
return "";
}
sub check_platform {
my ($value) = (@_);
my $legal_platforms = get_legal_field_values('rep_platform');
if (lsearch(['', @$legal_platforms], $value) < 0) {
return "Must be empty or a legal platform value: one of " .
join(", ", @$legal_platforms);
}
return "";
}
sub check_opsys {
my ($value) = (@_);
my $legal_OS = get_legal_field_values('op_sys');
if (lsearch(['', @$legal_OS], $value) < 0) {
return "Must be empty or a legal operating system value: one of " .
join(", ", @$legal_OS);
}
return "";
}
sub check_bug_status {
my $bug_status = shift;
my @closed_bug_statuses = map {$_->name} closed_bug_statuses();
if (lsearch(\@closed_bug_statuses, $bug_status) < 0) {
return "Must be a valid closed status: one of " . join(', ', @closed_bug_statuses);
}
return "";
}
sub check_group {
my $group_name = shift;
return "" unless $group_name;
my $group = new Bugzilla::Group({'name' => $group_name});
unless (defined $group) {
return "Must be an existing group name";
}
return "";
}
sub check_shadowdb {
my ($value) = (@_);
$value = trim($value);
if ($value eq "") {
return "";
}
if (!Bugzilla->params->{'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_url {
my ($url) = (@_);
return '' if $url eq ''; # Allow empty URLs
if ($url !~ m:/$:) {
return 'must be a legal URL, absolute or relative, ending 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
my $webdotdir = bz_locations()->{'webdotdir'};
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) = @_;
$list || return 'You need to specify at least one authentication mechanism';
for my $class (split /,\s*/, $list) {
my $res = check_multi($class, $entry);
return $res if $res;
if ($class eq 'RADIUS') {
eval "require Authen::Radius";
return "Error requiring Authen::Radius: '$@'" if $@;
return "RADIUS servername (RADIUS_server) is missing" unless Bugzilla->params->{"RADIUS_server"};
return "RADIUS_secret is empty" unless Bugzilla->params->{"RADIUS_secret"};
}
elsif ($class eq 'LDAP') {
eval "require Net::LDAP";
return "Error requiring Net::LDAP: '$@'" if $@;
return "LDAP servername (LDAPserver) is missing" unless Bugzilla->params->{"LDAPserver"};
return "LDAPBaseDN is empty" unless Bugzilla->params->{"LDAPBaseDN"};
}
}
return "";
}
sub check_image_converter {
my ($value, $hash) = @_;
if ($value == 1){
eval "require Image::Magick";
return "Error requiring Image::Magick: '$@'" if $@;
}
return "";
}
sub check_mail_delivery_method {
my $check = check_multi(@_);
return $check if $check;
my $mailer = shift;
if ($mailer eq 'sendmail' && $^O =~ /MSWin32/i) {
# look for sendmail.exe
return "Failed to locate " . SENDMAIL_EXE
unless -e SENDMAIL_EXE;
}
return "";
}
sub check_maxattachmentsize {
my $check = check_numeric(@_);
return $check if $check;
my $size = shift;
my $dbh = Bugzilla->dbh;
if ($dbh->isa('Bugzilla::DB::Mysql')) {
my (undef, $max_packet) = $dbh->selectrow_array(
q{SHOW VARIABLES LIKE 'max\_allowed\_packet'});
my $byte_size = $size * 1024;
if ($max_packet < $byte_size) {
return "You asked for a maxattachmentsize of $byte_size bytes,"
. " but the max_allowed_packet setting in MySQL currently"
. " only allows packets up to $max_packet bytes";
}
}
return "";
}
sub check_notification {
my $option = shift;
my @current_version =
(BUGZILLA_VERSION =~ m/^(\d+)\.(\d+)(?:(rc|\.)(\d+))?\+?$/);
if ($current_version[1] % 2 && $option eq 'stable_branch_release') {
return "You are currently running a development snapshot, and so your " .
"installation is not based on a branch. If you want to be notified " .
"about the next stable release, you should select " .
"'latest_stable_release' instead";
}
return "";
}
sub check_smtp_auth {
my $username = shift;
if ($username) {
eval "require Authen::SASL";
return "Error requiring Authen::SASL: '$@'" if $@;
}
return "";
}
sub check_theschwartz_available {
if (!eval { require TheSchwartz; require Daemon::Generic; }) {
return "Using the job queue requires that you have certain Perl"
. " modules installed. See the output of checksetup.pl"
. " for more information";
}
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)
# p -- A short text entry field (as with type = 't'), but the string is
# replaced by asterisks (appropriate for passwords)
# 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 a string
# with a list of selected values (as a comma-separated list), 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.
#
# o -- A list of values, orderable, and with many selectable (shows up as a
# JavaScript-enhanced select box if JavaScript is enabled, and a text
# entry field if not)
# Set up in the same way as type m.
#
# 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 types C<s>, C<o> or 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

View File

@@ -1,126 +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 => 'docs_urlbase',
type => 't',
default => 'docs/%lang%/html/',
checker => \&check_url
},
{
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 => 'utf8',
type => 'b',
default => '0',
checker => \&check_utf8
},
{
name => 'shutdownhtml',
type => 'l',
default => ''
},
{
name => 'announcehtml',
type => 'l',
default => ''
},
{
name => 'proxy_url',
type => 't',
default => ''
},
{
name => 'upgrade_notification',
type => 's',
choices => ['development_snapshot', 'latest_stable_release',
'stable_branch_release', 'disabled'],
default => 'latest_stable_release',
checker => \&check_notification
} );
return @param_list;
}
1;

View File

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

View File

@@ -1,108 +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;
use Bugzilla::Group;
$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 => 's',
choices => \&_get_all_group_names,
default => 'editbugs',
checker => \&check_group
},
{
name => 'insidergroup',
type => 's',
choices => \&_get_all_group_names,
default => '',
checker => \&check_group
},
{
name => 'timetrackinggroup',
type => 's',
choices => \&_get_all_group_names,
default => 'editbugs',
checker => \&check_group
},
{
name => 'querysharegroup',
type => 's',
choices => \&_get_all_group_names,
default => 'editbugs',
checker => \&check_group
},
{
name => 'usevisibilitygroups',
type => 'b',
default => 0
},
{
name => 'strict_isolation',
type => 'b',
default => 0
} );
return @param_list;
}
sub _get_all_group_names {
my @group_names = map {$_->name} Bugzilla::Group->get_all;
unshift(@group_names, '');
return \@group_names;
}
1;

View File

@@ -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::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 => 'LDAPstarttls',
type => 'b',
default => 0
},
{
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;

View File

@@ -1,108 +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;
use Email::Send;
$Bugzilla::Config::MTA::sortkey = "10";
sub get_param_list {
my $class = shift;
my @param_list = (
{
name => 'mail_delivery_method',
type => 's',
# Bugzilla is not ready yet to send mails to newsgroups, and 'IO'
# is of no use for now as we already have our own 'Test' mode.
choices => [grep {$_ ne 'NNTP' && $_ ne 'IO'} Email::Send->new()->all_mailers(), 'None'],
default => 'Sendmail',
checker => \&check_mail_delivery_method
},
{
name => 'mailfrom',
type => 't',
default => 'bugzilla-daemon'
},
{
name => 'use_mailer_queue',
type => 'b',
default => 0,
checker => \&check_theschwartz_available,
},
{
name => 'sendmailnow',
type => 'b',
default => 1
},
{
name => 'smtpserver',
type => 't',
default => 'localhost'
},
{
name => 'smtp_username',
type => 't',
default => '',
checker => \&check_smtp_auth
},
{
name => 'smtp_password',
type => 'p',
default => ''
},
{
name => 'smtp_debug',
type => 'b',
default => 0
},
{
name => 'whinedays',
type => 't',
default => 7,
checker => \&check_numeric
},
{
name => 'globalwatchers',
type => 't',
default => '',
}, );
return @param_list;
}
1;

View File

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

View File

@@ -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::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&amp;bug_status=NEW&amp;bug_status=ASSIGNED&amp;bug_status=REOPENED&amp;emailassigned_to1=1&amp;emailreporter1=1&amp;emailtype1=exact&amp;email1=%userid%&amp;field0-0-0=bug_status&amp;type0-0-0=notequals&amp;value0-0-0=UNCONFIRMED&amp;field0-0-1=reporter&amp;type0-0-1=equals&amp;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
},
{
name => 'specific_search_allow_empty_words',
type => 'b',
default => 1
}
);
return @param_list;
}
1;

View File

@@ -1,60 +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 Marc Schumann.
# Portions created by Marc Schumann are Copyright (c) 2007 Marc Schumann.
# All rights reserved.
#
# Contributor(s): Marc Schumann <wurblzap@gmail.com>
#
package Bugzilla::Config::RADIUS;
use strict;
use Bugzilla::Config::Common;
$Bugzilla::Config::RADIUS::sortkey = "09";
sub get_param_list {
my $class = shift;
my @param_list = (
{
name => 'RADIUS_server',
type => 't',
default => ''
},
{
name => 'RADIUS_secret',
type => 't',
default => ''
},
{
name => 'RADIUS_NAS_IP',
type => 't',
default => ''
},
{
name => 'RADIUS_email_suffix',
type => 't',
default => ''
},
);
return @param_list;
}
1;

View File

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

View File

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

View File

@@ -1,517 +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>
# Marc Schumann <wurblzap@gmail.com>
package Bugzilla::Constants;
use strict;
use base qw(Exporter);
# For bz_locations
use File::Basename;
@Bugzilla::Constants::EXPORT = qw(
BUGZILLA_VERSION
bz_locations
IS_NULL
NOT_NULL
CONTROLMAPNA
CONTROLMAPSHOWN
CONTROLMAPDEFAULT
CONTROLMAPMANDATORY
AUTH_OK
AUTH_NODATA
AUTH_ERROR
AUTH_LOGINFAILED
AUTH_DISABLED
AUTH_NO_SUCH_USER
USER_PASSWORD_MIN_LENGTH
USER_PASSWORD_MAX_LENGTH
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
MAX_COMMENT_LENGTH
CMT_NORMAL
CMT_DUPE_OF
CMT_HAS_DUPE
CMT_POPULAR_VOTES
CMT_MOVED_TO
THROW_ERROR
RELATIONSHIPS
REL_ASSIGNEE REL_QA REL_REPORTER REL_CC REL_VOTER REL_GLOBAL_WATCHER
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
EVT_BUG_CREATED
NEG_EVENTS
EVT_UNCONFIRMED EVT_CHANGED_BY_ME
GLOBAL_EVENTS
EVT_FLAG_REQUESTED EVT_REQUESTED_FLAG
FULLTEXT_BUGLIST_LIMIT
ADMIN_GROUP_NAME
PER_PRODUCT_PRIVILEGES
SENDMAIL_EXE
SENDMAIL_PATH
FIELD_TYPE_UNKNOWN
FIELD_TYPE_FREETEXT
FIELD_TYPE_SINGLE_SELECT
FIELD_TYPE_MULTI_SELECT
FIELD_TYPE_TEXTAREA
FIELD_TYPE_DATETIME
FIELD_TYPE_BUG_ID
FIELD_TYPE_BUG_URLS
USAGE_MODE_BROWSER
USAGE_MODE_CMDLINE
USAGE_MODE_WEBSERVICE
USAGE_MODE_EMAIL
ERROR_MODE_WEBPAGE
ERROR_MODE_DIE
ERROR_MODE_DIE_SOAP_FAULT
INSTALLATION_MODE_INTERACTIVE
INSTALLATION_MODE_NON_INTERACTIVE
DB_MODULE
ROOT_USER
ON_WINDOWS
MAX_TOKEN_AGE
MAX_LOGINCOOKIE_AGE
SAFE_PROTOCOLS
MIN_SMALLINT
MAX_SMALLINT
MAX_LEN_QUERY_NAME
MAX_CLASSIFICATION_SIZE
MAX_PRODUCT_SIZE
MAX_MILESTONE_SIZE
MAX_COMPONENT_SIZE
MAX_FIELD_VALUE_SIZE
MAX_FREETEXT_LENGTH
MAX_BUG_URL_LENGTH
PASSWORD_DIGEST_ALGORITHM
PASSWORD_SALT_LENGTH
);
@Bugzilla::Constants::EXPORT_OK = qw(contenttypes);
# CONSTANTS
#
# Bugzilla version
use constant BUGZILLA_VERSION => "3.4";
# These are unique values that are unlikely to match a string or a number,
# to be used in criteria for match() functions and other things. They start
# and end with spaces because most Bugzilla stuff has trim() called on it,
# so this is unlikely to match anything we get out of the DB.
#
# We can't use a reference, because Template Toolkit doesn't work with
# them properly (constants.IS_NULL => {} just returns an empty string instead
# of the reference).
use constant IS_NULL => ' __IS_NULL__ ';
use constant NOT_NULL => ' __NOT_NULL__ ';
#
# 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 AUTH_NO_SUCH_USER => 5;
# The minimum and maximum lengths a password must have.
use constant USER_PASSWORD_MIN_LENGTH => 3;
use constant USER_PASSWORD_MAX_LENGTH => 16;
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/csv" ,
"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_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 in _check_comment(). Gives the max length allowed for a comment.
use constant MAX_COMMENT_LENGTH => 65535;
# The type of bug comments.
use constant CMT_NORMAL => 0;
use constant CMT_DUPE_OF => 1;
use constant CMT_HAS_DUPE => 2;
use constant CMT_POPULAR_VOTES => 3;
use constant CMT_MOVED_TO => 4;
# Determine whether a validation routine should return 0 or throw
# an error when the validation fails.
use constant THROW_ERROR => 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 REL_GLOBAL_WATCHER => 5;
use constant RELATIONSHIPS => REL_ASSIGNEE, REL_QA, REL_REPORTER, REL_CC,
REL_VOTER, REL_GLOBAL_WATCHER;
# 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 EVT_BUG_CREATED => 10;
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, EVT_BUG_CREATED;
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';
# Privileges which can be per-product.
use constant PER_PRODUCT_PRIVILEGES => ('editcomponents', 'editbugs', 'canconfirm');
# Path to sendmail.exe (Windows only)
use constant SENDMAIL_EXE => '/usr/lib/sendmail.exe';
# Paths to search for the sendmail binary (non-Windows)
use constant SENDMAIL_PATH => '/usr/lib:/usr/sbin:/usr/ucblib';
# Field types. Match values in fielddefs.type column. These are purposely
# not named after database column types, since Bugzilla fields comprise not
# only storage but also logic. For example, we might add a "user" field type
# whose values are stored in an integer column in the database but for which
# we do more than we would do for a standard integer type (f.e. we might
# display a user picker).
use constant FIELD_TYPE_UNKNOWN => 0;
use constant FIELD_TYPE_FREETEXT => 1;
use constant FIELD_TYPE_SINGLE_SELECT => 2;
use constant FIELD_TYPE_MULTI_SELECT => 3;
use constant FIELD_TYPE_TEXTAREA => 4;
use constant FIELD_TYPE_DATETIME => 5;
use constant FIELD_TYPE_BUG_ID => 6;
use constant FIELD_TYPE_BUG_URLS => 7;
# The maximum number of days a token will remain valid.
use constant MAX_TOKEN_AGE => 3;
# How many days a logincookie will remain valid if not used.
use constant MAX_LOGINCOOKIE_AGE => 30;
# 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');
# Usage modes. Default USAGE_MODE_BROWSER. Use with Bugzilla->usage_mode.
use constant USAGE_MODE_BROWSER => 0;
use constant USAGE_MODE_CMDLINE => 1;
use constant USAGE_MODE_WEBSERVICE => 2;
use constant USAGE_MODE_EMAIL => 3;
# Error modes. Default set by Bugzilla->usage_mode (so ERROR_MODE_WEBPAGE
# usually). Use with Bugzilla->error_mode.
use constant ERROR_MODE_WEBPAGE => 0;
use constant ERROR_MODE_DIE => 1;
use constant ERROR_MODE_DIE_SOAP_FAULT => 2;
# The various modes that checksetup.pl can run in.
use constant INSTALLATION_MODE_INTERACTIVE => 0;
use constant INSTALLATION_MODE_NON_INTERACTIVE => 1;
# Data about what we require for different databases.
use constant DB_MODULE => {
'mysql' => {db => 'Bugzilla::DB::Mysql', db_version => '4.1.2',
dbd => {
package => 'DBD-mysql',
module => 'DBD::mysql',
# Disallow development versions
blacklist => ['_'],
# For UTF-8 support
version => '4.00',
},
name => 'MySQL'},
'pg' => {db => 'Bugzilla::DB::Pg', db_version => '8.00.0000',
dbd => {
package => 'DBD-Pg',
module => 'DBD::Pg',
version => '1.45',
},
name => 'PostgreSQL'},
'oracle'=> {db => 'Bugzilla::DB::Oracle', db_version => '10.02.0',
dbd => {
package => 'DBD-Oracle',
module => 'DBD::Oracle',
version => '1.19',
},
name => 'Oracle'},
};
# The user who should be considered "root" when we're giving
# instructions to Bugzilla administrators.
use constant ROOT_USER => $^O =~ /MSWin32/i ? 'Administrator' : 'root';
# True if we're on Win32.
use constant ON_WINDOWS => ($^O =~ /MSWin32/i);
use constant MIN_SMALLINT => -32768;
use constant MAX_SMALLINT => 32767;
# The longest that a saved search name can be.
use constant MAX_LEN_QUERY_NAME => 64;
# The longest classification name allowed.
use constant MAX_CLASSIFICATION_SIZE => 64;
# The longest product name allowed.
use constant MAX_PRODUCT_SIZE => 64;
# The longest milestone name allowed.
use constant MAX_MILESTONE_SIZE => 20;
# The longest component name allowed.
use constant MAX_COMPONENT_SIZE => 64;
# The maximum length for values of <select> fields.
use constant MAX_FIELD_VALUE_SIZE => 64;
# Maximum length allowed for free text fields.
use constant MAX_FREETEXT_LENGTH => 255;
# The longest a bug URL in a BUG_URLS field can be.
use constant MAX_BUG_URL_LENGTH => 255;
# This is the name of the algorithm used to hash passwords before storing
# them in the database. This can be any string that is valid to pass to
# Perl's "Digest" module. Note that if you change this, it won't take
# effect until a user changes his password.
use constant PASSWORD_DIGEST_ALGORITHM => 'SHA-256';
# How long of a salt should we use? Note that if you change this, none
# of your users will be able to log in until they reset their passwords.
use constant PASSWORD_SALT_LENGTH => 8;
sub bz_locations {
# We know that Bugzilla/Constants.pm must be in %INC at this point.
# So the only question is, what's the name of the directory
# above it? This is the most reliable way to get our current working
# directory under both mod_cgi and mod_perl. We call dirname twice
# to get the name of the directory above the "Bugzilla/" directory.
#
# Calling dirname twice like that won't work on VMS or AmigaOS
# but I doubt anybody runs Bugzilla on those.
#
# On mod_cgi this will be a relative path. On mod_perl it will be an
# absolute path.
my $libpath = dirname(dirname($INC{'Bugzilla/Constants.pm'}));
# We have to detaint $libpath, but we can't use Bugzilla::Util here.
$libpath =~ /(.*)/;
$libpath = $1;
my ($project, $localconfig, $datadir);
if ($ENV{'PROJECT'} && $ENV{'PROJECT'} =~ /^(\w+)$/) {
$project = $1;
$localconfig = "localconfig.$project";
$datadir = "data/$project";
} else {
$localconfig = "localconfig";
$datadir = "data";
}
# We have to return absolute paths for mod_perl.
# That means that if you modify these paths, they must be absolute paths.
return {
'libpath' => $libpath,
'ext_libpath' => "$libpath/lib",
# If you put the libraries in a different location than the CGIs,
# make sure this still points to the CGIs.
'cgi_path' => $libpath,
'templatedir' => "$libpath/template",
'project' => $project,
'localconfig' => "$libpath/$localconfig",
'datadir' => "$libpath/$datadir",
'attachdir' => "$libpath/$datadir/attachments",
'skinsdir' => "$libpath/skins",
# $webdotdir must be in the web server's tree somewhere. Even if you use a
# local dot, we output images to there. Also, if $webdotdir 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...
'webdotdir' => "$libpath/$datadir/webdot",
'extensionsdir' => "$libpath/extensions",
};
}
1;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,650 +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 Oracle Corporation.
# Portions created by Oracle are Copyright (C) 2007 Oracle Corporation.
# All Rights Reserved.
#
# Contributor(s): Lance Larsh <lance.larsh@oracle.com>
# Xiaoou Wu <xiaoou.wu@oracle.com>
# Max Kanat-Alexander <mkanat@bugzilla.org>
=head1 NAME
Bugzilla::DB::Oracle - Bugzilla database compatibility layer for Oracle
=head1 DESCRIPTION
This module overrides methods of the Bugzilla::DB module with Oracle
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::Oracle;
use strict;
use DBD::Oracle;
use DBD::Oracle qw(:ora_types);
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Util;
# This module extends the DB interface via inheritance
use base qw(Bugzilla::DB);
#####################################################################
# Constants
#####################################################################
use constant EMPTY_STRING => '__BZ_EMPTY_STR__';
use constant ISOLATION_LEVEL => 'READ COMMITTED';
use constant BLOB_TYPE => { ora_type => ORA_BLOB };
sub new {
my ($class, $user, $pass, $host, $dbname, $port) = @_;
# You can never connect to Oracle without a DB name,
# and there is no default DB.
$dbname ||= Bugzilla->localconfig->{db_name};
# Set the language enviroment
$ENV{'NLS_LANG'} = '.AL32UTF8' if Bugzilla->params->{'utf8'};
# construct the DSN from the parameters we got
my $dsn = "dbi:Oracle:host=$host;sid=$dbname";
$dsn .= ";port=$port" if $port;
my $attrs = { FetchHashKeyName => 'NAME_lc',
LongReadLen => ( Bugzilla->params->{'maxattachmentsize'}
|| 1000 ) * 1024,
};
my $self = $class->db_new($dsn, $user, $pass, $attrs);
# Needed by TheSchwartz
$self->{private_bz_dsn} = $dsn;
bless ($self, $class);
# Set the session's default date format to match MySQL
$self->do("ALTER SESSION SET NLS_DATE_FORMAT='YYYY-MM-DD HH24:MI:SS'");
$self->do("ALTER SESSION SET NLS_TIMESTAMP_FORMAT='YYYY-MM-DD HH24:MI:SS'");
$self->do("ALTER SESSION SET NLS_LENGTH_SEMANTICS='CHAR'")
if Bugzilla->params->{'utf8'};
# To allow case insensitive query.
$self->do("ALTER SESSION SET NLS_COMP='ANSI'");
$self->do("ALTER SESSION SET NLS_SORT='BINARY_AI'");
return $self;
}
sub bz_last_key {
my ($self, $table, $column) = @_;
my $seq = $table . "_" . $column . "_SEQ";
my ($last_insert_id) = $self->selectrow_array("SELECT $seq.CURRVAL "
. " FROM DUAL");
return $last_insert_id;
}
sub bz_check_regexp {
my ($self, $pattern) = @_;
eval { $self->do("SELECT 1 FROM DUAL WHERE "
. $self->sql_regexp($self->quote("a"), $pattern, 1)) };
$@ && ThrowUserError('illegal_regexp',
{ value => $pattern, dberror => $self->errstr });
}
sub bz_explain {
my ($self, $sql) = @_;
my $sth = $self->prepare("EXPLAIN PLAN FOR $sql");
$sth->execute();
my $explain = $self->selectcol_arrayref(
"SELECT PLAN_TABLE_OUTPUT FROM TABLE(DBMS_XPLAN.DISPLAY)");
return join("\n", @$explain);
}
sub sql_regexp {
my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_;
$real_pattern ||= $pattern;
$self->bz_check_regexp($real_pattern) if !$nocheck;
return "REGEXP_LIKE($expr, $pattern)";
}
sub sql_not_regexp {
my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_;
$real_pattern ||= $pattern;
$self->bz_check_regexp($real_pattern) if !$nocheck;
return "NOT REGEXP_LIKE($expr, $pattern)"
}
sub sql_limit {
my ($self, $limit, $offset) = @_;
if(defined $offset) {
return "/* LIMIT $limit $offset */";
}
return "/* LIMIT $limit */";
}
sub sql_string_concat {
my ($self, @params) = @_;
return 'CONCAT(' . join(', ', @params) . ')';
}
sub sql_string_until {
my ($self, $string, $substring) = @_;
return "SUBSTR($string, 1, "
. $self->sql_position($substring, $string)
. " - 1)";
}
sub sql_to_days {
my ($self, $date) = @_;
return " TO_CHAR(TO_DATE($date),'J') ";
}
sub sql_from_days{
my ($self, $date) = @_;
return " TO_DATE($date,'J') ";
}
sub sql_fulltext_search {
my ($self, $column, $text, $label) = @_;
$text = $self->quote($text);
trick_taint($text);
return "CONTAINS($column,$text,$label)", "SCORE($label)";
}
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) = @_;
if ($units =~ /YEAR|MONTH/i) {
return "NUMTOYMINTERVAL($interval,'$units')";
} else{
return "NUMTODSINTERVAL($interval,'$units')";
}
}
sub sql_position {
my ($self, $fragment, $text) = @_;
return "INSTR($text, $fragment)";
}
sub sql_in {
my ($self, $column_name, $in_list_ref) = @_;
my @in_list = @$in_list_ref;
return $self->SUPER::sql_in($column_name, $in_list_ref) if $#in_list < 1000;
my @in_str;
while (@in_list) {
my $length = $#in_list + 1;
my $splice = $length > 1000 ? 1000 : $length;
my @sub_in_list = splice(@in_list, 0, $splice);
push(@in_str,
$self->SUPER::sql_in($column_name, \@sub_in_list));
}
return "( " . join(" OR ", @in_str) . " )";
}
sub _bz_add_field_table {
my ($self, $name, $schema_ref, $type) = @_;
$self->SUPER::_bz_add_field_table($name, $schema_ref);
if (defined($type) && $type == FIELD_TYPE_MULTI_SELECT) {
my $uk_name = "UK_" . $self->_bz_schema->_hash_identifier($name . '_value');
$self->do("ALTER TABLE $name ADD CONSTRAINT $uk_name UNIQUE(value)");
}
}
sub bz_drop_table {
my ($self, $name) = @_;
my $table_exists = $self->bz_table_info($name);
if ($table_exists) {
$self->_bz_drop_fks($name);
$self->SUPER::bz_drop_table($name);
}
}
# Dropping all FKs for a specified table.
sub _bz_drop_fks {
my ($self, $table) = @_;
my @columns = $self->_bz_real_schema->get_table_columns($table);
foreach my $column (@columns) {
$self->bz_drop_fk($table, $column);
}
}
sub _fix_empty {
my ($string) = @_;
$string = '' if $string eq EMPTY_STRING;
return $string;
}
sub _fix_arrayref {
my ($row) = @_;
return undef if !defined $row;
foreach my $field (@$row) {
$field = _fix_empty($field) if defined $field;
}
return $row;
}
sub _fix_hashref {
my ($row) = @_;
return undef if !defined $row;
foreach my $value (values %$row) {
$value = _fix_empty($value) if defined $value;
}
return $row;
}
sub adjust_statement {
my ($sql) = @_;
# We can't just assume any occurrence of "''" in $sql is an empty
# string, since "''" can occur inside a string literal as a way of
# escaping a single "'" in the literal. Therefore we must be trickier...
# split the statement into parts by single-quotes. The negative value
# at the end to the split operator from dropping trailing empty strings
# (e.g., when $sql ends in "''")
my @parts = split /'/, $sql, -1;
if( !(@parts % 2) ) {
# Either the string is empty or the quotes are mismatched
# Returning input unmodified.
return $sql;
}
# We already verified that we have an odd number of parts. If we take
# the first part off now, we know we're entering the loop with an even
# number of parts
my @result;
my $part = shift @parts;
# Oracle requires a FROM clause in all SELECT statements, so append
# "FROM dual" to queries without one (e.g., "SELECT NOW()")
my $is_select = ($part =~ m/^\s*SELECT\b/io);
my $has_from = ($part =~ m/\bFROM\b/io) if $is_select;
# Oracle recognizes CURRENT_DATE, but not CURRENT_DATE()
$part =~ s/\bCURRENT_DATE\b\(\)/CURRENT_DATE/io;
# Oracle use SUBSTR instead of SUBSTRING
$part =~ s/\bSUBSTRING\b/SUBSTR/io;
# Oracle need no 'AS'
$part =~ s/\bAS\b//ig;
# Oracle doesn't have LIMIT, so if we find the LIMIT comment, wrap the
# query with "SELECT * FROM (...) WHERE rownum < $limit"
my ($limit,$offset) = ($part =~ m{/\* LIMIT (\d*) (\d*) \*/}o);
push @result, $part;
while( @parts ) {
my $string = shift @parts;
my $nonstring = shift @parts;
# if the non-string part is zero-length and there are more parts left,
# then this is an escaped quote inside a string literal
while( !(length $nonstring) && @parts ) {
# we know it's safe to remove two parts at a time, since we
# entered the loop with an even number of parts
$string .= "''" . shift @parts;
$nonstring = shift @parts;
}
# Look for a FROM if this is a SELECT and we haven't found one yet
$has_from = ($nonstring =~ m/\bFROM\b/io)
if ($is_select and !$has_from);
# Oracle recognizes CURRENT_DATE, but not CURRENT_DATE()
$nonstring =~ s/\bCURRENT_DATE\b\(\)/CURRENT_DATE/io;
# Oracle use SUBSTR instead of SUBSTRING
$nonstring =~ s/\bSUBSTRING\b/SUBSTR/io;
# Oracle need no 'AS'
$nonstring =~ s/\bAS\b//ig;
# Look for a LIMIT clause
($limit) = ($nonstring =~ m(/\* LIMIT (\d*) \*/)o);
if(!length($string)){
push @result, EMPTY_STRING;
push @result, $nonstring;
} else {
push @result, $string;
push @result, $nonstring;
}
}
my $new_sql = join "'", @result;
# Append "FROM dual" if this is a SELECT without a FROM clause
$new_sql .= " FROM DUAL" if ($is_select and !$has_from);
# Wrap the query with a "WHERE rownum <= ..." if we found LIMIT
if (defined($limit)) {
if ($new_sql !~ /\bWHERE\b/) {
$new_sql = $new_sql." WHERE 1=1";
}
my ($before_where, $after_where) = split /\bWHERE\b/i,$new_sql;
if (defined($offset)) {
if ($new_sql =~ /(.*\s+)FROM(\s+.*)/i) {
my ($before_from,$after_from) = ($1,$2);
$before_where = "$before_from FROM ($before_from,"
. " ROW_NUMBER() OVER (ORDER BY 1) R "
. " FROM $after_from ) ";
$after_where = " R BETWEEN $offset+1 AND $limit+$offset";
}
} else {
$after_where = " rownum <=$limit AND ".$after_where;
}
$new_sql = $before_where." WHERE ".$after_where;
}
return $new_sql;
}
sub do {
my $self = shift;
my $sql = shift;
$sql = adjust_statement($sql);
unshift @_, $sql;
return $self->SUPER::do(@_);
}
sub selectrow_array {
my $self = shift;
my $stmt = shift;
my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt);
unshift @_, $new_stmt;
if ( wantarray ) {
my @row = $self->SUPER::selectrow_array(@_);
_fix_arrayref(\@row);
return @row;
} else {
my $row = $self->SUPER::selectrow_array(@_);
$row = _fix_empty($row) if defined $row;
return $row;
}
}
sub selectrow_arrayref {
my $self = shift;
my $stmt = shift;
my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt);
unshift @_, $new_stmt;
my $ref = $self->SUPER::selectrow_arrayref(@_);
return undef if !defined $ref;
_fix_arrayref($ref);
return $ref;
}
sub selectrow_hashref {
my $self = shift;
my $stmt = shift;
my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt);
unshift @_, $new_stmt;
my $ref = $self->SUPER::selectrow_hashref(@_);
return undef if !defined $ref;
_fix_hashref($ref);
return $ref;
}
sub selectall_arrayref {
my $self = shift;
my $stmt = shift;
my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt);
unshift @_, $new_stmt;
my $ref = $self->SUPER::selectall_arrayref(@_);
return undef if !defined $ref;
foreach my $row (@$ref) {
if (ref($row) eq 'ARRAY') {
_fix_arrayref($row);
}
elsif (ref($row) eq 'HASH') {
_fix_hashref($row);
}
}
return $ref;
}
sub selectall_hashref {
my $self = shift;
my $stmt = shift;
my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt);
unshift @_, $new_stmt;
my $rows = $self->SUPER::selectall_hashref(@_);
return undef if !defined $rows;
foreach my $row (values %$rows) {
_fix_hashref($row);
}
return $rows;
}
sub selectcol_arrayref {
my $self = shift;
my $stmt = shift;
my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt);
unshift @_, $new_stmt;
my $ref = $self->SUPER::selectcol_arrayref(@_);
return undef if !defined $ref;
_fix_arrayref($ref);
return $ref;
}
sub prepare {
my $self = shift;
my $sql = shift;
my $new_sql = adjust_statement($sql);
unshift @_, $new_sql;
return bless $self->SUPER::prepare(@_),
'Bugzilla::DB::Oracle::st';
}
sub prepare_cached {
my $self = shift;
my $sql = shift;
my $new_sql = adjust_statement($sql);
unshift @_, $new_sql;
return bless $self->SUPER::prepare_cached(@_),
'Bugzilla::DB::Oracle::st';
}
sub quote_identifier {
my ($self,$id) = @_;
return $id;
}
#####################################################################
# Protected "Real Database" Schema Information Methods
#####################################################################
sub bz_table_columns_real {
my ($self, $table) = @_;
$table = uc($table);
my $cols = $self->selectcol_arrayref(
"SELECT LOWER(COLUMN_NAME) FROM USER_TAB_COLUMNS WHERE
TABLE_NAME = ? ORDER BY COLUMN_NAME", undef, $table);
return @$cols;
}
sub bz_table_list_real {
my ($self) = @_;
my $tables = $self->selectcol_arrayref(
"SELECT LOWER(TABLE_NAME) FROM USER_TABLES WHERE
TABLE_NAME NOT LIKE ? ORDER BY TABLE_NAME", undef, 'DR$%');
return @$tables;
}
#####################################################################
# Custom Database Setup
#####################################################################
sub bz_setup_database {
my $self = shift;
# Create a function that returns SYSDATE to emulate MySQL's "NOW()".
# Function NOW() is used widely in Bugzilla SQLs, but Oracle does not
# have that function, So we have to create one ourself.
$self->do("CREATE OR REPLACE FUNCTION NOW "
. " RETURN DATE IS BEGIN RETURN SYSDATE; END;");
$self->do("CREATE OR REPLACE FUNCTION CHAR_LENGTH(COLUMN_NAME VARCHAR2)"
. " RETURN NUMBER IS BEGIN RETURN LENGTH(COLUMN_NAME); END;");
# Create a WORLD_LEXER named BZ_LEX for multilingual fulltext search
my $lexer = $self->selectcol_arrayref(
"SELECT pre_name FROM CTXSYS.CTX_PREFERENCES WHERE pre_name = ? AND
pre_owner = ?",
undef,'BZ_LEX',uc(Bugzilla->localconfig->{db_user}));
if(!@$lexer) {
$self->do("BEGIN CTX_DDL.CREATE_PREFERENCE
('BZ_LEX', 'WORLD_LEXER'); END;");
}
$self->SUPER::bz_setup_database(@_);
my @tables = $self->bz_table_list_real();
foreach my $table (@tables) {
my @columns = $self->bz_table_columns_real($table);
foreach my $column (@columns) {
my $def = $self->bz_column_info($table, $column);
if ($def->{REFERENCES}) {
my $references = $def->{REFERENCES};
my $update = $references->{UPDATE} || 'CASCADE';
my $to_table = $references->{TABLE};
my $to_column = $references->{COLUMN};
my $fk_name = $self->_bz_schema->_get_fk_name($table,
$column,
$references);
if ( $update =~ /CASCADE/i ){
my $trigger_name = uc($fk_name . "_UC");
my $exist_trigger = $self->selectcol_arrayref(
"SELECT OBJECT_NAME FROM USER_OBJECTS
WHERE OBJECT_NAME = ?", undef, $trigger_name);
if(@$exist_trigger) {
$self->do("DROP TRIGGER $trigger_name");
}
my $tr_str = "CREATE OR REPLACE TRIGGER $trigger_name"
. " AFTER UPDATE OF $to_column ON $to_table "
. " REFERENCING "
. " NEW AS NEW "
. " OLD AS OLD "
. " FOR EACH ROW "
. " BEGIN "
. " UPDATE $table"
. " SET $column = :NEW.$to_column"
. " WHERE $column = :OLD.$to_column;"
. " END $trigger_name;";
$self->do($tr_str);
}
}
}
}
}
package Bugzilla::DB::Oracle::st;
use base qw(DBI::st);
sub fetchrow_arrayref {
my $self = shift;
my $ref = $self->SUPER::fetchrow_arrayref(@_);
return undef if !defined $ref;
Bugzilla::DB::Oracle::_fix_arrayref($ref);
return $ref;
}
sub fetchrow_array {
my $self = shift;
if ( wantarray ) {
my @row = $self->SUPER::fetchrow_array(@_);
Bugzilla::DB::Oracle::_fix_arrayref(\@row);
return @row;
} else {
my $row = $self->SUPER::fetchrow_array(@_);
$row = Bugzilla::DB::Oracle::_fix_empty($row) if defined $row;
return $row;
}
}
sub fetchrow_hashref {
my $self = shift;
my $ref = $self->SUPER::fetchrow_hashref(@_);
return undef if !defined $ref;
Bugzilla::DB::Oracle::_fix_hashref($ref);
return $ref;
}
sub fetchall_arrayref {
my $self = shift;
my $ref = $self->SUPER::fetchall_arrayref(@_);
return undef if !defined $ref;
foreach my $row (@$ref) {
if (ref($row) eq 'ARRAY') {
Bugzilla::DB::Oracle::_fix_arrayref($row);
}
elsif (ref($row) eq 'HASH') {
Bugzilla::DB::Oracle::_fix_hashref($row);
}
}
return $ref;
}
sub fetchall_hashref {
my $self = shift;
my $ref = $self->SUPER::fetchall_hashref(@_);
return undef if !defined $ref;
foreach my $row (values %$ref) {
Bugzilla::DB::Oracle::_fix_hashref($row);
}
return $ref;
}
sub fetch {
my $self = shift;
my $row = $self->SUPER::fetch(@_);
if ($row) {
Bugzilla::DB::Oracle::_fix_arrayref($row);
}
return $row;
}
1;

View File

@@ -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): 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 };
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:dbname=$dbname";
$dsn .= ";host=$host" if $host;
$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 $attrs = { pg_enable_utf8 => Bugzilla->params->{'utf8'} };
my $self = $class->db_new($dsn, $user, $pass, $attrs);
# all class local variables stored in DBI derived class needs to have
# a prefix 'private_'. See DBI documentation.
$self->{private_bz_tables_locked} = "";
# Needed by TheSchwartz
$self->{private_bz_dsn} = $dsn;
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, $nocheck, $real_pattern) = @_;
$real_pattern ||= $pattern;
$self->bz_check_regexp($real_pattern) if !$nocheck;
return "$expr ~* $pattern";
}
sub sql_not_regexp {
my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_;
$real_pattern ||= $pattern;
$self->bz_check_regexp($real_pattern) if !$nocheck;
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))';
}
# Tell us whether or not a particular sequence exists in the DB.
sub bz_sequence_exists {
my ($self, $seq_name) = @_;
my $exists = $self->selectrow_array(
'SELECT 1 FROM pg_statio_user_sequences WHERE relname = ?',
undef, $seq_name);
return $exists || 0;
}
sub bz_explain {
my ($self, $sql) = @_;
my $explain = $self->selectcol_arrayref("EXPLAIN ANALYZE $sql");
return join("\n", @$explain);
}
#####################################################################
# 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');
# Same for all the comments fields in the fulltext table.
$self->bz_drop_index('bugs_fulltext', 'bugs_fulltext_comments_idx');
$self->bz_drop_index('bugs_fulltext',
'bugs_fulltext_comments_noprivate_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'});
# Now that Bugzilla::Object uses sql_istrcmp, other tables
# also need a LOWER() index.
_fix_case_differences('fielddefs', 'name');
$self->bz_add_index('fielddefs', 'fielddefs_name_lower_idx',
{FIELDS => ['LOWER(name)'], TYPE => 'UNIQUE'});
_fix_case_differences('keyworddefs', 'name');
$self->bz_add_index('keyworddefs', 'keyworddefs_name_lower_idx',
{FIELDS => ['LOWER(name)'], TYPE => 'UNIQUE'});
_fix_case_differences('products', 'name');
$self->bz_add_index('products', 'products_name_lower_idx',
{FIELDS => ['LOWER(name)'], TYPE => 'UNIQUE'});
# bz_rename_column didn't correctly rename the sequence.
if ($self->bz_column_info('fielddefs', 'id')
&& $self->bz_sequence_exists('fielddefs_fieldid_seq'))
{
print "Fixing fielddefs_fieldid_seq sequence...\n";
$self->do("ALTER TABLE fielddefs_fieldid_seq RENAME TO fielddefs_id_seq");
$self->do("ALTER TABLE fielddefs ALTER COLUMN id
SET DEFAULT NEXTVAL('fielddefs_id_seq')");
}
}
# Renames things that differ only in case.
sub _fix_case_differences {
my ($table, $field) = @_;
my $dbh = Bugzilla->dbh;
my $duplicates = $dbh->selectcol_arrayref(
"SELECT DISTINCT LOWER($field) FROM $table
GROUP BY LOWER($field) HAVING COUNT(LOWER($field)) > 1");
foreach my $name (@$duplicates) {
my $dups = $dbh->selectcol_arrayref(
"SELECT $field FROM $table WHERE LOWER($field) = ?",
undef, $name);
my $primary = shift @$dups;
foreach my $dup (@$dups) {
my $new_name = "${dup}_";
# Make sure the new name isn't *also* a duplicate.
while (1) {
last if (!$dbh->selectrow_array(
"SELECT 1 FROM $table WHERE LOWER($field) = ?",
undef, lc($new_name)));
$new_name .= "_";
}
print "$table '$primary' and '$dup' have names that differ",
" only in case.\nRenaming '$dup' to '$new_name'...\n";
$dbh->do("UPDATE $table SET $field = ? WHERE $field = ?",
undef, $new_name, $dup);
}
}
}
#####################################################################
# 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

View File

@@ -1,369 +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.
};
use constant MYISAM_TABLES => qw(bugs_fulltext);
#------------------------------------------------------------------------------
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',
LONGTEXT => 'mediumtext',
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) = @_;
my $charset = Bugzilla->dbh->bz_db_is_utf8 ? "CHARACTER SET utf8" : '';
my $type = grep($_ eq $table, MYISAM_TABLES) ? 'MYISAM' : 'InnoDB';
return($self->SUPER::_get_create_table_ddl($table)
. " ENGINE = $type $charset");
} #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
#--------------------------------------------------------------------
sub get_create_database_sql {
my ($self, $name) = @_;
# We only create as utf8 if we have no params (meaning we're doing
# a new installation) or if the utf8 param is on.
my $create_utf8 = Bugzilla->params->{'utf8'}
|| !defined Bugzilla->params->{'utf8'};
my $charset = $create_utf8 ? "CHARACTER SET utf8" : '';
return ("CREATE DATABASE $name $charset");
}
# 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_fk_sql {
my ($self, $table, $column, $references) = @_;
my $fk_name = $self->_get_fk_name($table, $column, $references);
my @sql = ("ALTER TABLE $table DROP FOREIGN KEY $fk_name");
my $dbh = Bugzilla->dbh;
# MySQL requires, and will create, an index on any column with
# an FK. It will name it after the fk, which we never do.
# So if there's an index named after the fk, we also have to delete it.
if ($dbh->bz_index_info_real($table, $fk_name)) {
push(@sql, $self->get_drop_index_ddl($table, $fk_name));
}
return @sql;
}
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;

View File

@@ -1,403 +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 Oracle Corporation.
# Portions created by Oracle are Copyright (C) 2007 Oracle Corporation.
# All Rights Reserved.
#
# Contributor(s): Lance Larsh <lance.larsh@oracle.com>
# Xiaoou Wu <xiaoou.wu@oracle.com>
# Max Kanat-Alexander <mkanat@bugzilla.org>
package Bugzilla::DB::Schema::Oracle;
###############################################################################
#
# DB::Schema implementation for Oracle
#
###############################################################################
use strict;
use base qw(Bugzilla::DB::Schema);
use Carp qw(confess);
use Bugzilla::Util;
use constant ADD_COLUMN => 'ADD';
# Whether this is true or not, this is what it needs to be in order for
# hash_identifier to maintain backwards compatibility with versions before
# 3.2rc2.
use constant MAX_IDENTIFIER_LEN => 27;
#------------------------------------------------------------------------------
sub _initialize {
my $self = shift;
$self = $self->SUPER::_initialize(@_);
$self->{db_specific} = {
BOOLEAN => 'integer',
FALSE => '0',
TRUE => '1',
INT1 => 'integer',
INT2 => 'integer',
INT3 => 'integer',
INT4 => 'integer',
SMALLSERIAL => 'integer',
MEDIUMSERIAL => 'integer',
INTSERIAL => 'integer',
TINYTEXT => 'varchar(255)',
MEDIUMTEXT => 'varchar(4000)',
LONGTEXT => 'clob',
LONGBLOB => 'blob',
DATETIME => 'date',
};
$self->_adjust_schema;
return $self;
} #eosub--_initialize
#--------------------------------------------------------------------
sub get_table_ddl {
my $self = shift;
my $table = shift;
unshift @_, $table;
my @ddl = $self->SUPER::get_table_ddl(@_);
my @fields = @{ $self->{abstract_schema}{$table}{FIELDS} || [] };
while (@fields) {
my $field_name = shift @fields;
my $field_info = shift @fields;
# Create triggers to deal with empty string.
if ( $field_info->{TYPE} =~ /varchar|TEXT/i
&& $field_info->{NOTNULL} ) {
push (@ddl, _get_notnull_trigger_ddl($table, $field_name));
}
# Create sequences and triggers to emulate SERIAL datatypes.
if ( $field_info->{TYPE} =~ /SERIAL/i ) {
push (@ddl, $self->_get_create_seq_ddl($table, $field_name));
}
}
return @ddl;
} #eosub--get_table_ddl
# Extend superclass method to create Oracle Text indexes if index type
# is FULLTEXT from schema. Returns a "create index" SQL statement.
sub _get_create_index_ddl {
my ($self, $table_name, $index_name, $index_fields, $index_type) = @_;
$index_name = "idx_" . $self->_hash_identifier($index_name);
if ($index_type eq 'FULLTEXT') {
my $sql = "CREATE INDEX $index_name ON $table_name ("
. join(',',@$index_fields)
. ") INDEXTYPE IS CTXSYS.CONTEXT "
. " PARAMETERS('LEXER BZ_LEX SYNC(ON COMMIT)')" ;
return $sql;
}
return($self->SUPER::_get_create_index_ddl($table_name, $index_name,
$index_fields, $index_type));
}
sub get_drop_index_ddl {
my $self = shift;
my ($table, $name) = @_;
$name = 'idx_' . $self->_hash_identifier($name);
return $self->SUPER::get_drop_index_ddl($table, $name);
}
# Oracle supports the use of FOREIGN KEY integrity constraints
# to define the referential integrity actions, including:
# - Update and delete No Action (default)
# - Delete CASCADE
# - Delete SET NULL
sub get_fk_ddl {
my ($self, $table, $column, $references) = @_;
return "" if !$references;
my $update = $references->{UPDATE} || 'CASCADE';
my $delete = $references->{DELETE};
my $to_table = $references->{TABLE} || confess "No table in reference";
my $to_column = $references->{COLUMN} || confess "No column in reference";
my $fk_name = $self->_get_fk_name($table, $column, $references);
my $fk_string = "\n CONSTRAINT $fk_name FOREIGN KEY ($column)\n"
. " REFERENCES $to_table($to_column)\n";
$fk_string = $fk_string . " ON DELETE $delete" if $delete;
if ( $update =~ /CASCADE/i ){
my $tr_str = "CREATE OR REPLACE TRIGGER ${fk_name}_UC"
. " AFTER UPDATE OF $to_column ON $to_table "
. " REFERENCING "
. " NEW AS NEW "
. " OLD AS OLD "
. " FOR EACH ROW "
. " BEGIN "
. " UPDATE $table"
. " SET $column = :NEW.$to_column"
. " WHERE $column = :OLD.$to_column;"
. " END ${fk_name}_UC;";
my $dbh = Bugzilla->dbh;
$dbh->do($tr_str);
}
return $fk_string;
}
sub get_drop_fk_sql {
my $self = shift;
my ($table, $column, $references) = @_;
my $fk_name = $self->_get_fk_name(@_);
my @sql;
if (!$references->{UPDATE} || $references->{UPDATE} =~ /CASCADE/i) {
push(@sql, "DROP TRIGGER ${fk_name}_uc");
}
push(@sql, $self->SUPER::get_drop_fk_sql(@_));
return @sql;
}
sub _get_fk_name {
my ($self, $table, $column, $references) = @_;
my $to_table = $references->{TABLE};
my $to_column = $references->{COLUMN};
my $fk_name = "${table}_${column}_${to_table}_${to_column}";
$fk_name = "fk_" . $self->_hash_identifier($fk_name);
return $fk_name;
}
sub get_alter_column_ddl {
my ($self, $table, $column, $new_def, $set_nulls_to) = @_;
my @statements;
my $old_def = $self->get_column_abstract($table, $column);
my $specific = $self->{db_specific};
# If the types have changed, we have to deal with that.
if (uc(trim($old_def->{TYPE})) ne uc(trim($new_def->{TYPE}))) {
push(@statements, $self->_get_alter_type_sql($table, $column,
$new_def, $old_def));
}
my $default = $new_def->{DEFAULT};
my $default_old = $old_def->{DEFAULT};
# This first condition prevents "uninitialized value" errors.
if (!defined $default && !defined $default_old) {
# Do Nothing
}
# If we went from having a default to not having one
elsif (!defined $default && defined $default_old) {
push(@statements, "ALTER TABLE $table MODIFY $column"
. " DEFAULT NULL");
}
# If we went from no default to a default, or we changed the default.
elsif ( (defined $default && !defined $default_old) ||
($default ne $default_old) )
{
$default = $specific->{$default} if exists $specific->{$default};
push(@statements, "ALTER TABLE $table MODIFY $column "
. " DEFAULT $default");
}
# If we went from NULL to NOT NULL.
if (!$old_def->{NOTNULL} && $new_def->{NOTNULL}) {
my $setdefault;
# Handle any fields that were NULL before, if we have a default,
$setdefault = $new_def->{DEFAULT} if exists $new_def->{DEFAULT};
# But if we have a set_nulls_to, that overrides the DEFAULT
# (although nobody would usually specify both a default and
# a set_nulls_to.)
$setdefault = $set_nulls_to if defined $set_nulls_to;
if (defined $setdefault) {
push(@statements, "UPDATE $table SET $column = $setdefault"
. " WHERE $column IS NULL");
}
push(@statements, "ALTER TABLE $table MODIFY $column"
. " NOT NULL");
push (@statements, _get_notnull_trigger_ddl($table, $column))
if $old_def->{TYPE} =~ /varchar|text/i
&& $new_def->{TYPE} =~ /varchar|text/i;
}
# If we went from NOT NULL to NULL
elsif ($old_def->{NOTNULL} && !$new_def->{NOTNULL}) {
push(@statements, "ALTER TABLE $table MODIFY $column"
. " NULL");
push(@statements, "DROP TRIGGER ${table}_${column}")
if $new_def->{TYPE} =~ /varchar|text/i
&& $old_def->{TYPE} =~ /varchar|text/i;
}
# If we went from not being a PRIMARY KEY to being a PRIMARY KEY.
if (!$old_def->{PRIMARYKEY} && $new_def->{PRIMARYKEY}) {
push(@statements, "ALTER TABLE $table ADD PRIMARY KEY ($column)");
}
# If we went from being a PK to not being a PK
elsif ( $old_def->{PRIMARYKEY} && !$new_def->{PRIMARYKEY} ) {
push(@statements, "ALTER TABLE $table DROP PRIMARY KEY");
}
return @statements;
}
sub _get_alter_type_sql {
my ($self, $table, $column, $new_def, $old_def) = @_;
my @statements;
my $type = $new_def->{TYPE};
$type = $self->{db_specific}->{$type}
if exists $self->{db_specific}->{$type};
if ($type =~ /serial/i && $old_def->{TYPE} !~ /serial/i) {
die("You cannot specify a DEFAULT on a SERIAL-type column.")
if $new_def->{DEFAULT};
}
if ( ($old_def->{TYPE} =~ /LONGTEXT/i && $new_def->{TYPE} !~ /LONGTEXT/i)
|| ($old_def->{TYPE} !~ /LONGTEXT/i && $new_def->{TYPE} =~ /LONGTEXT/i)
) {
# LONG to VARCHAR or VARCHAR to LONG is not allowed in Oracle,
# just a way to work around.
# Determine whether column_temp is already exist.
my $dbh=Bugzilla->dbh;
my $column_exist = $dbh->selectcol_arrayref(
"SELECT CNAME FROM COL WHERE TNAME = UPPER(?) AND
CNAME = UPPER(?)", undef,$table,$column . "_temp");
if(!@$column_exist) {
push(@statements,
"ALTER TABLE $table ADD ${column}_temp $type");
}
push(@statements, "UPDATE $table SET ${column}_temp = $column");
push(@statements, "COMMIT");
push(@statements, "ALTER TABLE $table DROP COLUMN $column");
push(@statements,
"ALTER TABLE $table RENAME COLUMN ${column}_temp TO $column");
} else {
push(@statements, "ALTER TABLE $table MODIFY $column $type");
}
if ($new_def->{TYPE} =~ /serial/i && $old_def->{TYPE} !~ /serial/i) {
push(@statements, _get_create_seq_ddl($table, $column));
}
# If this column is no longer SERIAL, we need to drop the sequence
# that went along with it.
if ($old_def->{TYPE} =~ /serial/i && $new_def->{TYPE} !~ /serial/i) {
push(@statements, "DROP SEQUENCE ${table}_${column}_SEQ");
push(@statements, "DROP TRIGGER ${table}_${column}_TR");
}
# If this column is changed to type TEXT/VARCHAR, we need to deal with
# empty string.
if ( $old_def->{TYPE} !~ /varchar|text/i
&& $new_def->{TYPE} =~ /varchar|text/i
&& $new_def->{NOTNULL} )
{
push (@statements, _get_notnull_trigger_ddl($table, $column));
}
# If this column is no longer TEXT/VARCHAR, we need to drop the trigger
# that went along with it.
if ( $old_def->{TYPE} =~ /varchar|text/i
&& $old_def->{NOTNULL}
&& $new_def->{TYPE} !~ /varchar|text/i )
{
push(@statements, "DROP TRIGGER ${table}_${column}");
}
return @statements;
}
sub get_rename_column_ddl {
my ($self, $table, $old_name, $new_name) = @_;
if (lc($old_name) eq lc($new_name)) {
# if the only change is a case change, return an empty list.
return ();
}
my @sql = ("ALTER TABLE $table RENAME COLUMN $old_name TO $new_name");
my $def = $self->get_column_abstract($table, $old_name);
if ($def->{TYPE} =~ /SERIAL/i) {
# We have to rename the series also, and fix the default of the series.
push(@sql, "RENAME ${table}_${old_name}_SEQ TO
${table}_${new_name}_seq");
my $serial_sql =
"CREATE OR REPLACE TRIGGER ${table}_${new_name}_TR "
. " BEFORE INSERT ON ${table} "
. " FOR EACH ROW "
. " BEGIN "
. " SELECT ${table}_${new_name}_SEQ.NEXTVAL "
. " INTO :NEW.${new_name} FROM DUAL; "
. " END;";
push(@sql, $serial_sql);
push(@sql, "DROP TRIGGER ${table}_${old_name}_TR");
}
if ($def->{TYPE} =~ /varchar|text/i && $def->{NOTNULL} ) {
push(@sql, _get_notnull_trigger_ddl($table,$new_name));
push(@sql, "DROP TRIGGER ${table}_${old_name}");
}
return @sql;
}
sub _get_notnull_trigger_ddl {
my ($table, $column) = @_;
my $notnull_sql = "CREATE OR REPLACE TRIGGER "
. " ${table}_${column}"
. " BEFORE INSERT OR UPDATE ON ". $table
. " FOR EACH ROW"
. " BEGIN "
. " IF :NEW.". $column ." IS NULL THEN "
. " SELECT '" . Bugzilla::DB::Oracle->EMPTY_STRING
. "' INTO :NEW.". $column ." FROM DUAL; "
. " END IF; "
. " END ".$table.";";
return $notnull_sql;
}
sub _get_create_seq_ddl {
my ($self, $table, $column, $start_with) = @_;
$start_with ||= 1;
my @ddl;
my $seq_name = "${table}_${column}_SEQ";
my $seq_sql = "CREATE SEQUENCE $seq_name "
. " INCREMENT BY 1 "
. " START WITH $start_with "
. " NOMAXVALUE "
. " NOCYCLE "
. " NOCACHE";
my $serial_sql = "CREATE OR REPLACE TRIGGER ${table}_${column}_TR "
. " BEFORE INSERT ON ${table} "
. " FOR EACH ROW "
. " BEGIN "
. " SELECT ${seq_name}.NEXTVAL "
. " INTO :NEW.${column} FROM DUAL; "
. " END;";
push (@ddl, $seq_sql);
push (@ddl, $serial_sql);
return @ddl;
}
1;

View File

@@ -1,166 +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);
use Storable qw(dclone);
#------------------------------------------------------------------------------
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',
LONGTEXT => 'text',
LONGBLOB => 'bytea',
DATETIME => 'timestamp(0) without time zone',
};
$self->_adjust_schema;
return $self;
} #eosub--_initialize
#--------------------------------------------------------------------
sub get_rename_column_ddl {
my ($self, $table, $old_name, $new_name) = @_;
if (lc($old_name) eq lc($new_name)) {
# if the only change is a case change, return an empty list, since Pg
# is case-insensitive and will return an error about a duplicate name
return ();
}
my @sql = ("ALTER TABLE $table RENAME COLUMN $old_name TO $new_name");
my $def = $self->get_column_abstract($table, $old_name);
if ($def->{TYPE} =~ /SERIAL/i) {
# We have to rename the series also, and fix the default of the series.
push(@sql, "ALTER TABLE ${table}_${old_name}_seq
RENAME TO ${table}_${new_name}_seq");
push(@sql, "ALTER TABLE $table ALTER COLUMN $new_name
SET DEFAULT NEXTVAL('${table}_${new_name}_seq')");
}
return @sql;
}
sub get_rename_table_sql {
my ($self, $old_name, $new_name) = @_;
if (lc($old_name) eq lc($new_name)) {
# if the only change is a case change, return an empty list, since Pg
# is case-insensitive and will return an error about a duplicate name
return ();
}
return ("ALTER TABLE $old_name RENAME TO $new_name");
}
sub _get_alter_type_sql {
my ($self, $table, $column, $new_def, $old_def) = @_;
my @statements;
my $type = $new_def->{TYPE};
$type = $self->{db_specific}->{$type}
if exists $self->{db_specific}->{$type};
if ($type =~ /serial/i && $old_def->{TYPE} !~ /serial/i) {
die("You cannot specify a DEFAULT on a SERIAL-type column.")
if $new_def->{DEFAULT};
$type =~ s/serial/integer/i;
}
# On Pg, you don't need UNIQUE if you're a PK--it creates
# two identical indexes otherwise.
$type =~ s/unique//i if $new_def->{PRIMARYKEY};
push(@statements, "ALTER TABLE $table ALTER COLUMN $column
TYPE $type");
if ($new_def->{TYPE} =~ /serial/i && $old_def->{TYPE} !~ /serial/i) {
push(@statements, "CREATE SEQUENCE ${table}_${column}_seq");
push(@statements, "SELECT setval('${table}_${column}_seq',
MAX($table.$column))
FROM $table");
push(@statements, "ALTER TABLE $table ALTER COLUMN $column
SET DEFAULT nextval('${table}_${column}_seq')");
}
# If this column is no longer SERIAL, we need to drop the sequence
# that went along with it.
if ($old_def->{TYPE} =~ /serial/i && $new_def->{TYPE} !~ /serial/i) {
push(@statements, "ALTER TABLE $table ALTER COLUMN $column
DROP DEFAULT");
# XXX Pg actually won't let us drop the sequence, even though it's
# no longer in use. So we harmlessly leave behind a sequence
# that does nothing.
#push(@statements, "DROP SEQUENCE ${table}_${column}_seq");
}
return @statements;
}
1;

View File

@@ -1,234 +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>
# Marc Schumann <wurblzap@gmail.com>
# Frédéric Buclin <LpSolit@gmail.com>
package Bugzilla::Error;
use strict;
use base qw(Exporter);
@Bugzilla::Error::EXPORT = qw(ThrowCodeError ThrowTemplateError ThrowUserError);
use Bugzilla::Constants;
use Bugzilla::WebService::Constants;
use Bugzilla::Util;
use Date::Format;
# We cannot use $^S to detect if we are in an eval(), because mod_perl
# already eval'uates everything, so $^S = 1 in all cases under mod_perl!
sub _in_eval {
my $in_eval = 0;
for (my $stack = 1; my $sub = (caller($stack))[3]; $stack++) {
last if $sub =~ /^ModPerl/;
$in_eval = 1 if $sub =~ /^\(eval\)/;
}
return $in_eval;
}
sub _throw_error {
my ($name, $error, $vars) = @_;
my $dbh = Bugzilla->dbh;
$vars ||= {};
$vars->{error} = $error;
# Make sure any transaction is rolled back (if supported).
# If we are within an eval(), do not roll back transactions as we are
# eval'uating some test on purpose.
$dbh->bz_rollback_transaction() if ($dbh->bz_in_transaction() && !_in_eval());
my $datadir = bz_locations()->{'datadir'};
# 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->error_mode == ERROR_MODE_WEBPAGE) {
print Bugzilla->cgi->header();
$template->process($name, $vars)
|| ThrowTemplateError($template->error());
}
else {
my $message;
$template->process($name, $vars, \$message)
|| ThrowTemplateError($template->error());
if (Bugzilla->error_mode == ERROR_MODE_DIE) {
die("$message\n");
}
elsif (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT) {
# Clone the hash so we aren't modifying the constant.
my %error_map = %{ WS_ERROR_CODE() };
require Bugzilla::Hook;
Bugzilla::Hook::process('webservice-error_codes',
{ error_map => \%error_map });
my $code = $error_map{$error};
if (!$code) {
$code = ERROR_UNKNOWN_FATAL if $name =~ /code/i;
$code = ERROR_UNKNOWN_TRANSIENT if $name =~ /user/i;
}
die SOAP::Fault->faultcode($code)->faultstring($message);
}
}
exit;
}
sub ThrowUserError {
_throw_error("global/user-error.html.tmpl", @_);
}
sub ThrowCodeError {
_throw_error("global/code-error.html.tmpl", @_);
}
sub ThrowTemplateError {
my ($template_err) = @_;
my $dbh = Bugzilla->dbh;
# Make sure the transaction is rolled back (if supported).
$dbh->bz_rollback_transaction() if $dbh->bz_in_transaction();
my $vars = {};
if (Bugzilla->error_mode == ERROR_MODE_DIE) {
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->params->{'maintainer'};
my $error = html_quote($vars->{'template_error_msg'});
my $error2 = 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,"&amp;")
.replace(/</g,"&lt;")
.replace(/>/g,"&gt;") + "</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>

File diff suppressed because it is too large Load Diff

View File

@@ -1,445 +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 Initial Developer of the Original Code is NASA.
# Portions created by NASA are Copyright (C) 2006 San Jose State
# University Foundation. All Rights Reserved.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org>
use strict;
package Bugzilla::Field::Choice;
use base qw(Bugzilla::Object);
use Bugzilla::Config qw(SetParam write_params);
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Field;
use Bugzilla::Util qw(trim detaint_natural);
use Scalar::Util qw(blessed);
##################
# Initialization #
##################
use constant DB_COLUMNS => qw(
id
value
sortkey
visibility_value_id
);
use constant UPDATE_COLUMNS => qw(
value
sortkey
visibility_value_id
);
use constant NAME_FIELD => 'value';
use constant LIST_ORDER => 'sortkey, value';
use constant REQUIRED_CREATE_FIELDS => qw(value);
use constant VALIDATORS => {
value => \&_check_value,
sortkey => \&_check_sortkey,
visibility_value_id => \&_check_visibility_value_id,
};
use constant CLASS_MAP => {
bug_status => 'Bugzilla::Status',
product => 'Bugzilla::Product',
};
use constant DEFAULT_MAP => {
op_sys => 'defaultopsys',
rep_platform => 'defaultplatform',
priority => 'defaultpriority',
bug_severity => 'defaultseverity',
};
#################
# Class Factory #
#################
# Bugzilla::Field::Choice is actually an abstract base class. Every field
# type has its own dynamically-generated class for its values. This allows
# certain fields to have special types, like how bug_status's values
# are Bugzilla::Status objects.
sub type {
my ($class, $field) = @_;
my $field_obj = blessed $field ? $field : Bugzilla::Field->check($field);
my $field_name = $field_obj->name;
if ($class->CLASS_MAP->{$field_name}) {
return $class->CLASS_MAP->{$field_name};
}
# For generic classes, we use a lowercase class name, so as
# not to interfere with any real subclasses we might make some day.
my $package = "Bugzilla::Field::Choice::$field_name";
Bugzilla->request_cache->{"field_$package"} = $field_obj;
# This package only needs to be created once. We check if the DB_TABLE
# glob for this package already exists, which tells us whether or not
# we need to create the package (this works even under mod_perl, where
# this package definition will persist across requests)).
if (!defined *{"${package}::DB_TABLE"}) {
eval <<EOC;
package $package;
use base qw(Bugzilla::Field::Choice);
use constant DB_TABLE => '$field_name';
EOC
}
return $package;
}
################
# Constructors #
################
# We just make new() enforce this, which should give developers
# the understanding that you can't use Bugzilla::Field::Choice
# without calling type().
sub new {
my $class = shift;
if ($class eq 'Bugzilla::Field::Choice') {
ThrowCodeError('field_choice_must_use_type');
}
$class->SUPER::new(@_);
}
#########################
# Database Manipulation #
#########################
# Our subclasses can take more arguments than we normally accept.
# So, we override create() to remove arguments that aren't valid
# columns. (Normally Bugzilla::Object dies if you pass arguments
# that aren't valid columns.)
sub create {
my $class = shift;
my ($params) = @_;
foreach my $key (keys %$params) {
if (!grep {$_ eq $key} $class->DB_COLUMNS) {
delete $params->{$key};
}
}
return $class->SUPER::create(@_);
}
sub update {
my $self = shift;
my $dbh = Bugzilla->dbh;
my $fname = $self->field->name;
$dbh->bz_start_transaction();
my ($changes, $old_self) = $self->SUPER::update(@_);
if (exists $changes->{value}) {
my ($old, $new) = @{ $changes->{value} };
if ($self->field->type == FIELD_TYPE_MULTI_SELECT) {
$dbh->do("UPDATE bug_$fname SET value = ? WHERE value = ?",
undef, $new, $old);
}
else {
$dbh->do("UPDATE bugs SET $fname = ? WHERE $fname = ?",
undef, $new, $old);
}
if ($old_self->is_default) {
my $param = $self->DEFAULT_MAP->{$self->field->name};
SetParam($param, $self->name);
write_params();
}
}
$dbh->bz_commit_transaction();
return wantarray ? ($changes, $old_self) : $changes;
}
sub remove_from_db {
my $self = shift;
if ($self->is_default) {
ThrowUserError('fieldvalue_is_default',
{ field => $self->field, value => $self,
param_name => $self->DEFAULT_MAP->{$self->field->name},
});
}
if ($self->is_static) {
ThrowUserError('fieldvalue_not_deletable',
{ field => $self->field, value => $self });
}
if ($self->bug_count) {
ThrowUserError("fieldvalue_still_has_bugs",
{ field => $self->field, value => $self });
}
$self->_check_if_controller();
$self->SUPER::remove_from_db();
}
# Factored out to make life easier for subclasses.
sub _check_if_controller {
my $self = shift;
my $vis_fields = $self->controls_visibility_of_fields;
my $values = $self->controlled_values;
if (@$vis_fields || scalar(keys %$values)) {
ThrowUserError('fieldvalue_is_controller',
{ value => $self, fields => [map($_->name, @$vis_fields)],
vals => $values });
}
}
#############
# Accessors #
#############
sub sortkey { return $_[0]->{'sortkey'}; }
sub bug_count {
my $self = shift;
return $self->{bug_count} if defined $self->{bug_count};
my $dbh = Bugzilla->dbh;
my $fname = $self->field->name;
my $count;
if ($self->field->type == FIELD_TYPE_MULTI_SELECT) {
$count = $dbh->selectrow_array("SELECT COUNT(*) FROM bug_$fname
WHERE value = ?", undef, $self->name);
}
else {
$count = $dbh->selectrow_array("SELECT COUNT(*) FROM bugs
WHERE $fname = ?",
undef, $self->name);
}
$self->{bug_count} = $count;
return $count;
}
sub field {
my $invocant = shift;
my $class = ref $invocant || $invocant;
my $cache = Bugzilla->request_cache;
# This is just to make life easier for subclasses. Our auto-generated
# subclasses from type() already have this set.
$cache->{"field_$class"} ||=
new Bugzilla::Field({ name => $class->DB_TABLE });
return $cache->{"field_$class"};
}
sub is_default {
my $self = shift;
my $name = $self->DEFAULT_MAP->{$self->field->name};
# If it doesn't exist in DEFAULT_MAP, then there is no parameter
# related to this field.
return 0 unless $name;
return ($self->name eq Bugzilla->params->{$name}) ? 1 : 0;
}
sub is_static {
my $self = shift;
# If we need to special-case Resolution for *anything* else, it should
# get its own subclass.
if ($self->field->name eq 'resolution') {
return grep($_ eq $self->name, ('', 'FIXED', 'MOVED', 'DUPLICATE'))
? 1 : 0;
}
elsif ($self->field->custom) {
return $self->name eq '---' ? 1 : 0;
}
return 0;
}
sub controls_visibility_of_fields {
my $self = shift;
$self->{controls_visibility_of_fields} ||= Bugzilla::Field->match(
{ visibility_field_id => $self->field->id,
visibility_value_id => $self->id });
return $self->{controls_visibility_of_fields};
}
sub visibility_value {
my $self = shift;
if ($self->{visibility_value_id}) {
$self->{visibility_value} ||=
Bugzilla::Field::Choice->type($self->field->value_field)->new(
$self->{visibility_value_id});
}
return $self->{visibility_value};
}
sub controlled_values {
my $self = shift;
return $self->{controlled_values} if defined $self->{controlled_values};
my $fields = $self->field->controls_values_of;
my %controlled_values;
foreach my $field (@$fields) {
$controlled_values{$field->name} =
Bugzilla::Field::Choice->type($field)
->match({ visibility_value_id => $self->id });
}
$self->{controlled_values} = \%controlled_values;
return $self->{controlled_values};
}
############
# Mutators #
############
sub set_name { $_[0]->set('value', $_[1]); }
sub set_sortkey { $_[0]->set('sortkey', $_[1]); }
sub set_visibility_value {
my ($self, $value) = @_;
$self->set('visibility_value_id', $value);
delete $self->{visibility_value};
}
##############
# Validators #
##############
sub _check_value {
my ($invocant, $value) = @_;
my $field = $invocant->field;
$value = trim($value);
# Make sure people don't rename static values
if (blessed($invocant) && $value ne $invocant->name
&& $invocant->is_static)
{
ThrowUserError('fieldvalue_not_editable',
{ field => $field, old_value => $invocant });
}
ThrowUserError('fieldvalue_undefined') if !defined $value || $value eq "";
ThrowUserError('fieldvalue_name_too_long', { value => $value })
if length($value) > MAX_FIELD_VALUE_SIZE;
my $exists = $invocant->type($field)->new({ name => $value });
if ($exists && (!blessed($invocant) || $invocant->id != $exists->id)) {
ThrowUserError('fieldvalue_already_exists',
{ field => $field, value => $exists });
}
return $value;
}
sub _check_sortkey {
my ($invocant, $value) = @_;
$value = trim($value);
return 0 if !$value;
# Store for the error message in case detaint_natural clears it.
my $orig_value = $value;
detaint_natural($value)
|| ThrowUserError('fieldvalue_sortkey_invalid',
{ sortkey => $orig_value,
field => $invocant->field });
return $value;
}
sub _check_visibility_value_id {
my ($invocant, $value_id) = @_;
$value_id = trim($value_id);
my $field = $invocant->field->value_field;
return undef if !$field || !$value_id;
my $value_obj = Bugzilla::Field::Choice->type($field)
->check({ id => $value_id });
return $value_obj->id;
}
1;
__END__
=head1 NAME
Bugzilla::Field::Choice - A legal value for a <select>-type field.
=head1 SYNOPSIS
my $field = new Bugzilla::Field({name => 'bug_status'});
my $choice = new Bugzilla::Field::Choice->type($field)->new(1);
my $choices = Bugzilla::Field::Choice->type($field)->new_from_list([1,2,3]);
my $choices = Bugzilla::Field::Choice->type($field)->get_all();
my $choices = Bugzilla::Field::Choice->type($field->match({ sortkey => 10 });
=head1 DESCRIPTION
This is an implementation of L<Bugzilla::Object>, but with a twist.
You can't call any class methods (such as C<new>, C<create>, etc.)
directly on C<Bugzilla::Field::Choice> itself. Instead, you have to
call C<Bugzilla::Field::Choice-E<gt>type($field)> to get the class
you're going to instantiate, and then you call the methods on that.
We do that because each field has its own database table for its values, so
each value type needs its own class.
See the L</SYNOPSIS> for examples of how this works.
=head1 METHODS
=head2 Class Factory
In object-oriented design, a "class factory" is a method that picks
and returns the right class for you, based on an argument that you pass.
=over
=item C<type>
Takes a single argument, which is either the name of a field from the
C<fielddefs> table, or a L<Bugzilla::Field> object representing a field.
Returns an appropriate subclass of C<Bugzilla::Field::Choice> that you
can now call class methods on (like C<new>, C<create>, C<match>, etc.)
B<NOTE>: YOU CANNOT CALL CLASS METHODS ON C<Bugzilla::Field::Choice>. You
must call C<type> to get a class you can call methods on.
=back
=head2 Accessors
These are in addition to the standard L<Bugzilla::Object> accessors.
=over
=item C<sortkey>
The key that determines the sort order of this item.
=item C<field>
The L<Bugzilla::Field> object that this field value belongs to.
=item C<controlled_values>
Tells you which values in B<other> fields appear (become visible) when this
value is set in its field.
Returns a hashref of arrayrefs. The hash keys are the names of fields,
and the values are arrays of C<Bugzilla::Field::Choice> objects,
representing values that this value controls the visibility of, for
that field.
=back

File diff suppressed because it is too large Load Diff

View File

@@ -1,482 +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>
use strict;
package Bugzilla::FlagType;
=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 *
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
use Bugzilla::User;
use Bugzilla::Error;
use Bugzilla::Util;
use Bugzilla::Group;
use base qw(Bugzilla::Object);
###############################
#### Initialization ####
###############################
=begin private
=head1 PRIVATE VARIABLES/CONSTANTS
=over
=item C<DB_COLUMNS>
basic sets of columns and tables for getting flag types from the
database.
=back
=cut
use constant DB_COLUMNS => qw(
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<DB_TABLE>
Which database(s) is the data coming from?
Note: when adding tables to DB_TABLE, 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.
=back
=end private
=cut
use constant DB_TABLE => 'flagtypes';
use constant LIST_ORDER => 'flagtypes.sortkey, flagtypes.name';
###############################
#### Accessors ######
###############################
=head2 METHODS
=over
=item C<id>
Returns the ID of the flagtype.
=item C<name>
Returns the name of the flagtype.
=item C<description>
Returns the description of the flagtype.
=item C<cc_list>
Returns the concatenated CC list for the flagtype, as a single string.
=item C<target_type>
Returns whether the flagtype applies to bugs or attachments.
=item C<is_active>
Returns whether the flagtype is active or disabled. Flags being
in a disabled flagtype are not deleted. It only prevents you from
adding new flags to it.
=item C<is_requestable>
Returns whether you can request for the given flagtype
(i.e. whether the '?' flag is available or not).
=item C<is_requesteeble>
Returns whether you can ask someone specifically or not.
=item C<is_multiplicable>
Returns whether you can have more than one flag for the given
flagtype in a given bug/attachment.
=item C<sortkey>
Returns the sortkey of the flagtype.
=back
=cut
sub id { return $_[0]->{'id'}; }
sub name { return $_[0]->{'name'}; }
sub description { return $_[0]->{'description'}; }
sub cc_list { return $_[0]->{'cc_list'}; }
sub target_type { return $_[0]->{'target_type'} eq 'b' ? 'bug' : 'attachment'; }
sub is_active { return $_[0]->{'is_active'}; }
sub is_requestable { return $_[0]->{'is_requestable'}; }
sub is_requesteeble { return $_[0]->{'is_requesteeble'}; }
sub is_multiplicable { return $_[0]->{'is_multiplicable'}; }
sub sortkey { return $_[0]->{'sortkey'}; }
sub request_group_id { return $_[0]->{'request_group_id'}; }
sub grant_group_id { return $_[0]->{'grant_group_id'}; }
###############################
#### Methods ####
###############################
=pod
=over
=item C<grant_list>
Returns a reference to an array of users who have permission to grant this flag type.
The arrays are populated with hashrefs containing the login, identity and visibility of users.
=item C<grant_group>
Returns the group (as a Bugzilla::Group object) in which a user
must be in order to grant or deny a request.
=item C<request_group>
Returns the group (as a Bugzilla::Group object) in which a user
must be in order to request or clear a flag.
=item C<flag_count>
Returns the number of flags belonging to the flagtype.
=item C<inclusions>
Return a hash of product/component IDs and names
explicitly associated with the flagtype.
=item C<exclusions>
Return a hash of product/component IDs and names
explicitly excluded from the flagtype.
=back
=cut
sub grant_list {
my $self = shift;
my @custusers;
my @allusers = @{Bugzilla->user->get_userlist};
foreach my $user (@allusers) {
my $user_obj = new Bugzilla::User({name => $user->{login}});
push(@custusers, $user) if $user_obj->can_set_flag($self);
}
return \@custusers;
}
sub grant_group {
my $self = shift;
if (!defined $self->{'grant_group'} && $self->{'grant_group_id'}) {
$self->{'grant_group'} = new Bugzilla::Group($self->{'grant_group_id'});
}
return $self->{'grant_group'};
}
sub request_group {
my $self = shift;
if (!defined $self->{'request_group'} && $self->{'request_group_id'}) {
$self->{'request_group'} = new Bugzilla::Group($self->{'request_group_id'});
}
return $self->{'request_group'};
}
sub flag_count {
my $self = shift;
if (!defined $self->{'flag_count'}) {
$self->{'flag_count'} =
Bugzilla->dbh->selectrow_array('SELECT COUNT(*) FROM flags
WHERE type_id = ?', undef, $self->{'id'});
}
return $self->{'flag_count'};
}
sub inclusions {
my $self = shift;
$self->{'inclusions'} ||= get_clusions($self->id, 'in');
return $self->{'inclusions'};
}
sub exclusions {
my $self = shift;
$self->{'exclusions'} ||= get_clusions($self->id, 'ex');
return $self->{'exclusions'};
}
######################################################################
# Public Functions
######################################################################
=pod
=head1 PUBLIC FUNCTIONS/METHODS
=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)>
Queries the database for flag types matching the given criteria
and returns a list of matching flagtype objects.
=back
=cut
sub match {
my ($criteria) = @_;
my $dbh = Bugzilla->dbh;
# Depending on the criteria, we may have to append additional tables.
my $tables = [DB_TABLE];
my @criteria = sqlify_criteria($criteria, $tables);
$tables = join(' ', @$tables);
$criteria = join(' AND ', @criteria);
my $flagtype_ids = $dbh->selectcol_arrayref("SELECT id FROM $tables WHERE $criteria");
return Bugzilla::FlagType->new_from_list($flagtype_ids);
}
=pod
=over
=item C<count($criteria)>
Returns the total number of flag types matching the given criteria.
=back
=cut
sub count {
my ($criteria) = @_;
my $dbh = Bugzilla->dbh;
# Depending on the criteria, we may have to append additional tables.
my $tables = [DB_TABLE];
my @criteria = sqlify_criteria($criteria, $tables);
$tables = join(' ', @$tables);
$criteria = join(' AND ', @criteria);
my $count = $dbh->selectrow_array("SELECT COUNT(flagtypes.id)
FROM $tables WHERE $criteria");
return $count;
}
######################################################################
# 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) = @_;
my $dbh = Bugzilla->dbh;
# 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}) {
my $name = $dbh->quote($criteria->{name});
trick_taint($name); # Detaint data as we have quoted it.
push(@criteria, "flagtypes.name = $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 = $criteria->{target_type} eq 'bug'? 'b' : 'a';
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;
}
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>
=item Frédéric Buclin <LpSolit@gmail.com>
=back
=cut

View File

@@ -1,432 +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>
# Max Kanat-Alexander <mkanat@bugzilla.org>
use strict;
package Bugzilla::Group;
use base qw(Bugzilla::Object);
use Bugzilla::Constants;
use Bugzilla::Util;
use Bugzilla::Error;
use Bugzilla::Config qw(:admin);
###############################
##### Module Initialization ###
###############################
use constant DB_COLUMNS => qw(
groups.id
groups.name
groups.description
groups.isbuggroup
groups.userregexp
groups.isactive
groups.icon_url
);
use constant DB_TABLE => 'groups';
use constant LIST_ORDER => 'isbuggroup, name';
use constant VALIDATORS => {
name => \&_check_name,
description => \&_check_description,
userregexp => \&_check_user_regexp,
isactive => \&_check_is_active,
isbuggroup => \&_check_is_bug_group,
icon_url => \&_check_icon_url,
};
use constant REQUIRED_CREATE_FIELDS => qw(name description isbuggroup);
use constant UPDATE_COLUMNS => qw(
name
description
userregexp
isactive
icon_url
);
# Parameters that are lists of groups.
use constant GROUP_PARAMS => qw(chartgroup insidergroup timetrackinggroup
querysharegroup);
###############################
#### Accessors ######
###############################
sub description { return $_[0]->{'description'}; }
sub is_bug_group { return $_[0]->{'isbuggroup'}; }
sub user_regexp { return $_[0]->{'userregexp'}; }
sub is_active { return $_[0]->{'isactive'}; }
sub icon_url { return $_[0]->{'icon_url'}; }
sub members_direct {
my ($self) = @_;
return $self->{members_direct} if defined $self->{members_direct};
my $dbh = Bugzilla->dbh;
my $user_ids = $dbh->selectcol_arrayref(
"SELECT user_group_map.user_id
FROM user_group_map
WHERE user_group_map.group_id = ?
AND grant_type = " . GRANT_DIRECT . "
AND isbless = 0", undef, $self->id);
require Bugzilla::User;
$self->{members_direct} = Bugzilla::User->new_from_list($user_ids);
return $self->{members_direct};
}
sub grant_direct {
my ($self, $type) = @_;
$self->{grant_direct} ||= {};
return $self->{grant_direct}->{$type}
if defined $self->{grant_direct}->{$type};
my $dbh = Bugzilla->dbh;
my $ids = $dbh->selectcol_arrayref(
"SELECT member_id FROM group_group_map
WHERE grantor_id = ? AND grant_type = $type",
undef, $self->id) || [];
$self->{grant_direct}->{$type} = $self->new_from_list($ids);
return $self->{grant_direct}->{$type};
}
sub granted_by_direct {
my ($self, $type) = @_;
$self->{granted_by_direct} ||= {};
return $self->{granted_by_direct}->{$type}
if defined $self->{granted_by_direct}->{$type};
my $dbh = Bugzilla->dbh;
my $ids = $dbh->selectcol_arrayref(
"SELECT grantor_id FROM group_group_map
WHERE member_id = ? AND grant_type = $type",
undef, $self->id) || [];
$self->{granted_by_direct}->{$type} = $self->new_from_list($ids);
return $self->{granted_by_direct}->{$type};
}
###############################
#### Methods ####
###############################
sub set_description { $_[0]->set('description', $_[1]); }
sub set_is_active { $_[0]->set('isactive', $_[1]); }
sub set_name { $_[0]->set('name', $_[1]); }
sub set_user_regexp { $_[0]->set('userregexp', $_[1]); }
sub set_icon_url { $_[0]->set('icon_url', $_[1]); }
sub update {
my $self = shift;
my $changes = $self->SUPER::update(@_);
if (exists $changes->{name}) {
my ($old_name, $new_name) = @{$changes->{name}};
my $update_params;
foreach my $group (GROUP_PARAMS) {
if ($old_name eq Bugzilla->params->{$group}) {
SetParam($group, $new_name);
$update_params = 1;
}
}
write_params() if $update_params;
}
# If we've changed this group to be active, fix any Mandatory groups.
$self->_enforce_mandatory if (exists $changes->{isactive}
&& $changes->{isactive}->[1]);
$self->_rederive_regexp() if exists $changes->{userregexp};
return $changes;
}
# Add missing entries in bug_group_map for bugs created while
# a mandatory group was disabled and which is now enabled again.
sub _enforce_mandatory {
my ($self) = @_;
my $dbh = Bugzilla->dbh;
my $gid = $self->id;
my $bug_ids =
$dbh->selectcol_arrayref('SELECT bugs.bug_id
FROM bugs
INNER JOIN group_control_map
ON group_control_map.product_id = bugs.product_id
LEFT JOIN bug_group_map
ON bug_group_map.bug_id = bugs.bug_id
AND bug_group_map.group_id = group_control_map.group_id
WHERE group_control_map.group_id = ?
AND group_control_map.membercontrol = ?
AND bug_group_map.group_id IS NULL',
undef, ($gid, CONTROLMAPMANDATORY));
my $sth = $dbh->prepare('INSERT INTO bug_group_map (bug_id, group_id) VALUES (?, ?)');
foreach my $bug_id (@$bug_ids) {
$sth->execute($bug_id, $gid);
}
}
sub is_active_bug_group {
my $self = shift;
return $self->is_active && $self->is_bug_group;
}
sub _rederive_regexp {
my ($self) = @_;
my $dbh = Bugzilla->dbh;
my $sth = $dbh->prepare("SELECT userid, login_name, group_id
FROM profiles
LEFT JOIN user_group_map
ON user_group_map.user_id = profiles.userid
AND group_id = ?
AND grant_type = ?
AND isbless = 0");
my $sthadd = $dbh->prepare("INSERT INTO user_group_map
(user_id, group_id, grant_type, isbless)
VALUES (?, ?, ?, 0)");
my $sthdel = $dbh->prepare("DELETE FROM user_group_map
WHERE user_id = ? AND group_id = ?
AND grant_type = ? and isbless = 0");
$sth->execute($self->id, GRANT_REGEXP);
my $regexp = $self->user_regexp;
while (my ($uid, $login, $present) = $sth->fetchrow_array) {
if ($regexp ne '' and $login =~ /$regexp/i) {
$sthadd->execute($uid, $self->id, GRANT_REGEXP) unless $present;
} else {
$sthdel->execute($uid, $self->id, GRANT_REGEXP) if $present;
}
}
}
sub members_non_inherited {
my ($self) = @_;
return $self->{members_non_inherited}
if exists $self->{members_non_inherited};
my $member_ids = Bugzilla->dbh->selectcol_arrayref(
'SELECT DISTINCT user_id FROM user_group_map
WHERE isbless = 0 AND group_id = ?',
undef, $self->id) || [];
require Bugzilla::User;
$self->{members_non_inherited} = Bugzilla::User->new_from_list($member_ids);
return $self->{members_non_inherited};
}
sub flatten_group_membership {
my ($self, @groups) = @_;
my $dbh = Bugzilla->dbh;
my $sth;
my @groupidstocheck = @groups;
my %groupidschecked = ();
$sth = $dbh->prepare("SELECT member_id FROM group_group_map
WHERE grantor_id = ?
AND grant_type = " . GROUP_MEMBERSHIP);
while (my $node = shift @groupidstocheck) {
$sth->execute($node);
my $member;
while (($member) = $sth->fetchrow_array) {
if (!$groupidschecked{$member}) {
$groupidschecked{$member} = 1;
push @groupidstocheck, $member;
push @groups, $member unless grep $_ == $member, @groups;
}
}
}
return \@groups;
}
################################
##### Module Subroutines ###
################################
sub create {
my $class = shift;
my ($params) = @_;
my $dbh = Bugzilla->dbh;
print get_text('install_group_create', { name => $params->{name} }) . "\n"
if Bugzilla->usage_mode == USAGE_MODE_CMDLINE;
my $group = $class->SUPER::create(@_);
# Since we created a new group, give the "admin" group all privileges
# initially.
my $admin = new Bugzilla::Group({name => 'admin'});
# This function is also used to create the "admin" group itself,
# so there's a chance it won't exist yet.
if ($admin) {
my $sth = $dbh->prepare('INSERT INTO group_group_map
(member_id, grantor_id, grant_type)
VALUES (?, ?, ?)');
$sth->execute($admin->id, $group->id, GROUP_MEMBERSHIP);
$sth->execute($admin->id, $group->id, GROUP_BLESS);
$sth->execute($admin->id, $group->id, GROUP_VISIBLE);
}
$group->_rederive_regexp() if $group->user_regexp;
return $group;
}
sub ValidateGroupName {
my ($name, @users) = (@_);
my $dbh = Bugzilla->dbh;
my $query = "SELECT id FROM groups " .
"WHERE name = ?";
if (Bugzilla->params->{'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;
}
###############################
### Validators ###
###############################
sub _check_name {
my ($invocant, $name) = @_;
$name = trim($name);
$name || ThrowUserError("empty_group_name");
# If we're creating a Group or changing the name...
if (!ref($invocant) || $invocant->name ne $name) {
my $exists = new Bugzilla::Group({name => $name });
ThrowUserError("group_exists", { name => $name }) if $exists;
}
return $name;
}
sub _check_description {
my ($invocant, $desc) = @_;
$desc = trim($desc);
$desc || ThrowUserError("empty_group_description");
return $desc;
}
sub _check_user_regexp {
my ($invocant, $regex) = @_;
$regex = trim($regex) || '';
ThrowUserError("invalid_regexp") unless (eval {qr/$regex/});
return $regex;
}
sub _check_is_active { return $_[1] ? 1 : 0; }
sub _check_is_bug_group {
return $_[1] ? 1 : 0;
}
sub _check_icon_url { return $_[1] ? clean_text($_[1]) : undef; }
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 $user_reg_exp = $group->user_reg_exp;
my $is_active = $group->is_active;
my $icon_url = $group->icon_url;
my $is_active_bug_group = $group->is_active_bug_group;
my $group_id = Bugzilla::Group::ValidateGroupName('admin', @users);
my @groups = Bugzilla::Group->get_all;
=head1 DESCRIPTION
Group.pm represents a Bugzilla Group object. It is an implementation
of L<Bugzilla::Object>, and thus has all the methods that L<Bugzilla::Object>
provides, in addition to any methods documented below.
=head1 SUBROUTINES
=over
=item C<create>
Note that in addition to what L<Bugzilla::Object/create($params)>
normally does, this function also makes the new group be inherited
by the C<admin> group. That is, the C<admin> group will automatically
be a member of this group.
=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.
=back
=head1 METHODS
=over
=item C<members_non_inherited>
Returns an arrayref of L<Bugzilla::User> objects representing people who are
"directly" in this group, meaning that they're in it because they match
the group regular expression, or they have been actually added to the
group manually.
=item C<flatten_group_membership>
Accepts a list of groups and returns a list of all the groups whose members
inherit membership in any group on the list. So, we can determine if a user
is in any of the groups input to flatten_group_membership by querying the
user_group_map for any user with DIRECT or REGEXP membership IN() the list
of groups returned.
=back

View File

@@ -1,543 +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): Zach Lipton <zach@zachlipton.com>
#
package Bugzilla::Hook;
use Bugzilla::Constants;
use Bugzilla::Util;
use Bugzilla::Error;
use strict;
sub process {
my ($name, $args) = @_;
# get a list of all extensions
my @extensions = glob(bz_locations()->{'extensionsdir'} . "/*");
# check each extension to see if it uses the hook
# if so, invoke the extension source file:
foreach my $extension (@extensions) {
# all of these variables come directly from code or directory names.
# If there's malicious data here, we have much bigger issues to
# worry about, so we can safely detaint them:
trick_taint($extension);
# Skip CVS directories and any hidden files/dirs.
next if $extension =~ m{/CVS$} || $extension =~ m{/\.[^/]+$};
next if -e "$extension/disabled";
if (-e $extension.'/code/'.$name.'.pl') {
Bugzilla->hook_args($args);
# Allow extensions to load their own libraries.
local @INC = ("$extension/lib", @INC);
do($extension.'/code/'.$name.'.pl');
ThrowCodeError('extension_invalid',
{ errstr => $@, name => $name, extension => $extension }) if $@;
# Flush stored data.
Bugzilla->hook_args({});
}
}
}
sub enabled_plugins {
my $extdir = bz_locations()->{'extensionsdir'};
my @extensions = glob("$extdir/*");
my %enabled;
foreach my $extension (@extensions) {
trick_taint($extension);
my $extname = $extension;
$extname =~ s{^\Q$extdir\E/}{};
next if $extname eq 'CVS' || $extname =~ /^\./;
next if -e "$extension/disabled";
# Allow extensions to load their own libraries.
local @INC = ("$extension/lib", @INC);
$enabled{$extname} = do("$extension/info.pl");
ThrowCodeError('extension_invalid',
{ errstr => $@, name => 'version',
extension => $extension }) if $@;
}
return \%enabled;
}
1;
__END__
=head1 NAME
Bugzilla::Hook - Extendable extension hooks for Bugzilla code
=head1 SYNOPSIS
use Bugzilla::Hook;
Bugzilla::Hook::process("hookname", { arg => $value, arg2 => $value2 });
=head1 DESCRIPTION
Bugzilla allows extension modules to drop in and add routines at
arbitrary points in Bugzilla code. These points are referred to as
hooks. When a piece of standard Bugzilla code wants to allow an extension
to perform additional functions, it uses Bugzilla::Hook's L</process>
subroutine to invoke any extension code if installed.
There is a sample extension in F<extensions/example/> that demonstrates
most of the things described in this document, as well as many of the
hooks available.
=head2 How Hooks Work
When a hook named C<HOOK_NAME> is run, Bugzilla will attempt to invoke any
source files named F<extensions/*/code/HOOK_NAME.pl>.
So, for example, if your extension is called "testopia", and you
want to have code run during the L</install-update_db> hook, you
would have a file called F<extensions/testopia/code/install-update_db.pl>
that contained perl code to run during that hook.
=head2 Arguments Passed to Hooks
Some L<hooks|/HOOKS> have params that are passed to them.
These params are accessible through L<Bugzilla/hook_args>.
That returns a hashref. Very frequently, if you want your
hook to do anything, you have to modify these variables.
=head2 Versioning Extensions
Every extension must have a file in its root called F<info.pl>.
This file must return a hash when called with C<do>.
The hash must contain a 'version' key with the current version of the
extension. Extension authors can also add any extra infomration to this hash if
required, by adding a new key beginning with x_ which will not be used the
core Bugzilla code.
=head1 SUBROUTINES
=over
=item C<process>
=over
=item B<Description>
Invoke any code hooks with a matching name from any installed extensions.
See C<customization.xml> in the Bugzilla Guide for more information on
Bugzilla's extension mechanism.
=item B<Params>
=over
=item C<$name> - The name of the hook to invoke.
=item C<$args> - A hashref. The named args to pass to the hook.
They will be accessible to the hook via L<Bugzilla/hook_args>.
=back
=item B<Returns> (nothing)
=back
=back
=head1 HOOKS
This describes what hooks exist in Bugzilla currently. They are mostly
in alphabetical order, but some related hooks are near each other instead
of being alphabetical.
=head2 auth-login_methods
This allows you to add new login types to Bugzilla.
(See L<Bugzilla::Auth::Login>.)
Params:
=over
=item C<modules>
This is a hash--a mapping from login-type "names" to the actual module on
disk. The keys will be all the values that were passed to
L<Bugzilla::Auth/login> for the C<Login> parameter. The values are the
actual path to the module on disk. (For example, if the key is C<DB>, the
value is F<Bugzilla/Auth/Login/DB.pm>.)
For your extension, the path will start with
F<extensions/yourextension/lib/>. (See the code in the example extension.)
If your login type is in the hash as a key, you should set that key to the
right path to your module. That module's C<new> method will be called,
probably with empty parameters. If your login type is I<not> in the hash,
you should not set it.
You will be prevented from adding new keys to the hash, so make sure your
key is in there before you modify it. (In other words, you can't add in
login methods that weren't passed to L<Bugzilla::Auth/login>.)
=back
=head2 auth-verify_methods
This works just like L</auth-login_methods> except it's for
login verification methods (See L<Bugzilla::Auth::Verify>.) It also
takes a C<modules> parameter, just like L</auth-login_methods>.
=head2 bug-columns
This allows you to add new fields that will show up in every L<Bugzilla::Bug>
object. Note that you will also need to use the L</bug-fields> hook in
conjunction with this hook to make this work.
Params:
=over
=item C<columns> - An arrayref containing an array of column names. Push
your column name(s) onto the array.
=back
=head2 bug-end_of_create
This happens at the end of L<Bugzilla::Bug/create>, after all other changes are
made to the database. This occurs inside a database transaction.
Params:
=over
=item C<bug> - The changed bug object, with all fields set to their updated
values.
=item C<timestamp> - The timestamp used for all updates in this transaction.
=back
=head2 bug-end_of_update
This happens at the end of L<Bugzilla::Bug/update>, after all other changes are
made to the database. This generally occurs inside a database transaction.
Params:
=over
=item C<bug> - The changed bug object, with all fields set to their updated
values.
=item C<timestamp> - The timestamp used for all updates in this transaction.
=item C<changes> - The hash of changed fields.
C<$changes-E<gt>{field} = [old, new]>
=back
=head2 bug-fields
Allows the addition of database fields from the bugs table to the standard
list of allowable fields in a L<Bugzilla::Bug> object, so that
you can call the field as a method.
Note: You should add here the names of any fields you added in L</bug-columns>.
Params:
=over
=item C<columns> - A arrayref containing an array of column names. Push
your column name(s) onto the array.
=back
=head2 buglist-columns
This happens in buglist.cgi after the standard columns have been defined and
right before the display column determination. It gives you the opportunity
to add additional display columns.
Params:
=over
=item C<columns> - A hashref, where the keys are unique string identifiers
for the column being defined and the values are hashrefs with the
following fields:
=over
=item C<name> - The name of the column in the database.
=item C<title> - The title of the column as displayed to users.
=back
The definition is structured as:
$columns->{$id} = { name => $name, title => $title };
=back
=head2 colchange-columns
This happens in F<colchange.cgi> right after the list of possible display
columns have been defined and gives you the opportunity to add additional
display columns to the list of selectable columns.
Params:
=over
=item C<columns> - An arrayref containing an array of column IDs. Any IDs
added by this hook must have been defined in the the buglist-columns hook.
See L</buglist-columns>.
=back
=head2 config-add_panels
If you want to add new panels to the Parameters administrative interface,
this is where you do it.
Params:
=over
=item C<panel_modules>
A hashref, where the keys are the "name" of the module and the value
is the Perl module containing that config module. For example, if
the name is C<Auth>, the value would be C<Bugzilla::Config::Auth>.
For your extension, the Perl module name must start with
C<extensions::yourextension::lib>. (See the code in the example
extension.)
=back
=head2 config-modify_panels
This is how you modify already-existing panels in the Parameters
administrative interface. For example, if you wanted to add a new
Auth method (modifying Bugzilla::Config::Auth) this is how you'd
do it.
Params:
=over
=item C<panels>
A hashref, where the keys are lower-case panel "names" (like C<auth>,
C<admin>, etc.) and the values are hashrefs. The hashref contains a
single key, C<params>. C<params> is an arrayref--the return value from
C<get_param_list> for that module. You can modify C<params> and
your changes will be reflected in the interface.
Adding new keys to C<panels> will have no effect. You should use
L</config-add_panels> if you want to add new panels.
=back
=head2 enter_bug-entrydefaultvars
This happens right before the template is loaded on enter_bug.cgi.
Params:
=over
=item C<vars> - A hashref. The variables that will be passed into the template.
=back
=head2 flag-end_of_update
This happens at the end of L<Bugzilla::Flag/process>, after all other changes
are made to the database and after emails are sent. It gives you a before/after
snapshot of flags so you can react to specific flag changes.
This generally occurs inside a database transaction.
Note that the interface to this hook is B<UNSTABLE> and it may change in the
future.
Params:
=over
=item C<bug> - The changed bug object.
=item C<timestamp> - The timestamp used for all updates in this transaction.
=item C<old_flags> - The snapshot of flag summaries from before the change.
=item C<new_flags> - The snapshot of flag summaries after the change. Call
C<my ($removed, $added) = diff_arrays(old_flags, new_flags)> to get the list of
changed flags, and search for a specific condition like C<added eq 'review-'>.
=back
=head2 install-before_final_checks
Allows execution of custom code before the final checks are done in
checksetup.pl.
Params:
=over
=item C<silent>
A flag that indicates whether or not checksetup is running in silent mode.
=back
=head2 install-requirements
Because of the way Bugzilla installation works, there can't be a normal
hook during the time that F<checksetup.pl> checks what modules are
installed. (C<Bugzilla::Hook> needs to have those modules installed--it's
a chicken-and-egg problem.)
So instead of the way hooks normally work, this hook just looks for two
subroutines (or constants, since all constants are just subroutines) in
your file, called C<OPTIONAL_MODULES> and C<REQUIRED_MODULES>,
which should return arrayrefs in the same format as C<OPTIONAL_MODULES> and
C<REQUIRED_MODULES> in L<Bugzilla::Install::Requirements>.
These subroutines will be passed an arrayref that contains the current
Bugzilla requirements of the same type, in case you want to modify
Bugzilla's requirements somehow. (Probably the most common would be to
alter a version number or the "feature" element of C<OPTIONAL_MODULES>.)
F<checksetup.pl> will add these requirements to its own.
Please remember--if you put something in C<REQUIRED_MODULES>, then
F<checksetup.pl> B<cannot complete> unless the user has that module
installed! So use C<OPTIONAL_MODULES> whenever you can.
=head2 install-update_db
This happens at the very end of all the tables being updated
during an installation or upgrade. If you need to modify your custom
schema, do it here. No params are passed.
=head2 db_schema-abstract_schema
This allows you to add tables to Bugzilla. Note that we recommend that you
prefix the names of your tables with some word, so that they don't conflict
with any future Bugzilla tables.
If you wish to add new I<columns> to existing Bugzilla tables, do that
in L</install-update_db>.
Params:
=over
=item C<schema> - A hashref, in the format of
L<Bugzilla::DB::Schema/ABSTRACT_SCHEMA>. Add new hash keys to make new table
definitions. F<checksetup.pl> will automatically add these tables to the
database when run.
=back
=head2 mailer-before_send
Called right before L<Bugzilla::Mailer> sends a message to the MTA.
Params:
=over
=item C<email> - The C<Email::MIME> object that's about to be sent.
=back
=head2 product-confirm_delete
Called before displaying the confirmation message when deleting a product.
Params:
=over
=item C<vars> - The template vars hashref.
=back
=head2 webservice
This hook allows you to add your own modules to the WebService. (See
L<Bugzilla::WebService>.)
Params:
=over
=item C<dispatch>
A hashref that you can specify the names of your modules and what Perl
module handles the functions for that module. (This is actually sent to
L<SOAP::Lite/dispatch_with>. You can see how that's used in F<xmlrpc.cgi>.)
The Perl module name must start with C<extensions::yourextension::lib::>
(replace C<yourextension> with the name of your extension). The C<package>
declaration inside that module must also start with
C<extensions::yourextension::lib::> in that module's code.
Example:
$dispatch->{Example} = "extensions::example::lib::Example";
And then you'd have a module F<extensions/example/lib/Example.pm>
It's recommended that all the keys you put in C<dispatch> start with the
name of your extension, so that you don't conflict with the standard Bugzilla
WebService functions (and so that you also don't conflict with other
plugins).
=back
=head2 webservice-error_codes
If your webservice extension throws custom errors, you can set numeric
codes for those errors here.
Extensions should use error codes above 10000, unless they are re-using
an already-existing error code.
Params:
=over
=item C<error_map>
A hash that maps the names of errors (like C<invalid_param>) to numbers.
See L<Bugzilla::WebService::Constants/WS_ERROR_CODE> for an example.
=back

View File

@@ -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.
#
# Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org>
package Bugzilla::Install;
# Functions in this this package can assume that the database
# has been set up, params are available, localconfig is
# available, and any module can be used.
#
# If you want to write an installation function that can't
# make those assumptions, then it should go into one of the
# packages under the Bugzilla::Install namespace.
use strict;
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Group;
use Bugzilla::Product;
use Bugzilla::User;
use Bugzilla::User::Setting;
use Bugzilla::Util qw(get_text);
use Bugzilla::Version;
sub SETTINGS {
return {
# 2005-03-03 travis@sedsystems.ca -- Bug 41972
display_quips => { options => ["on", "off"], default => "on" },
# 2005-03-10 travis@sedsystems.ca -- Bug 199048
comment_sort_order => { options => ["oldest_to_newest", "newest_to_oldest",
"newest_to_oldest_desc_first"],
default => "oldest_to_newest" },
# 2005-05-12 bugzilla@glob.com.au -- Bug 63536
post_bug_submit_action => { options => ["next_bug", "same_bug", "nothing"],
default => "next_bug" },
# 2005-06-29 wurblzap@gmail.com -- Bug 257767
csv_colsepchar => { options => [',',';'], default => ',' },
# 2005-10-26 wurblzap@gmail.com -- Bug 291459
zoom_textareas => { options => ["on", "off"], default => "on" },
# 2005-10-21 LpSolit@gmail.com -- Bug 313020
per_bug_queries => { options => ['on', 'off'], default => 'off' },
# 2006-05-01 olav@bkor.dhs.org -- Bug 7710
state_addselfcc => { options => ['always', 'never', 'cc_unless_role'],
default => 'cc_unless_role' },
# 2006-08-04 wurblzap@gmail.com -- Bug 322693
skin => { subclass => 'Skin', default => 'Dusk' },
# 2006-12-10 LpSolit@gmail.com -- Bug 297186
lang => { subclass => 'Lang',
default => ${Bugzilla->languages}[0] },
# 2007-07-02 altlist@gmail.com -- Bug 225731
quote_replies => { options => ['quoted_reply', 'simple_reply', 'off'],
default => "quoted_reply" },
# 2008-08-27 LpSolit@gmail.com -- Bug 182238
timezone => { subclass => 'Timezone', default => 'local' },
}
};
use constant SYSTEM_GROUPS => (
{
name => 'admin',
description => 'Administrators'
},
{
name => 'tweakparams',
description => 'Can change Parameters'
},
{
name => 'editusers',
description => 'Can edit or disable users'
},
{
name => 'creategroups',
description => 'Can create and destroy groups'
},
{
name => 'editclassifications',
description => 'Can create, destroy, and edit classifications'
},
{
name => 'editcomponents',
description => 'Can create, destroy, and edit components'
},
{
name => 'editkeywords',
description => 'Can create, destroy, and edit keywords'
},
{
name => 'editbugs',
description => 'Can edit all bug fields',
userregexp => '.*'
},
{
name => 'canconfirm',
description => 'Can confirm a bug or mark it a duplicate'
},
{
name => 'bz_canusewhines',
description => 'User can configure whine reports for self'
},
{
name => 'bz_sudoers',
description => 'Can perform actions as other users'
},
# There are also other groups created in update_system_groups.
);
use constant DEFAULT_CLASSIFICATION => {
name => 'Unclassified',
description => 'Not assigned to any classification'
};
use constant DEFAULT_PRODUCT => {
name => 'TestProduct',
description => 'This is a test product.'
. ' This ought to be blown away and replaced with real stuff in a'
. ' finished installation of bugzilla.'
};
use constant DEFAULT_COMPONENT => {
name => 'TestComponent',
description => 'This is a test component in the test product database.'
. ' This ought to be blown away and replaced with real stuff in'
. ' a finished installation of Bugzilla.'
};
sub update_settings {
my %settings = %{SETTINGS()};
foreach my $setting (keys %settings) {
add_setting($setting,
$settings{$setting}->{options},
$settings{$setting}->{default},
$settings{$setting}->{subclass});
}
}
sub update_system_groups {
my $dbh = Bugzilla->dbh;
# Create most of the system groups
foreach my $definition (SYSTEM_GROUPS) {
my $exists = new Bugzilla::Group({ name => $definition->{name} });
$definition->{isbuggroup} = 0;
Bugzilla::Group->create($definition) unless $exists;
}
# Certain groups need something done after they are created. We do
# that here.
# Make sure people who can whine at others can also whine.
if (!new Bugzilla::Group({name => 'bz_canusewhineatothers'})) {
my $whineatothers = Bugzilla::Group->create({
name => 'bz_canusewhineatothers',
description => 'Can configure whine reports for other users',
isbuggroup => 0 });
my $whine = new Bugzilla::Group({ name => 'bz_canusewhines' });
$dbh->do('INSERT INTO group_group_map (grantor_id, member_id)
VALUES (?,?)', undef, $whine->id, $whineatothers->id);
}
# Make sure sudoers are automatically protected from being sudoed.
if (!new Bugzilla::Group({name => 'bz_sudo_protect'})) {
my $sudo_protect = Bugzilla::Group->create({
name => 'bz_sudo_protect',
description => 'Can not be impersonated by other users',
isbuggroup => 0 });
my $sudo = new Bugzilla::Group({ name => 'bz_sudoers' });
$dbh->do('INSERT INTO group_group_map (grantor_id, member_id)
VALUES (?,?)', undef, $sudo_protect->id, $sudo->id);
}
# Re-evaluate all regexps, to keep them up-to-date.
my $sth = $dbh->prepare(
"SELECT profiles.userid, profiles.login_name, groups.id,
groups.userregexp, user_group_map.group_id
FROM (profiles CROSS JOIN groups)
LEFT JOIN user_group_map
ON user_group_map.user_id = profiles.userid
AND user_group_map.group_id = groups.id
AND user_group_map.grant_type = ?
WHERE userregexp != '' OR user_group_map.group_id IS NOT NULL");
my $sth_add = $dbh->prepare(
"INSERT INTO user_group_map (user_id, group_id, isbless, grant_type)
VALUES (?, ?, 0, " . GRANT_REGEXP . ")");
my $sth_del = $dbh->prepare(
"DELETE FROM user_group_map
WHERE user_id = ? AND group_id = ? AND isbless = 0
AND grant_type = " . GRANT_REGEXP);
$sth->execute(GRANT_REGEXP);
while (my ($uid, $login, $gid, $rexp, $present) = $sth->fetchrow_array()) {
if ($login =~ m/$rexp/i) {
$sth_add->execute($uid, $gid) unless $present;
} else {
$sth_del->execute($uid, $gid) if $present;
}
}
}
# This function should be called only after creating the admin user.
sub create_default_product {
my $dbh = Bugzilla->dbh;
# Make the default Classification if it doesn't already exist.
if (!$dbh->selectrow_array('SELECT 1 FROM classifications')) {
my $class = DEFAULT_CLASSIFICATION;
print get_text('install_default_classification',
{ name => $class->{name} }) . "\n";
$dbh->do('INSERT INTO classifications (name, description)
VALUES (?, ?)',
undef, $class->{name}, $class->{description});
}
# And same for the default product/component.
if (!$dbh->selectrow_array('SELECT 1 FROM products')) {
my $default_prod = DEFAULT_PRODUCT;
print get_text('install_default_product',
{ name => $default_prod->{name} }) . "\n";
$dbh->do(q{INSERT INTO products (name, description)
VALUES (?,?)},
undef, $default_prod->{name}, $default_prod->{description});
my $product = new Bugzilla::Product({name => $default_prod->{name}});
# The default version.
Bugzilla::Version::create(Bugzilla::Version::DEFAULT_VERSION, $product);
# And we automatically insert the default milestone.
$dbh->do(q{INSERT INTO milestones (product_id, value, sortkey)
SELECT id, defaultmilestone, 0
FROM products});
# Get the user who will be the owner of the Product.
# We pick the admin with the lowest id, or we insert
# an invalid "0" into the database, just so that we can
# create the component.
my $admin_group = new Bugzilla::Group({name => 'admin'});
my ($admin_id) = $dbh->selectrow_array(
'SELECT user_id FROM user_group_map WHERE group_id = ?
ORDER BY user_id ' . $dbh->sql_limit(1),
undef, $admin_group->id) || 0;
my $default_comp = DEFAULT_COMPONENT;
$dbh->do("INSERT INTO components (name, product_id, description,
initialowner)
VALUES (?, ?, ?, ?)", undef, $default_comp->{name},
$product->id, $default_comp->{description}, $admin_id);
}
}
sub create_admin {
my ($params) = @_;
my $dbh = Bugzilla->dbh;
my $template = Bugzilla->template;
my $admin_group = new Bugzilla::Group({ name => 'admin' });
my $admin_inheritors =
Bugzilla::Group->flatten_group_membership($admin_group->id);
my $admin_group_ids = join(',', @$admin_inheritors);
my ($admin_count) = $dbh->selectrow_array(
"SELECT COUNT(*) FROM user_group_map
WHERE group_id IN ($admin_group_ids)");
return if $admin_count;
my %answer = %{Bugzilla->installation_answers};
my $login = $answer{'ADMIN_EMAIL'};
my $password = $answer{'ADMIN_PASSWORD'};
my $full_name = $answer{'ADMIN_REALNAME'};
if (!$login || !$password || !$full_name) {
print "\n" . get_text('install_admin_setup') . "\n\n";
}
while (!$login) {
print get_text('install_admin_get_email') . ' ';
$login = <STDIN>;
chomp $login;
eval { Bugzilla::User->check_login_name_for_creation($login); };
if ($@) {
print $@ . "\n";
undef $login;
}
}
while (!defined $full_name) {
print get_text('install_admin_get_name') . ' ';
$full_name = <STDIN>;
chomp($full_name);
}
if (!$password) {
$password = _prompt_for_password(
get_text('install_admin_get_password'));
}
my $admin = Bugzilla::User->create({ login_name => $login,
realname => $full_name,
cryptpassword => $password });
make_admin($admin);
}
sub make_admin {
my ($user) = @_;
my $dbh = Bugzilla->dbh;
$user = ref($user) ? $user
: new Bugzilla::User(login_to_id($user, THROW_ERROR));
my $admin_group = new Bugzilla::Group({ name => 'admin' });
# Admins get explicit membership and bless capability for the admin group
$dbh->selectrow_array("SELECT id FROM groups WHERE name = 'admin'");
my $group_insert = $dbh->prepare(
'INSERT INTO user_group_map (user_id, group_id, isbless, grant_type)
VALUES (?, ?, ?, ?)');
# These are run in an eval so that we can ignore the error of somebody
# already being granted these things.
eval {
$group_insert->execute($user->id, $admin_group->id, 0, GRANT_DIRECT);
};
eval {
$group_insert->execute($user->id, $admin_group->id, 1, GRANT_DIRECT);
};
# Admins should also have editusers directly, even though they'll usually
# inherit it. People could have changed their inheritance structure.
my $editusers = new Bugzilla::Group({ name => 'editusers' });
eval {
$group_insert->execute($user->id, $editusers->id, 0, GRANT_DIRECT);
};
print "\n", get_text('install_admin_created', { user => $user }), "\n";
}
sub _prompt_for_password {
my $prompt = shift;
my $password;
while (!$password) {
# trap a few interrupts so we can fix the echo if we get aborted.
local $SIG{HUP} = \&_password_prompt_exit;
local $SIG{INT} = \&_password_prompt_exit;
local $SIG{QUIT} = \&_password_prompt_exit;
local $SIG{TERM} = \&_password_prompt_exit;
system("stty","-echo") unless ON_WINDOWS; # disable input echoing
print $prompt, ' ';
$password = <STDIN>;
chomp $password;
print "\n", get_text('install_confirm_password'), ' ';
my $pass2 = <STDIN>;
chomp $pass2;
eval { validate_password($password, $pass2); };
if ($@) {
print "\n$@\n";
undef $password;
}
system("stty","echo") unless ON_WINDOWS;
}
return $password;
}
# This is just in case we get interrupted while getting a password.
sub _password_prompt_exit {
# re-enable input echoing
system("stty","echo") unless ON_WINDOWS;
exit 1;
}
sub reset_password {
my $login = shift;
my $user = Bugzilla::User->check($login);
my $prompt = "\n" . get_text('install_reset_password', { user => $user });
my $password = _prompt_for_password($prompt);
$user->set_password($password);
$user->update();
print "\n", get_text('install_reset_password_done'), "\n";
}
1;
__END__
=head1 NAME
Bugzilla::Install - Functions and variables having to do with
installation.
=head1 SYNOPSIS
use Bugzilla::Install;
Bugzilla::Install::update_settings();
=head1 DESCRIPTION
This module is used primarily by L<checksetup.pl> during installation.
This module contains functions that deal with general installation
issues after the database is completely set up and configured.
=head1 CONSTANTS
=over
=item C<SETTINGS>
Contains information about Settings, used by L</update_settings()>.
=back
=head1 SUBROUTINES
=over
=item C<update_settings()>
Description: Adds and updates Settings for users.
Params: none
Returns: nothing.
=item C<create_default_product()>
Description: Creates the default product and classification if
they don't exist.
Params: none
Returns: nothing
=back

View File

@@ -1,251 +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 Everything Solved, Inc.
# Portions created by Everything Solved are Copyright (C) 2007
# Everything Solved, Inc. All Rights Reserved.
#
# Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org>
package Bugzilla::Install::CPAN;
use strict;
use base qw(Exporter);
our @EXPORT = qw(set_cpan_config install_module BZ_LIB);
use Bugzilla::Constants;
use Bugzilla::Install::Util qw(bin_loc install_string);
use CPAN;
use Cwd qw(abs_path);
use File::Path qw(rmtree);
use List::Util qw(shuffle);
# We need the absolute path of ext_libpath, because CPAN chdirs around
# and so we can't use a relative directory.
#
# We need it often enough (and at compile time, in install-module.pl) so
# we make it a constant.
use constant BZ_LIB => abs_path(bz_locations()->{ext_libpath});
# CPAN requires nearly all of its parameters to be set, or it will start
# asking questions to the user. We want to avoid that, so we have
# defaults here for most of the required parameters we know about, in case
# any of them aren't set. The rest are handled by set_cpan_defaults().
use constant CPAN_DEFAULTS => {
auto_commit => 0,
# We always force builds, so there's no reason to cache them.
build_cache => 0,
cache_metadata => 1,
index_expire => 1,
scan_cache => 'atstart',
inhibit_startup_message => 1,
mbuild_install_build_command => './Build',
curl => bin_loc('curl'),
gzip => bin_loc('gzip'),
links => bin_loc('links'),
lynx => bin_loc('lynx'),
make => bin_loc('make'),
pager => bin_loc('less'),
tar => bin_loc('tar'),
unzip => bin_loc('unzip'),
wget => bin_loc('wget'),
urllist => [shuffle qw(
http://cpan.pair.com/
http://mirror.hiwaay.net/CPAN/
ftp://ftp.dc.aleron.net/pub/CPAN/
http://perl.secsup.org/
http://mirrors.kernel.org/cpan/)],
};
sub install_module {
my ($name, $notest) = @_;
my $bzlib = BZ_LIB;
# Certain modules require special stuff in order to not prompt us.
my $original_makepl = $CPAN::Config->{makepl_arg};
# This one's a regex in case we're doing Template::Plugin::GD and it
# pulls in Template-Toolkit as a dependency.
if ($name =~ /^Template/) {
$CPAN::Config->{makepl_arg} .= " TT_ACCEPT=y TT_EXTRAS=n";
}
elsif ($name eq 'XML::Twig') {
$CPAN::Config->{makepl_arg} = "-n $original_makepl";
}
elsif ($name eq 'Net::LDAP') {
$CPAN::Config->{makepl_arg} .= " --skipdeps";
}
elsif ($name eq 'SOAP::Lite') {
$CPAN::Config->{makepl_arg} .= " --noprompt";
}
my $module = CPAN::Shell->expand('Module', $name);
print install_string('install_module',
{ module => $name, version => $module->cpan_version }) . "\n";
if ($notest) {
CPAN::Shell->notest('install', $name);
}
else {
CPAN::Shell->force('install', $name);
}
# If it installed any binaries in the Bugzilla directory, delete them.
if (-d "$bzlib/bin") {
File::Path::rmtree("$bzlib/bin");
}
$CPAN::Config->{makepl_arg} = $original_makepl;
}
sub set_cpan_config {
my $do_global = shift;
my $bzlib = BZ_LIB;
# We set defaults before we do anything, otherwise CPAN will
# start asking us questions as soon as we load its configuration.
eval { require CPAN::Config; };
_set_cpan_defaults();
# Calling a senseless autoload that does nothing makes us
# automatically load any existing configuration.
# We want to avoid the "invalid command" message.
open(my $saveout, ">&STDOUT");
open(STDOUT, '>/dev/null');
eval { CPAN->ignore_this_error_message_from_bugzilla; };
undef $@;
close(STDOUT);
open(STDOUT, '>&', $saveout);
my $dir = $CPAN::Config->{cpan_home};
if (!defined $dir || !-w $dir) {
# If we can't use the standard CPAN build dir, we try to make one.
$dir = "$ENV{HOME}/.cpan";
mkdir $dir;
# If we can't make one, we finally try to use the Bugzilla directory.
if (!-w $dir) {
print "WARNING: Using the Bugzilla directory as the CPAN home.\n";
$dir = "$bzlib/.cpan";
}
}
$CPAN::Config->{cpan_home} = $dir;
$CPAN::Config->{build_dir} = "$dir/build";
# We always force builds, so there's no reason to cache them.
$CPAN::Config->{keep_source_where} = "$dir/source";
# This is set both here and in defaults so that it's always true.
$CPAN::Config->{inhibit_startup_message} = 1;
# Automatically install dependencies.
$CPAN::Config->{prerequisites_policy} = 'follow';
# Unless specified, we install the modules into the Bugzilla directory.
if (!$do_global) {
$CPAN::Config->{makepl_arg} .= " LIB=\"$bzlib\""
. " INSTALLMAN1DIR=\"$bzlib/man/man1\""
. " INSTALLMAN3DIR=\"$bzlib/man/man3\""
# The bindirs are here because otherwise we'll try to write to
# the system binary dirs, and that will cause CPAN to die.
. " INSTALLBIN=\"$bzlib/bin\""
. " INSTALLSCRIPT=\"$bzlib/bin\""
# INSTALLDIRS=perl is set because that makes sure that MakeMaker
# always uses the directories we've specified here.
. " INSTALLDIRS=perl";
$CPAN::Config->{mbuild_arg} = "--install_base \"$bzlib\"";
# When we're not root, sometimes newer versions of CPAN will
# try to read/modify things that belong to root, unless we set
# certain config variables.
$CPAN::Config->{histfile} = "$dir/histfile";
$CPAN::Config->{use_sqlite} = 0;
$CPAN::Config->{prefs_dir} = "$dir/prefs";
# Unless we actually set PERL5LIB, some modules can't install
# themselves, like DBD::mysql, DBD::Pg, and XML::Twig.
my $current_lib = $ENV{PERL5LIB} ? $ENV{PERL5LIB} . ':' : '';
$ENV{PERL5LIB} = $current_lib . $bzlib;
}
}
sub _set_cpan_defaults {
# If CPAN hasn't been configured, we try to use some reasonable defaults.
foreach my $key (keys %{CPAN_DEFAULTS()}) {
$CPAN::Config->{$key} = CPAN_DEFAULTS->{$key}
if !defined $CPAN::Config->{$key};
}
my @missing;
# In newer CPANs, this is in HandleConfig. In older CPANs, it's in
# Config.
if (eval { require CPAN::HandleConfig }) {
@missing = CPAN::HandleConfig->missing_config_data;
}
else {
@missing = CPAN::Config->missing_config_data;
}
foreach my $key (@missing) {
$CPAN::Config->{$key} = '';
}
}
1;
__END__
=head1 NAME
Bugzilla::Install::CPAN - Routines to install Perl modules from CPAN.
=head1 SYNOPSIS
use Bugzilla::Install::CPAN;
set_cpan_config();
install_module('Module::Name', 1);
=head1 DESCRIPTION
This is primarily used by L<install-module> to do the "hard work" of
installing CPAN modules.
=head1 SUBROUTINES
=over
=item C<set_cpan_config>
Sets up the configuration of CPAN for this session. Must be called
before L</install_module>. Takes one boolean parameter. If true,
L</install_module> will install modules globally instead of to the
local F<lib/> directory. On most systems, you have to be root to do that.
=item C<install_module>
Installs a module from CPAN. Takes two arguments:
=over
=item C<$name> - The name of the module, just like you'd pass to the
C<install> command in the CPAN shell.
=item C<$notest> - If true, we skip running tests on this module. This
can greatly speed up the installation time.
=back
Note that calling this function prints a B<lot> of information to
STDOUT and STDERR.
=back

File diff suppressed because it is too large Load Diff

View File

@@ -1,694 +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): Max Kanat-Alexander <mkanat@bugzilla.org>
# Bill Barry <after.fallout@gmail.com>
package Bugzilla::Install::Filesystem;
# NOTE: This package may "use" any modules that it likes,
# and localconfig is available. However, all functions in this
# package should assume that:
#
# * Templates are not available.
# * Files do not have the correct permissions.
# * The database does not exist.
use strict;
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Install::Localconfig;
use Bugzilla::Util;
use File::Find;
use File::Path;
use File::Basename;
use IO::File;
use POSIX ();
use base qw(Exporter);
our @EXPORT = qw(
update_filesystem
create_htaccess
fix_all_file_permissions
);
# This looks like a constant because it effectively is, but
# it has to call other subroutines and read the current filesystem,
# so it's defined as a sub. This is not exported, so it doesn't have
# a perldoc. However, look at the various hashes defined inside this
# function to understand what it returns. (There are comments throughout.)
#
# The rationale for the file permissions is that the web server generally
# runs as apache, so the cgi scripts should not be writable for apache,
# otherwise someone may find it possible to change the cgis when exploiting
# some security flaw somewhere (not necessarily in Bugzilla!)
sub FILESYSTEM {
my $datadir = bz_locations()->{'datadir'};
my $attachdir = bz_locations()->{'attachdir'};
my $extensionsdir = bz_locations()->{'extensionsdir'};
my $webdotdir = bz_locations()->{'webdotdir'};
my $templatedir = bz_locations()->{'templatedir'};
my $libdir = bz_locations()->{'libpath'};
my $extlib = bz_locations()->{'ext_libpath'};
my $skinsdir = bz_locations()->{'skinsdir'};
my $localconfig = bz_locations()->{'localconfig'};
my $ws_group = Bugzilla->localconfig->{'webservergroup'};
# The set of permissions that we use:
# FILES
# Executable by the web server
my $ws_executable = $ws_group ? 0750 : 0755;
# Executable by the owner only.
my $owner_executable = 0700;
# Readable by the web server.
my $ws_readable = $ws_group ? 0640 : 0644;
# Readable by the owner only.
my $owner_readable = 0600;
# Writeable by the web server.
my $ws_writeable = $ws_group ? 0660 : 0666;
# DIRECTORIES
# Readable by the web server.
my $ws_dir_readable = $ws_group ? 0750 : 0755;
# Readable only by the owner.
my $owner_dir_readable = 0700;
# Writeable by the web server.
my $ws_dir_writeable = $ws_group ? 0770 : 01777;
# The web server can overwrite files owned by other users,
# in this directory.
my $ws_dir_full_control = $ws_group ? 0770 : 0777;
# Note: When being processed by checksetup, these have their permissions
# set in this order: %all_dirs, %recurse_dirs, %all_files.
#
# Each is processed in alphabetical order of keys, so shorter keys
# will have their permissions set before longer keys (thus setting
# the permissions on parent directories before setting permissions
# on their children).
# --- FILE PERMISSIONS (Non-created files) --- #
my %files = (
'*' => { perms => $ws_readable },
'*.cgi' => { perms => $ws_executable },
'whineatnews.pl' => { perms => $ws_executable },
'collectstats.pl' => { perms => $ws_executable },
'checksetup.pl' => { perms => $owner_executable },
'importxml.pl' => { perms => $ws_executable },
'runtests.pl' => { perms => $owner_executable },
'testserver.pl' => { perms => $ws_executable },
'whine.pl' => { perms => $ws_executable },
'customfield.pl' => { perms => $owner_executable },
'email_in.pl' => { perms => $ws_executable },
'sanitycheck.pl' => { perms => $ws_executable },
'jobqueue.pl' => { perms => $owner_executable },
'install-module.pl' => { perms => $owner_executable },
"$localconfig.old" => { perms => $owner_readable },
'docs/makedocs.pl' => { perms => $owner_executable },
'docs/style.css' => { perms => $ws_readable },
'docs/*/rel_notes.txt' => { perms => $ws_readable },
'docs/*/README.docs' => { perms => $owner_readable },
"$datadir/bugzilla-update.xml" => { perms => $ws_writeable },
"$datadir/params" => { perms => $ws_writeable },
"$datadir/mailer.testfile" => { perms => $ws_writeable },
);
# Directories that we want to set the perms on, but not
# recurse through. These are directories we didn't create
# in checkesetup.pl.
my %non_recurse_dirs = (
'.' => $ws_dir_readable,
docs => $ws_dir_readable,
);
# This sets the permissions for each item inside each of these
# directories, including the directory itself.
# 'CVS' directories are special, though, and are never readable by
# the webserver.
my %recurse_dirs = (
# Writeable directories
"$datadir/template" => { files => $ws_readable,
dirs => $ws_dir_full_control },
$attachdir => { files => $ws_writeable,
dirs => $ws_dir_writeable },
$webdotdir => { files => $ws_writeable,
dirs => $ws_dir_writeable },
graphs => { files => $ws_writeable,
dirs => $ws_dir_writeable },
# Readable directories
"$datadir/mining" => { files => $ws_readable,
dirs => $ws_dir_readable },
"$datadir/duplicates" => { files => $ws_readable,
dirs => $ws_dir_readable },
"$libdir/Bugzilla" => { files => $ws_readable,
dirs => $ws_dir_readable },
$extlib => { files => $ws_readable,
dirs => $ws_dir_readable },
$templatedir => { files => $ws_readable,
dirs => $ws_dir_readable },
$extensionsdir => { files => $ws_readable,
dirs => $ws_dir_readable },
images => { files => $ws_readable,
dirs => $ws_dir_readable },
css => { files => $ws_readable,
dirs => $ws_dir_readable },
js => { files => $ws_readable,
dirs => $ws_dir_readable },
$skinsdir => { files => $ws_readable,
dirs => $ws_dir_readable },
t => { files => $owner_readable,
dirs => $owner_dir_readable },
'docs/*/html' => { files => $ws_readable,
dirs => $ws_dir_readable },
'docs/*/pdf' => { files => $ws_readable,
dirs => $ws_dir_readable },
'docs/*/txt' => { files => $ws_readable,
dirs => $ws_dir_readable },
'docs/*/images' => { files => $ws_readable,
dirs => $ws_dir_readable },
'docs/lib' => { files => $owner_readable,
dirs => $owner_dir_readable },
'docs/*/xml' => { files => $owner_readable,
dirs => $owner_dir_readable },
);
# --- FILES TO CREATE --- #
# The name of each directory that we should actually *create*,
# pointing at its default permissions.
my %create_dirs = (
$datadir => $ws_dir_full_control,
"$datadir/mining" => $ws_dir_readable,
"$datadir/duplicates" => $ws_dir_readable,
$attachdir => $ws_dir_writeable,
$extensionsdir => $ws_dir_readable,
graphs => $ws_dir_writeable,
$webdotdir => $ws_dir_writeable,
"$skinsdir/custom" => $ws_dir_readable,
"$skinsdir/contrib" => $ws_dir_readable,
);
# The name of each file, pointing at its default permissions and
# default contents.
my %create_files = ();
# Each standard stylesheet has an associated custom stylesheet that
# we create. Also, we create placeholders for standard stylesheets
# for contrib skins which don't provide them themselves.
foreach my $skin_dir ("$skinsdir/custom", <$skinsdir/contrib/*>) {
next if basename($skin_dir) =~ /^cvs$/i;
$create_dirs{"$skin_dir/yui"} = $ws_dir_readable;
foreach my $base_css (<$skinsdir/standard/*.css>) {
_add_custom_css($skin_dir, basename($base_css), \%create_files, $ws_readable);
}
foreach my $dir_css (<$skinsdir/standard/*/*.css>) {
$dir_css =~ s{.+?([^/]+/[^/]+)$}{$1};
_add_custom_css($skin_dir, $dir_css, \%create_files, $ws_readable);
}
}
# Because checksetup controls the creation of index.html separately
# from all other files, it gets its very own hash.
my %index_html = (
'index.html' => { perms => $ws_readable, contents => <<EOT
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="Refresh" content="0; URL=index.cgi">
</head>
<body>
<h1>I think you are looking for <a href="index.cgi">index.cgi</a></h1>
</body>
</html>
EOT
}
);
# Because checksetup controls the .htaccess creation separately
# by a localconfig variable, these go in a separate variable from
# %create_files.
my $ht_default_deny = <<EOT;
# nothing in this directory is retrievable unless overridden by an .htaccess
# in a subdirectory
deny from all
EOT
my %htaccess = (
"$attachdir/.htaccess" => { perms => $ws_readable,
contents => $ht_default_deny },
"$libdir/Bugzilla/.htaccess" => { perms => $ws_readable,
contents => $ht_default_deny },
"$extlib/.htaccess" => { perms => $ws_readable,
contents => $ht_default_deny },
"$templatedir/.htaccess" => { perms => $ws_readable,
contents => $ht_default_deny },
'.htaccess' => { perms => $ws_readable, contents => <<EOT
# Don't allow people to retrieve non-cgi executable files or our private data
<FilesMatch ^(.*\\.pm|.*\\.pl|.*localconfig.*)\$>
deny from all
</FilesMatch>
EOT
},
"$webdotdir/.htaccess" => { perms => $ws_readable, contents => <<EOT
# Restrict access to .dot files to the public webdot server at research.att.com
# if research.att.com ever changes their IP, or if you use a different
# webdot server, you'll need to edit this
<FilesMatch \\.dot\$>
Allow from 192.20.225.0/24
Deny from all
</FilesMatch>
# Allow access to .png files created by a local copy of 'dot'
<FilesMatch \\.png\$>
Allow from all
</FilesMatch>
# And no directory listings, either.
Deny from all
EOT
},
# Even though $datadir may not (and should not) be accessible from the
# web server, we can't know for sure, so create the .htaccess anyway.
# It's harmless if it isn't accessible...
"$datadir/.htaccess" => { perms => $ws_readable, contents => <<EOT
# Nothing in this directory is retrievable unless overridden by an .htaccess
# in a subdirectory.
deny from all
EOT
},
);
my %all_files = (%create_files, %htaccess, %index_html, %files);
my %all_dirs = (%create_dirs, %non_recurse_dirs);
return {
create_dirs => \%create_dirs,
recurse_dirs => \%recurse_dirs,
all_dirs => \%all_dirs,
create_files => \%create_files,
htaccess => \%htaccess,
index_html => \%index_html,
all_files => \%all_files,
};
}
sub update_filesystem {
my ($params) = @_;
my $fs = FILESYSTEM();
my %dirs = %{$fs->{create_dirs}};
my %files = %{$fs->{create_files}};
my $datadir = bz_locations->{'datadir'};
# If the graphs/ directory doesn't exist, we're upgrading from
# a version old enough that we need to update the $datadir/mining
# format.
if (-d "$datadir/mining" && !-d 'graphs') {
_update_old_charts($datadir);
}
# By sorting the dirs, we assure that shorter-named directories
# (meaning parent directories) are always created before their
# child directories.
foreach my $dir (sort keys %dirs) {
unless (-d $dir) {
print "Creating $dir directory...\n";
mkdir $dir || die $!;
# For some reason, passing in the permissions to "mkdir"
# doesn't work right, but doing a "chmod" does.
chmod $dirs{$dir}, $dir || die $!;
}
}
_create_files(%files);
if ($params->{index_html}) {
_create_files(%{$fs->{index_html}});
}
elsif (-e 'index.html') {
my $templatedir = bz_locations()->{'templatedir'};
print <<EOT;
*** It appears that you still have an old index.html hanging around.
Either the contents of this file should be moved into a template and
placed in the '$templatedir/en/custom' directory, or you should delete
the file.
EOT
}
# Delete old files that no longer need to exist
# 2001-04-29 jake@bugzilla.org - Remove oldemailtech
# http://bugzilla.mozilla.org/show_bugs.cgi?id=71552
if (-d 'shadow') {
print "Removing shadow directory...\n";
rmtree("shadow");
}
if (-e "$datadir/versioncache") {
print "Removing versioncache...\n";
unlink "$datadir/versioncache";
}
if (-e "$datadir/duplicates.rdf") {
print "Removing duplicates.rdf...\n";
unlink "$datadir/duplicates.rdf";
unlink "$datadir/duplicates-old.rdf";
}
}
# A simple helper for creating "empty" CSS files.
sub _add_custom_css {
my ($skin_dir, $path, $create_files, $perms) = @_;
$create_files->{"$skin_dir/$path"} = { perms => $perms, contents => <<EOT
/*
* Custom rules for $path.
* The rules you put here override rules in that stylesheet.
*/
EOT
};
}
sub create_htaccess {
_create_files(%{FILESYSTEM()->{htaccess}});
# Repair old .htaccess files
my $htaccess = new IO::File('.htaccess', 'r') || die ".htaccess: $!";
my $old_data;
{ local $/; $old_data = <$htaccess>; }
$htaccess->close;
my $repaired = 0;
if ($old_data =~ s/\|localconfig\|/\|.*localconfig.*\|/) {
$repaired = 1;
}
if ($old_data !~ /\(\.\*\\\.pm\|/) {
$old_data =~ s/\(/(.*\\.pm\|/;
$repaired = 1;
}
if ($repaired) {
print "Repairing .htaccess...\n";
$htaccess = new IO::File('.htaccess', 'w') || die $!;
print $htaccess $old_data;
$htaccess->close;
}
my $webdot_dir = bz_locations()->{'webdotdir'};
# The public webdot IP address changed.
my $webdot = new IO::File("$webdot_dir/.htaccess", 'r')
|| die "$webdot_dir/.htaccess: $!";
my $webdot_data;
{ local $/; $webdot_data = <$webdot>; }
$webdot->close;
if ($webdot_data =~ /192\.20\.225\.10/) {
print "Repairing $webdot_dir/.htaccess...\n";
$webdot_data =~ s/192\.20\.225\.10/192.20.225.0\/24/g;
$webdot = new IO::File("$webdot_dir/.htaccess", 'w') || die $!;
print $webdot $webdot_data;
$webdot->close;
}
}
# A helper for the above functions.
sub _create_files {
my (%files) = @_;
# It's not necessary to sort these, but it does make the
# output of checksetup.pl look a bit nicer.
foreach my $file (sort keys %files) {
unless (-e $file) {
print "Creating $file...\n";
my $info = $files{$file};
my $fh = new IO::File($file, O_WRONLY | O_CREAT, $info->{perms})
|| die $!;
print $fh $info->{contents} if $info->{contents};
$fh->close;
}
}
}
# If you ran a REALLY old version of Bugzilla, your chart files are in the
# wrong format. This code is a little messy, because it's very old, and
# when moving it into this module, I couldn't test it so I left it almost
# completely alone.
sub _update_old_charts {
my ($datadir) = @_;
print "Updating old chart storage format...\n";
foreach my $in_file (glob("$datadir/mining/*")) {
# Don't try and upgrade image or db files!
next if (($in_file =~ /\.gif$/i) ||
($in_file =~ /\.png$/i) ||
($in_file =~ /\.db$/i) ||
($in_file =~ /\.orig$/i));
rename("$in_file", "$in_file.orig") or next;
open(IN, "$in_file.orig") or next;
open(OUT, '>', $in_file) or next;
# Fields in the header
my @declared_fields;
# Fields we changed to half way through by mistake
# This list comes from an old version of collectstats.pl
# This part is only for people who ran later versions of 2.11 (devel)
my @intermediate_fields = qw(DATE UNCONFIRMED NEW ASSIGNED REOPENED
RESOLVED VERIFIED CLOSED);
# Fields we actually want (matches the current collectstats.pl)
my @out_fields = qw(DATE NEW ASSIGNED REOPENED UNCONFIRMED RESOLVED
VERIFIED CLOSED FIXED INVALID WONTFIX LATER REMIND
DUPLICATE WORKSFORME MOVED);
while (<IN>) {
if (/^# fields?: (.*)\s$/) {
@declared_fields = map uc, (split /\||\r/, $1);
print OUT "# fields: ", join('|', @out_fields), "\n";
}
elsif (/^(\d+\|.*)/) {
my @data = split(/\||\r/, $1);
my %data;
if (@data == @declared_fields) {
# old format
for my $i (0 .. $#declared_fields) {
$data{$declared_fields[$i]} = $data[$i];
}
}
elsif (@data == @intermediate_fields) {
# Must have changed over at this point
for my $i (0 .. $#intermediate_fields) {
$data{$intermediate_fields[$i]} = $data[$i];
}
}
elsif (@data == @out_fields) {
# This line's fine - it has the right number of entries
for my $i (0 .. $#out_fields) {
$data{$out_fields[$i]} = $data[$i];
}
}
else {
print "Oh dear, input line $. of $in_file had " .
scalar(@data) . " fields\nThis was unexpected.",
" You may want to check your data files.\n";
}
print OUT join('|',
map { defined ($data{$_}) ? ($data{$_}) : "" } @out_fields),
"\n";
}
else {
print OUT;
}
}
close(IN);
close(OUT);
}
}
sub fix_all_file_permissions {
my ($output) = @_;
my $ws_group = Bugzilla->localconfig->{'webservergroup'};
my $group_id = _check_web_server_group($ws_group, $output);
return if ON_WINDOWS;
my $fs = FILESYSTEM();
my %files = %{$fs->{all_files}};
my %dirs = %{$fs->{all_dirs}};
my %recurse_dirs = %{$fs->{recurse_dirs}};
print get_text('install_file_perms_fix') . "\n" if $output;
my $owner_id = POSIX::getuid();
$group_id = POSIX::getgid() unless defined $group_id;
foreach my $dir (sort keys %dirs) {
next unless -d $dir;
_fix_perms($dir, $owner_id, $group_id, $dirs{$dir});
}
foreach my $dir (sort keys %recurse_dirs) {
next unless -d $dir;
# Set permissions on the directory itself.
my $perms = $recurse_dirs{$dir};
_fix_perms($dir, $owner_id, $group_id, $perms->{dirs});
# Now recurse through the directory and set the correct permissions
# on subdirectories and files.
find({ no_chdir => 1, wanted => sub {
my $name = $File::Find::name;
if (-d $name) {
_fix_perms($name, $owner_id, $group_id, $perms->{dirs});
}
else {
_fix_perms($name, $owner_id, $group_id, $perms->{files});
}
}}, $dir);
}
foreach my $file (sort keys %files) {
# %files supports globs
foreach my $filename (glob $file) {
# Don't touch directories.
next if -d $filename || !-e $filename;
_fix_perms($filename, $owner_id, $group_id,
$files{$file}->{perms});
}
}
_fix_cvs_dirs($owner_id, '.');
}
# A helper for fix_all_file_permissions
sub _fix_cvs_dirs {
my ($owner_id, $dir) = @_;
my $owner_gid = POSIX::getgid();
find({ no_chdir => 1, wanted => sub {
my $name = $File::Find::name;
if ($File::Find::dir =~ /\/CVS/ || $_ eq '.cvsignore'
|| (-d $name && $_ eq 'CVS')) {
_fix_perms($name, $owner_id, $owner_gid, 0700);
}
}}, $dir);
}
sub _fix_perms {
my ($name, $owner, $group, $perms) = @_;
#printf ("Changing $name to %o\n", $perms);
chown $owner, $group, $name
|| warn "Failed to change ownership of $name: $!";
chmod $perms, $name
|| warn "Failed to change permissions of $name: $!";
}
sub _check_web_server_group {
my ($group, $output) = @_;
my $filename = bz_locations()->{'localconfig'};
my $group_id;
# If we are on Windows, webservergroup does nothing
if (ON_WINDOWS && $group && $output) {
print "\n\n" . get_text('install_webservergroup_windows') . "\n\n";
}
# If we're not on Windows, make sure that webservergroup isn't
# empty.
elsif (!ON_WINDOWS && !$group && $output) {
print "\n\n" . get_text('install_webservergroup_empty') . "\n\n";
}
# If we're not on Windows, make sure we are actually a member of
# the webservergroup.
elsif (!ON_WINDOWS && $group) {
$group_id = getgrnam($group);
ThrowCodeError('invalid_webservergroup', { group => $group })
unless defined $group_id;
# If on unix, see if we need to print a warning about a webservergroup
# that we can't chgrp to
if ($output && $< != 0 && !grep($_ eq $group_id, split(" ", $)))) {
print "\n\n" . get_text('install_webservergroup_not_in') . "\n\n";
}
}
return $group_id;
}
1;
__END__
=head1 NAME
Bugzilla::Install::Filesystem - Fix up the filesystem during
installation.
=head1 DESCRIPTION
This module is used primarily by L<checksetup.pl> to modify the
filesystem during installation, including creating the data/ directory.
=head1 SUBROUTINES
=over
=item C<update_filesystem({ index_html => 0 })>
Description: Creates all the directories and files that Bugzilla
needs to function but doesn't ship with. Also does
any updates to these files as necessary during an
upgrade.
Params: C<index_html> - Whether or not we should create
the F<index.html> file.
Returns: nothing
=item C<create_htaccess()>
Description: Creates all of the .htaccess files for Apache,
in the various Bugzilla directories. Also updates
the .htaccess files if they need updating.
Params: none
Returns: nothing
=item C<fix_all_file_permissions($output)>
Description: Sets all the file permissions on all of Bugzilla's files
to what they should be. Note that permissions are different
depending on whether or not C<$webservergroup> is set
in F<localconfig>.
Params: C<$output> - C<true> if you want this function to print
out information about what it's doing.
Returns: nothing
=back

View File

@@ -1,484 +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 Initial Developer of the Original Code is Everything Solved.
# Portions created by Everything Solved are Copyright (C) 2006
# Everything Solved. All Rights Reserved.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org>
package Bugzilla::Install::Localconfig;
# NOTE: This package may "use" any modules that it likes. However,
# all functions in this package should assume that:
#
# * The data/ directory does not exist.
# * Templates are not available.
# * Files do not have the correct permissions
# * The database is not up to date
use strict;
use Bugzilla::Constants;
use Bugzilla::Install::Util qw(bin_loc);
use Bugzilla::Util qw(generate_random_password);
use Data::Dumper;
use File::Basename qw(dirname);
use IO::File;
use Safe;
use base qw(Exporter);
our @EXPORT_OK = qw(
read_localconfig
update_localconfig
);
use constant LOCALCONFIG_VARS => (
{
name => 'create_htaccess',
default => 1,
desc => <<EOT
# If you are using Apache as your web server, Bugzilla can create .htaccess
# files for you that will instruct Apache not to serve files that shouldn't
# be accessed from the web browser (like your local configuration data and non-cgi
# executable files). For this to work, the directory your Bugzilla
# installation is in must be within the jurisdiction of a <Directory> block
# in the httpd.conf file that has 'AllowOverride Limit' in it. If it has
# 'AllowOverride All' or other options with Limit, that's fine.
# (Older Apache installations may use an access.conf file to store these
# <Directory> blocks.)
# If this is set to 1, Bugzilla will create these files if they don't exist.
# If this is set to 0, Bugzilla will not create these files.
EOT
},
{
name => 'webservergroup',
default => ON_WINDOWS ? '' : 'apache',
desc => q{# This is the group your web server runs as.
# If you have a Windows box, ignore this setting.
# If you do not have access to the group your web server runs under,
# set this to "". If you do set this to "", then your Bugzilla installation
# will be _VERY_ insecure, because some files will be world readable/writable,
# and so anyone who can get local access to your machine can do whatever they
# want. You should only have this set to "" if this is a testing installation
# and you cannot set this up any other way. YOU HAVE BEEN WARNED!
# If you set this to anything other than "", you will need to run checksetup.pl
# as} . ROOT_USER . qq{, or as a user who is a member of the specified group.\n}
},
{
name => 'db_driver',
default => 'mysql',
desc => <<EOT
# What SQL database to use. Default is mysql. List of supported databases
# can be obtained by listing Bugzilla/DB directory - every module corresponds
# to one supported database and the name corresponds to a driver name.
EOT
},
{
name => 'db_host',
default => 'localhost',
desc =>
"# The DNS name of the host that the database server runs on.\n"
},
{
name => 'db_name',
default => 'bugs',
desc => "# The name of the database\n"
},
{
name => 'db_user',
default => 'bugs',
desc => "# Who we connect to the database as.\n"
},
{
name => 'db_pass',
default => '',
desc => <<EOT
# Enter your database password here. It's normally advisable to specify
# a password for your bugzilla database user.
# If you use apostrophe (') or a backslash (\\) in your password, you'll
# need to escape it by preceding it with a '\\' character. (\\') or (\\)
# (Far simpler just not to use those characters.)
EOT
},
{
name => 'db_port',
default => 0,
desc => <<EOT
# Sometimes the database server is running on a non-standard port. If that's
# the case for your database server, set this to the port number that your
# database server is running on. Setting this to 0 means "use the default
# port for my database server."
EOT
},
{
name => 'db_sock',
default => '',
desc => <<EOT
# MySQL Only: Enter a path to the unix socket for MySQL. If this is
# blank, then MySQL's compiled-in default will be used. You probably
# want that.
EOT
},
{
name => 'db_check',
default => 1,
desc => <<EOT
# Should checksetup.pl try to verify that your database setup is correct?
# (with some combinations of database servers/Perl modules/moonphase this
# doesn't work)
EOT
},
{
name => 'index_html',
default => 0,
desc => <<EOT
# With the introduction of a configurable index page using the
# template toolkit, Bugzilla's main index page is now index.cgi.
# Most web servers will allow you to use index.cgi as a directory
# index, and many come preconfigured that way, but if yours doesn't
# then you'll need an index.html file that provides redirection
# to index.cgi. Setting \$index_html to 1 below will allow
# checksetup.pl to create one for you if it doesn't exist.
# NOTE: checksetup.pl will not replace an existing file, so if you
# wish to have checksetup.pl create one for you, you must
# make sure that index.html doesn't already exist
EOT
},
{
name => 'cvsbin',
default => \&_get_default_cvsbin,
desc => <<EOT
# For some optional functions of Bugzilla (such as the pretty-print patch
# viewer), we need the cvs binary to access files and revisions.
# Because it's possible that this program is not in your path, you can specify
# its location here. Please specify the full path to the executable.
EOT
},
{
name => 'interdiffbin',
default => \&_get_default_interdiffbin,
desc => <<EOT
# For some optional functions of Bugzilla (such as the pretty-print patch
# viewer), we need the interdiff binary to make diffs between two patches.
# Because it's possible that this program is not in your path, you can specify
# its location here. Please specify the full path to the executable.
EOT
},
{
name => 'diffpath',
default => \&_get_default_diffpath,
desc => <<EOT
# The interdiff feature needs diff, so we have to have that path.
# Please specify the directory name only; do not use trailing slash.
EOT
},
{
name => 'site_wide_secret',
default => sub { generate_random_password(256) },
desc => <<EOT
# This secret key is used by your installation for the creation and
# validation of encrypted tokens to prevent unsolicited changes,
# such as bug changes. A random string is generated by default.
# It's very important that this key is kept secret. It also must be
# very long.
EOT
},
);
sub read_localconfig {
my ($include_deprecated) = @_;
my $filename = bz_locations()->{'localconfig'};
my %localconfig;
if (-e $filename) {
my $s = new Safe;
# Some people like to store their database password in another file.
$s->permit('dofile');
$s->rdo($filename);
if ($@ || $!) {
my $err_msg = $@ ? $@ : $!;
die <<EOT;
An error has occurred while reading your 'localconfig' file. The text of
the error message is:
$err_msg
Please fix the error in your 'localconfig' file. Alternately, rename your
'localconfig' file, rerun checksetup.pl, and re-enter your answers.
\$ mv -f localconfig localconfig.old
\$ ./checksetup.pl
EOT
}
my @read_symbols;
if ($include_deprecated) {
# First we have to get the whole symbol table
my $safe_root = $s->root;
my %safe_package;
{ no strict 'refs'; %safe_package = %{$safe_root . "::"}; }
# And now we read the contents of every var in the symbol table.
# However:
# * We only include symbols that start with an alphanumeric
# character. This excludes symbols like "_<./localconfig"
# that show up in some perls.
# * We ignore the INC symbol, which exists in every package.
# * Perl 5.10 imports a lot of random symbols that all
# contain "::", and we want to ignore those.
@read_symbols = grep { /^[A-Za-z0-1]/ and !/^INC$/ and !/::/ }
(keys %safe_package);
}
else {
@read_symbols = map($_->{name}, LOCALCONFIG_VARS);
}
foreach my $var (@read_symbols) {
my $glob = $s->varglob($var);
# We can't get the type of a variable out of a Safe automatically.
# We can only get the glob itself. So we figure out its type this
# way, by trying first a scalar, then an array, then a hash.
#
# The interesting thing is that this converts all deprecated
# array or hash vars into hashrefs or arrayrefs, but that's
# fine since as I write this all modern localconfig vars are
# actually scalars.
if (defined $$glob) {
$localconfig{$var} = $$glob;
}
elsif (defined @$glob) {
$localconfig{$var} = \@$glob;
}
elsif (defined %$glob) {
$localconfig{$var} = \%$glob;
}
}
}
return \%localconfig;
}
#
# This is quite tricky. But fun!
#
# First we read the file 'localconfig'. Then we check if the variables we
# need are defined. If not, we will append the new settings to
# localconfig, instruct the user to check them, and stop.
#
# Why do it this way?
#
# Assume we will enhance Bugzilla and eventually more local configuration
# stuff arises on the horizon.
#
# But the file 'localconfig' is not in the Bugzilla CVS or tarfile. You
# know, we never want to overwrite your own version of 'localconfig', so
# we can't put it into the CVS/tarfile, can we?
#
# Now, when we need a new variable, we simply add the necessary stuff to
# LOCALCONFIG_VARS. When the user gets the new version of Bugzilla from CVS and
# runs checksetup, it finds out "Oh, there is something new". Then it adds
# some default value to the user's local setup and informs the user to
# check that to see if it is what the user wants.
#
# Cute, ey?
#
sub update_localconfig {
my ($params) = @_;
my $output = $params->{output} || 0;
my $answer = Bugzilla->installation_answers;
my $localconfig = read_localconfig('include deprecated');
my @new_vars;
foreach my $var (LOCALCONFIG_VARS) {
my $name = $var->{name};
if (!defined $localconfig->{$name}) {
push(@new_vars, $name);
$var->{default} = &{$var->{default}} if ref($var->{default}) eq 'CODE';
$localconfig->{$name} = $answer->{$name} || $var->{default};
}
}
if (!$localconfig->{'interdiffbin'} && $output) {
print <<EOT
OPTIONAL NOTE: If you want to be able to use the 'difference between two
patches' feature of Bugzilla (which requires the PatchReader Perl module
as well), you should install patchutils from:
http://cyberelk.net/tim/patchutils/
EOT
}
my @old_vars;
foreach my $var (keys %$localconfig) {
push(@old_vars, $var) if !grep($_->{name} eq $var, LOCALCONFIG_VARS);
}
my $filename = bz_locations->{'localconfig'};
# Move any custom or old variables into a separate file.
if (scalar @old_vars) {
my $filename_old = "$filename.old";
open(my $old_file, ">>$filename_old") || die "$filename_old: $!";
local $Data::Dumper::Purity = 1;
foreach my $var (@old_vars) {
print $old_file Data::Dumper->Dump([$localconfig->{$var}],
["*$var"]) . "\n\n";
}
close $old_file;
my $oldstuff = join(', ', @old_vars);
print <<EOT
The following variables are no longer used in $filename, and
have been moved to $filename_old: $oldstuff
EOT
}
# Re-write localconfig
open(my $fh, ">$filename") || die "$filename: $!";
foreach my $var (LOCALCONFIG_VARS) {
print $fh "\n", $var->{desc},
Data::Dumper->Dump([$localconfig->{$var->{name}}],
["*$var->{name}"]);
}
if (@new_vars) {
my $newstuff = join(', ', @new_vars);
print <<EOT;
This version of Bugzilla contains some variables that you may want to
change and adapt to your local settings. Please edit the file
$filename and rerun checksetup.pl.
The following variables are new to $filename since you last ran
checksetup.pl: $newstuff
EOT
exit;
}
# Reset the cache for Bugzilla->localconfig so that it will be re-read
delete Bugzilla->request_cache->{localconfig};
return { old_vars => \@old_vars, new_vars => \@new_vars };
}
sub _get_default_cvsbin { return bin_loc('cvs') }
sub _get_default_interdiffbin { return bin_loc('interdiff') }
sub _get_default_diffpath {
my $diff_bin = bin_loc('diff');
return dirname($diff_bin);
}
1;
__END__
=head1 NAME
Bugzilla::Install::Localconfig - Functions and variables dealing
with the manipulation and creation of the F<localconfig> file.
=head1 SYNOPSIS
use Bugzilla::Install::Requirements qw(update_localconfig);
update_localconfig({ output => 1 });
=head1 DESCRIPTION
This module is used primarily by L<checksetup.pl> to create and
modify the localconfig file. Most scripts should use L<Bugzilla/localconfig>
to access localconfig variables.
=head1 CONSTANTS
=over
=item C<LOCALCONFIG_VARS>
An array of hashrefs. These hashrefs contain three keys:
name - The name of the variable.
default - The default value for the variable. Should always be
something that can fit in a scalar.
desc - Additional text to put in localconfig before the variable
definition. Must end in a newline. Each line should start
with "#" unless you have some REALLY good reason not
to do that.
=item C<OLD_LOCALCONFIG_VARS>
An array of names of variables. If C<update_localconfig> finds these
variables defined in localconfig, it will print out a warning.
=back
=head1 SUBROUTINES
=over
=item C<read_localconfig>
=over
=item B<Description>
Reads the localconfig file and returns all valid values in a hashref.
=item B<Params>
=over
=item C<$include_deprecated>
C<true> if you want the returned hashref to include *any* variable
currently defined in localconfig, even if it doesn't exist in
C<LOCALCONFIG_VARS>. Generally this is is only for use
by L</update_localconfig>.
=back
=item B<Returns>
A hashref of the localconfig variables. If an array is defined in
localconfig, it will be an arrayref in the returned hash. If a
hash is defined, it will be a hashref in the returned hash.
Only includes variables specified in C<LOCALCONFIG_VARS>, unless
C<$include_deprecated> is true.
=back
=item C<update_localconfig>
Description: Adds any new variables to localconfig that aren't
currently defined there. Also optionally prints out
a message about vars that *should* be there and aren't.
Exits the program if it adds any new vars.
Params: C<$output> - C<true> if the function should display informational
output and warnings. It will always display errors or
any message which would cause program execution to halt.
Returns: A hashref, with C<old_vals> being an array of names of variables
that were removed, and C<new_vals> being an array of names
of variables that were added to localconfig.
=back

View File

@@ -1,682 +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): Max Kanat-Alexander <mkanat@bugzilla.org>
# Marc Schumann <wurblzap@gmail.com>
package Bugzilla::Install::Requirements;
# NOTE: This package MUST NOT "use" any Bugzilla modules other than
# Bugzilla::Constants, anywhere. We may "use" standard perl modules.
#
# Subroutines may "require" and "import" from modules, but they
# MUST NOT "use."
use strict;
use Bugzilla::Constants;
use Bugzilla::Install::Util qw(vers_cmp install_string);
use List::Util qw(max);
use Safe;
use base qw(Exporter);
our @EXPORT = qw(
REQUIRED_MODULES
OPTIONAL_MODULES
check_requirements
check_graphviz
have_vers
install_command
);
# This is how many *'s are in the top of each "box" message printed
# by checksetup.pl.
use constant TABLE_WIDTH => 71;
# The below two constants are subroutines so that they can implement
# a hook. Other than that they are actually constants.
# "package" is the perl package we're checking for. "module" is the name
# of the actual module we load with "require" to see if the package is
# installed or not. "version" is the version we need, or 0 if we'll accept
# any version.
#
# "blacklist" is an arrayref of regular expressions that describe versions that
# are 'blacklisted'--that is, even if the version is high enough, Bugzilla
# will refuse to say that it's OK to run with that version.
sub REQUIRED_MODULES {
my $perl_ver = sprintf('%vd', $^V);
my @modules = (
{
package => 'CGI.pm',
module => 'CGI',
# Perl 5.10 requires CGI 3.33 due to a taint issue when
# uploading attachments, see bug 416382.
# Require CGI 3.21 for -httponly support, see bug 368502.
version => (vers_cmp($perl_ver, '5.10') > -1) ? '3.33' : '3.21'
},
{
package => 'Digest-SHA',
module => 'Digest::SHA',
version => 0
},
{
package => 'TimeDate',
module => 'Date::Format',
version => '2.21'
},
# 0.28 fixed some important bugs in DateTime.
{
package => 'DateTime',
module => 'DateTime',
version => '0.28'
},
# 0.79 is required to work on Windows Vista and Windows Server 2008.
# As correctly detecting the flavor of Windows is not easy,
# we require this version for all Windows installations.
# 0.71 fixes a major bug affecting all platforms.
{
package => 'DateTime-TimeZone',
module => 'DateTime::TimeZone',
version => ON_WINDOWS ? '0.79' : '0.71'
},
{
package => 'PathTools',
module => 'File::Spec',
version => '0.84'
},
{
package => 'DBI',
module => 'DBI',
version => '1.41'
},
# 2.22 fixes various problems related to UTF8 strings in hash keys,
# as well as line endings on Windows.
{
package => 'Template-Toolkit',
module => 'Template',
version => '2.22'
},
{
package => 'Email-Send',
module => 'Email::Send',
version => ON_WINDOWS ? '2.16' : '2.00',
blacklist => ['^2\.196$']
},
{
package => 'Email-MIME',
module => 'Email::MIME',
version => '1.861'
},
{
package => 'Email-MIME-Encodings',
module => 'Email::MIME::Encodings',
# Fixes bug 486206
version => '1.313',
},
{
package => 'Email-MIME-Modifier',
module => 'Email::MIME::Modifier',
version => '1.442'
},
{
package => 'URI',
module => 'URI',
version => 0
},
);
my $all_modules = _get_extension_requirements(
'REQUIRED_MODULES', \@modules);
return $all_modules;
};
sub OPTIONAL_MODULES {
my @modules = (
{
package => 'GD',
module => 'GD',
version => '1.20',
feature => 'Graphical Reports, New Charts, Old Charts'
},
{
package => 'Chart',
module => 'Chart::Base',
version => '1.0',
feature => 'New Charts, Old Charts'
},
{
package => 'Template-GD',
# This module tells us whether or not Template-GD is installed
# on Template-Toolkits after 2.14, and still works with 2.14 and lower.
module => 'Template::Plugin::GD::Image',
version => 0,
feature => 'Graphical Reports'
},
{
package => 'GDTextUtil',
module => 'GD::Text',
version => 0,
feature => 'Graphical Reports'
},
{
package => 'GDGraph',
module => 'GD::Graph',
version => 0,
feature => 'Graphical Reports'
},
{
package => 'XML-Twig',
module => 'XML::Twig',
version => 0,
feature => 'Move Bugs Between Installations'
},
{
package => 'MIME-tools',
# MIME::Parser is packaged as MIME::Tools on ActiveState Perl
module => ON_WINDOWS ? 'MIME::Tools' : 'MIME::Parser',
version => '5.406',
feature => 'Move Bugs Between Installations'
},
{
package => 'libwww-perl',
module => 'LWP::UserAgent',
version => 0,
feature => 'Automatic Update Notifications'
},
{
package => 'PatchReader',
module => 'PatchReader',
version => '0.9.4',
feature => 'Patch Viewer'
},
{
package => 'PerlMagick',
module => 'Image::Magick',
version => 0,
feature => 'Optionally Convert BMP Attachments to PNGs'
},
{
package => 'perl-ldap',
module => 'Net::LDAP',
version => 0,
feature => 'LDAP Authentication'
},
{
package => 'Authen-SASL',
module => 'Authen::SASL',
version => 0,
feature => 'SMTP Authentication'
},
{
package => 'RadiusPerl',
module => 'Authen::Radius',
version => 0,
feature => 'RADIUS Authentication'
},
{
package => 'SOAP-Lite',
module => 'SOAP::Lite',
version => 0,
# These versions (0.70 -> 0.710.05) are affected by bug 468009
blacklist => ['^0\.70', '^0\.710?\.0[1-5]$'],
feature => 'XML-RPC Interface'
},
{
# We need the 'utf8_mode' method of HTML::Parser, for HTML::Scrubber.
package => 'HTML-Parser',
module => 'HTML::Parser',
version => '3.40',
feature => 'More HTML in Product/Group Descriptions'
},
{
package => 'HTML-Scrubber',
module => 'HTML::Scrubber',
version => 0,
feature => 'More HTML in Product/Group Descriptions'
},
# Inbound Email
{
package => 'Email-MIME-Attachment-Stripper',
module => 'Email::MIME::Attachment::Stripper',
version => 0,
feature => 'Inbound Email'
},
{
package => 'Email-Reply',
module => 'Email::Reply',
version => 0,
feature => 'Inbound Email'
},
# Mail Queueing
{
package => 'TheSchwartz',
module => 'TheSchwartz',
version => 0,
feature => 'Mail Queueing',
},
{
package => 'Daemon-Generic',
module => 'Daemon::Generic',
version => 0,
feature => 'Mail Queueing',
},
# mod_perl
{
package => 'mod_perl',
module => 'mod_perl2',
version => '1.999022',
feature => 'mod_perl'
},
);
my $all_modules = _get_extension_requirements(
'OPTIONAL_MODULES', \@modules);
return $all_modules;
};
# This implements the install-requirements hook described in Bugzilla::Hook.
sub _get_extension_requirements {
my ($function, $base_modules) = @_;
my @all_modules;
# get a list of all extensions
my @extensions = glob(bz_locations()->{'extensionsdir'} . "/*");
foreach my $extension (@extensions) {
my $file = "$extension/code/install-requirements.pl";
if (-e $file) {
my $safe = new Safe;
# This is a very liberal Safe.
$safe->permit(qw(:browse require entereval caller));
$safe->rdo($file);
if ($@) {
warn $@;
next;
}
my $modules = eval { &{$safe->varglob($function)}($base_modules) };
next unless $modules;
push(@all_modules, @$modules);
}
}
unshift(@all_modules, @$base_modules);
return \@all_modules;
};
sub check_requirements {
my ($output) = @_;
print "\n", install_string('checking_modules'), "\n" if $output;
my $root = ROOT_USER;
my $missing = _check_missing(REQUIRED_MODULES, $output);
print "\n", install_string('checking_dbd'), "\n" if $output;
my $have_one_dbd = 0;
my $db_modules = DB_MODULE;
foreach my $db (keys %$db_modules) {
my $dbd = $db_modules->{$db}->{dbd};
$have_one_dbd = 1 if have_vers($dbd, $output);
}
print "\n", install_string('checking_optional'), "\n" if $output;
my $missing_optional = _check_missing(OPTIONAL_MODULES, $output);
# If we're running on Windows, reset the input line terminator so that
# console input works properly - loading CGI tends to mess it up
$/ = "\015\012" if ON_WINDOWS;
my $pass = !scalar(@$missing) && $have_one_dbd;
return {
pass => $pass,
one_dbd => $have_one_dbd,
missing => $missing,
optional => $missing_optional,
any_missing => !$pass || scalar(@$missing_optional),
};
}
# A helper for check_requirements
sub _check_missing {
my ($modules, $output) = @_;
my @missing;
foreach my $module (@$modules) {
unless (have_vers($module, $output)) {
push(@missing, $module);
}
}
return \@missing;
}
# Returns the build ID of ActivePerl. If several versions of
# ActivePerl are installed, it won't be able to know which one
# you are currently running. But that's our best guess.
sub _get_activestate_build_id {
eval 'use Win32::TieRegistry';
return 0 if $@;
my $key = Win32::TieRegistry->new('LMachine\Software\ActiveState\ActivePerl')
or return 0;
return $key->GetValue("CurrentVersion");
}
sub print_module_instructions {
my ($check_results, $output) = @_;
# First we print the long explanatory messages.
if (scalar @{$check_results->{missing}}) {
print install_string('modules_message_required');
}
if (!$check_results->{one_dbd}) {
print install_string('modules_message_db');
}
if (my @missing = @{$check_results->{optional}} and $output) {
print install_string('modules_message_optional');
# Now we have to determine how large the table cols will be.
my $longest_name = max(map(length($_->{package}), @missing));
# The first column header is at least 11 characters long.
$longest_name = 11 if $longest_name < 11;
# The table is TABLE_WIDTH characters long. There are seven mandatory
# characters (* and space) in the string. So, we have a total
# of TABLE_WIDTH - 7 characters to work with.
my $remaining_space = (TABLE_WIDTH - 7) - $longest_name;
print '*' x TABLE_WIDTH . "\n";
printf "* \%${longest_name}s * %-${remaining_space}s *\n",
'MODULE NAME', 'ENABLES FEATURE(S)';
print '*' x TABLE_WIDTH . "\n";
foreach my $package (@missing) {
printf "* \%${longest_name}s * %-${remaining_space}s *\n",
$package->{package}, $package->{feature};
}
}
# We only print the PPM repository note if we have to.
if ((!$output && @{$check_results->{missing}})
|| ($output && $check_results->{any_missing}))
{
if (ON_WINDOWS) {
my $perl_ver = sprintf('%vd', $^V);
# URL when running Perl 5.8.x.
my $url_to_theory58S = 'http://theoryx5.uwinnipeg.ca/ppms';
# Packages for Perl 5.10 are not compatible with Perl 5.8.
if (vers_cmp($perl_ver, '5.10') > -1) {
$url_to_theory58S = 'http://cpan.uwinnipeg.ca/PPMPackages/10xx/';
}
print install_string('ppm_repo_add',
{ theory_url => $url_to_theory58S });
# ActivePerls older than revision 819 require an additional command.
if (_get_activestate_build_id() < 819) {
print install_string('ppm_repo_up');
}
}
# If any output was required, we want to close the "table"
print "*" x TABLE_WIDTH . "\n";
}
# And now we print the actual installation commands.
if (my @missing = @{$check_results->{optional}} and $output) {
print install_string('commands_optional') . "\n\n";
foreach my $module (@missing) {
my $command = install_command($module);
printf "%15s: $command\n", $module->{package};
}
print "\n";
}
if (!$check_results->{one_dbd}) {
print install_string('commands_dbd') . "\n";
my %db_modules = %{DB_MODULE()};
foreach my $db (keys %db_modules) {
my $command = install_command($db_modules{$db}->{dbd});
printf "%10s: \%s\n", $db_modules{$db}->{name}, $command;
}
print "\n";
}
if (my @missing = @{$check_results->{missing}}) {
print install_string('commands_required') . "\n";
foreach my $package (@missing) {
my $command = install_command($package);
print " $command\n";
}
}
if ($output && $check_results->{any_missing} && !ON_WINDOWS) {
print install_string('install_all', { perl => $^X });
}
}
sub check_graphviz {
my ($output) = @_;
return 1 if (Bugzilla->params->{'webdotbase'} =~ /^https?:/);
printf("Checking for %15s %-9s ", "GraphViz", "(any)") if $output;
my $return = 0;
if(-x Bugzilla->params->{'webdotbase'}) {
print "ok: found\n" if $output;
$return = 1;
} else {
print "not a valid executable: " . Bugzilla->params->{'webdotbase'} . "\n";
}
my $webdotdir = bz_locations()->{'webdotdir'};
# Check .htaccess allows access to generated images
if (-e "$webdotdir/.htaccess") {
my $htaccess = new IO::File("$webdotdir/.htaccess", 'r')
|| die "$webdotdir/.htaccess: " . $!;
if (!grep(/png/, $htaccess->getlines)) {
print "Dependency graph images are not accessible.\n";
print "delete $webdotdir/.htaccess and re-run checksetup.pl to fix.\n";
}
$htaccess->close;
}
return $return;
}
# This was originally clipped from the libnet Makefile.PL, adapted here to
# use the below vers_cmp routine for accurate version checking.
sub have_vers {
my ($params, $output) = @_;
my $module = $params->{module};
my $package = $params->{package};
if (!$package) {
$package = $module;
$package =~ s/::/-/g;
}
my $wanted = $params->{version};
eval "require $module;";
# VERSION is provided by UNIVERSAL::
my $vnum = eval { $module->VERSION } || -1;
# CGI's versioning scheme went 2.75, 2.751, 2.752, 2.753, 2.76
# That breaks the standard version tests, so we need to manually correct
# the version
if ($module eq 'CGI' && $vnum =~ /(2\.7\d)(\d+)/) {
$vnum = $1 . "." . $2;
}
my $vstr;
if ($vnum eq "-1") { # string compare just in case it's non-numeric
$vstr = install_string('module_not_found');
}
elsif (vers_cmp($vnum,"0") > -1) {
$vstr = install_string('module_found', { ver => $vnum });
}
else {
$vstr = install_string('module_unknown_version');
}
my $vok = (vers_cmp($vnum,$wanted) > -1);
my $blacklisted;
if ($vok && $params->{blacklist}) {
$blacklisted = grep($vnum =~ /$_/, @{$params->{blacklist}});
$vok = 0 if $blacklisted;
}
if ($output) {
my $ok = $vok ? install_string('module_ok') : '';
my $black_string = $blacklisted ? install_string('blacklisted') : '';
my $want_string = $wanted ? "v$wanted" : install_string('any');
$ok = "$ok:" if $ok;
printf "%s %19s %-9s $ok $vstr $black_string\n",
install_string('checking_for'), $package, "($want_string)";
}
return $vok ? 1 : 0;
}
sub install_command {
my $module = shift;
my ($command, $package);
if (ON_WINDOWS) {
$command = 'ppm install %s';
$package = $module->{package};
}
else {
$command = "$^X install-module.pl \%s";
# Non-Windows installations need to use module names, because
# CPAN doesn't understand package names.
$package = $module->{module};
}
return sprintf $command, $package;
}
1;
__END__
=head1 NAME
Bugzilla::Install::Requirements - Functions and variables dealing
with Bugzilla's perl-module requirements.
=head1 DESCRIPTION
This module is used primarily by C<checksetup.pl> to determine whether
or not all of Bugzilla's prerequisites are installed. (That is, all the
perl modules it requires.)
=head1 CONSTANTS
=over 4
=item C<REQUIRED_MODULES>
An arrayref of hashrefs that describes the perl modules required by
Bugzilla. The hashes have two keys, C<name> and C<version>, which
represent the name of the module and the version that we require.
=back
=head1 SUBROUTINES
=over 4
=item C<check_requirements>
=over
=item B<Description>
This checks what optional or required perl modules are installed, like
C<checksetup.pl> does.
=item B<Params>
=over
=item C<$output> - C<true> if you want the function to print out information
about what it's doing, and the versions of everything installed.
=back
=item B<Returns>
A hashref containing these values:
=over
=item C<pass> - Whether or not we have all the mandatory requirements.
=item C<missing> - An arrayref containing any required modules that
are not installed or that are not up-to-date. Each item in the array is
a hashref in the format of items from L</REQUIRED_MODULES>.
=item C<optional> - The same as C<missing>, but for optional modules.
=item C<have_one_dbd> - True if at least one C<DBD::> module is installed.
=item C<any_missing> - True if there are any missing modules, even optional
modules.
=back
=back
=item C<check_graphviz($output)>
Description: Checks if the graphviz binary specified in the
C<webdotbase> parameter is a valid binary, or a valid URL.
Params: C<$output> - C<$true> if you want the function to
print out information about what it's doing.
Returns: C<1> if the check was successful, C<0> otherwise.
=item C<have_vers($module, $output)>
Description: Tells you whether or not you have the appropriate
version of the module requested. It also prints
out a message to the user explaining the check
and the result.
Params: C<$module> - A hashref, in the format of an item from
L</REQUIRED_MODULES>.
C<$output> - Set to true if you want this function to
print information to STDOUT about what it's
doing.
Returns: C<1> if you have the module installed and you have the
appropriate version. C<0> otherwise.
=item C<install_command($module)>
Description: Prints out the appropriate command to install the
module specified, depending on whether you're
on Windows or Linux.
Params: C<$module> - A hashref, in the format of an item from
L</REQUIRED_MODULES>.
Returns: nothing
=back

View File

@@ -1,550 +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 Everything Solved.
# Portions created by Everything Solved are Copyright (C) 2006
# Everything Solved. All Rights Reserved.
#
# Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org>
package Bugzilla::Install::Util;
# The difference between this module and Bugzilla::Util is that this
# module may require *only* Bugzilla::Constants and built-in
# perl modules.
use strict;
use Bugzilla::Constants;
use File::Basename;
use POSIX qw(setlocale LC_CTYPE);
use Safe;
use Scalar::Util qw(tainted);
use base qw(Exporter);
our @EXPORT_OK = qw(
bin_loc
get_version_and_os
indicate_progress
install_string
include_languages
template_include_path
vers_cmp
get_console_locale
);
sub bin_loc {
my ($bin) = @_;
return '' if ON_WINDOWS;
# Don't print any errors from "which"
open(my $saveerr, ">&STDERR");
open(STDERR, '>/dev/null');
my $loc = `which $bin`;
close(STDERR);
open(STDERR, ">&", $saveerr);
my $exit_code = $? >> 8; # See the perlvar manpage.
return '' if $exit_code > 0;
chomp($loc);
return $loc;
}
sub get_version_and_os {
# Display version information
my @os_details = POSIX::uname;
# 0 is the name of the OS, 2 is the major version,
my $os_name = $os_details[0] . ' ' . $os_details[2];
if (ON_WINDOWS) {
require Win32;
$os_name = Win32::GetOSName();
}
# $os_details[3] is the minor version.
return { bz_ver => BUGZILLA_VERSION,
perl_ver => sprintf('%vd', $^V),
os_name => $os_name,
os_ver => $os_details[3] };
}
sub indicate_progress {
my ($params) = @_;
my $current = $params->{current};
my $total = $params->{total};
my $every = $params->{every} || 1;
print "." if !($current % $every);
if ($current == $total || $current % ($every * 60) == 0) {
print "$current/$total (" . int($current * 100 / $total) . "%)\n";
}
}
sub install_string {
my ($string_id, $vars) = @_;
_cache()->{template_include_path} ||= template_include_path();
my $path = _cache()->{template_include_path};
my $string_template;
# Find the first template that defines this string.
foreach my $dir (@$path) {
my $base = "$dir/setup/strings";
$string_template = _get_string_from_file($string_id, "$base.txt.pl")
if !defined $string_template;
last if defined $string_template;
}
die "No language defines the string '$string_id'"
if !defined $string_template;
$vars ||= {};
my @replace_keys = keys %$vars;
foreach my $key (@replace_keys) {
my $replacement = $vars->{$key};
die "'$key' in '$string_id' is tainted: '$replacement'"
if tainted($replacement);
# We don't want people to start getting clever and inserting
# ##variable## into their values. So we check if any other
# key is listed in the *replacement* string, before doing
# the replacement. This is mostly to protect programmers from
# making mistakes.
if (grep($replacement =~ /##$key##/, @replace_keys)) {
die "Unsafe replacement for '$key' in '$string_id': '$replacement'";
}
$string_template =~ s/\Q##$key##\E/$replacement/g;
}
return $string_template;
}
sub include_languages {
my ($params) = @_;
$params ||= {};
# Basically, the way this works is that we have a list of languages
# that we *want*, and a list of languages that Bugzilla actually
# supports. The caller tells us what languages they want, by setting
# $ENV{HTTP_ACCEPT_LANGUAGE} or $params->{only_language}. The languages
# we support are those specified in $params->{use_languages}. Otherwise
# we support every language installed in the template/ directory.
my @wanted;
if ($params->{only_language}) {
@wanted = ($params->{only_language});
}
else {
@wanted = _sort_accept_language($ENV{'HTTP_ACCEPT_LANGUAGE'} || '');
}
my @supported;
if (defined $params->{use_languages}) {
@supported = @{$params->{use_languages}};
}
else {
my @dirs = glob(bz_locations()->{'templatedir'} . "/*");
@dirs = map(basename($_), @dirs);
@supported = grep($_ ne 'CVS', @dirs);
}
my @usedlanguages;
foreach my $wanted (@wanted) {
# If we support the language we want, or *any version* of
# the language we want, it gets pushed into @usedlanguages.
#
# Per RFC 1766 and RFC 2616, things like 'en' match 'en-us' and
# 'en-uk', but not the other way around. (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$wanted\E(-.+)?$/i, @supported) {
push (@usedlanguages, @found);
}
}
# We always include English at the bottom if it's not there, even if
# somebody removed it from use_languages.
if (!grep($_ eq 'en', @usedlanguages)) {
push(@usedlanguages, 'en');
}
return @usedlanguages;
}
sub template_include_path {
my @usedlanguages = include_languages(@_);
# Now, we add template directories in the order they will be searched:
# First, we add extension template directories, because extension templates
# override standard templates. Extensions may be localized in the same way
# that Bugzilla templates are localized.
my @include_path;
my @extensions = glob(bz_locations()->{'extensionsdir'} . "/*");
foreach my $extension (@extensions) {
foreach my $lang (@usedlanguages) {
_add_language_set(\@include_path, $lang, "$extension/template");
}
}
# Then, we add normal template directories, sorted by language.
foreach my $lang (@usedlanguages) {
_add_language_set(\@include_path, $lang);
}
return \@include_path;
}
# This is taken straight from Sort::Versions 1.5, which is not included
# with perl by default.
sub vers_cmp {
my ($a, $b) = @_;
# Remove leading zeroes - Bug 344661
$a =~ s/^0*(\d.+)/$1/;
$b =~ s/^0*(\d.+)/$1/;
my @A = ($a =~ /([-.]|\d+|[^-.\d]+)/g);
my @B = ($b =~ /([-.]|\d+|[^-.\d]+)/g);
my ($A, $B);
while (@A and @B) {
$A = shift @A;
$B = shift @B;
if ($A eq '-' and $B eq '-') {
next;
} elsif ( $A eq '-' ) {
return -1;
} elsif ( $B eq '-') {
return 1;
} elsif ($A eq '.' and $B eq '.') {
next;
} elsif ( $A eq '.' ) {
return -1;
} elsif ( $B eq '.' ) {
return 1;
} elsif ($A =~ /^\d+$/ and $B =~ /^\d+$/) {
if ($A =~ /^0/ || $B =~ /^0/) {
return $A cmp $B if $A cmp $B;
} else {
return $A <=> $B if $A <=> $B;
}
} else {
$A = uc $A;
$B = uc $B;
return $A cmp $B if $A cmp $B;
}
}
@A <=> @B;
}
######################
# Helper Subroutines #
######################
# Used by install_string
sub _get_string_from_file {
my ($string_id, $file) = @_;
return undef if !-e $file;
my $safe = new Safe;
$safe->rdo($file);
my %strings = %{$safe->varglob('strings')};
return $strings{$string_id};
}
# Used by template_include_path.
sub _add_language_set {
my ($array, $lang, $templatedir) = @_;
$templatedir ||= bz_locations()->{'templatedir'};
my @add = ("$templatedir/$lang/custom", "$templatedir/$lang/default");
my $project = bz_locations->{'project'};
unshift(@add, "$templatedir/$lang/$project") if $project;
foreach my $dir (@add) {
if (-d $dir) {
trick_taint($dir);
push(@$array, $dir);
}
}
}
# Make an ordered list out of a HTTP Accept-Language header (see RFC 2616, 14.4)
# We ignore '*' and <language-range>;q=0
# For languages with the same priority q the order remains unchanged.
sub _sort_accept_language {
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));
}
sub get_console_locale {
require Locale::Language;
my $locale = setlocale(LC_CTYPE);
my $language;
# Some distros set e.g. LC_CTYPE = fr_CH.UTF-8. We clean it up.
if ($locale =~ /^([^\.]+)/) {
$locale = $1;
}
$locale =~ s/_/-/;
# It's pretty sure that there is no language pack of the form fr-CH
# installed, so we also include fr as a wanted language.
if ($locale =~ /^(\S+)\-/) {
$language = $1;
$locale .= ",$language";
}
else {
$language = $locale;
}
# Some OSs or distributions may have setlocale return a string of the form
# German_Germany.1252 (this example taken from a Windows XP system), which
# is unsuitable for our needs because Bugzilla works on language codes.
# We try and convert them here.
if ($language = Locale::Language::language2code($language)) {
$locale .= ",$language";
}
return $locale;
}
# This is like request_cache, but it's used only by installation code
# for setup.cgi and things like that.
our $_cache = {};
sub _cache {
if ($ENV{MOD_PERL}) {
require Apache2::RequestUtil;
return Apache2::RequestUtil->request->pnotes();
}
return $_cache;
}
###############################
# Copied from Bugzilla::Util #
##############################
sub trick_taint {
require Carp;
Carp::confess("Undef to trick_taint") unless defined $_[0];
my $match = $_[0] =~ /^(.*)$/s;
$_[0] = $match ? $1 : undef;
return (defined($_[0]));
}
__END__
=head1 NAME
Bugzilla::Install::Util - Utility functions that are useful both during
installation and afterwards.
=head1 DESCRIPTION
This module contains various subroutines that are used primarily
during installation. However, these subroutines can also be useful to
non-installation code, so they have been split out into this module.
The difference between this module and L<Bugzilla::Util> is that this
module is safe to C<use> anywhere in Bugzilla, even during installation,
because it depends only on L<Bugzilla::Constants> and built-in perl modules.
None of the subroutines are exported by default--you must explicitly
export them.
=head1 SUBROUTINES
=over
=item C<bin_loc>
On *nix systems, given the name of a binary, returns the path to that
binary, if the binary is in the C<PATH>.
=item C<get_version_and_os>
Returns a hash containing information about what version of Bugzilla we're
running, what perl version we're using, and what OS we're running on.
=item C<get_console_locale>
Returns the language to use based on the LC_CTYPE value returned by the OS.
If LC_CTYPE is of the form fr-CH, then fr is appended to the list.
=item C<indicate_progress>
=over
=item B<Description>
This prints out lines of dots as a long update is going on, to let the user
know where we are and that we're not frozen. A new line of dots will start
every 60 dots.
Sample usage: C<indicate_progress({ total =E<gt> $total, current =E<gt>
$count, every =E<gt> 1 })>
=item B<Sample Output>
Here's some sample output with C<total = 1000> and C<every = 10>:
............................................................600/1000 (60%)
........................................
=item B<Params>
=over
=item C<total> - The total number of items we're processing.
=item C<current> - The number of the current item we're processing.
=item C<every> - How often the function should print out a dot.
For example, if this is 10, the function will print out a dot every
ten items. Defaults to 1 if not specified.
=back
=item B<Returns>: nothing
=back
=item C<install_string>
=over
=item B<Description>
This is a very simple method of templating strings for installation.
It should only be used by code that has to run before the Template Toolkit
can be used. (See the comments at the top of the various L<Bugzilla::Install>
modules to find out when it's safe to use Template Toolkit.)
It pulls strings out of the F<strings.txt.pl> "template" and replaces
any variable surrounded by double-hashes (##) with a value you specify.
This allows for localization of strings used during installation.
=item B<Example>
Let's say your template string looks like this:
The ##animal## jumped over the ##plant##.
Let's say that string is called 'animal_jump_plant'. So you call the function
like this:
install_string('animal_jump_plant', { animal => 'fox', plant => 'tree' });
That will output this:
The fox jumped over the tree.
=item B<Params>
=over
=item C<$string_id> - The name of the string from F<strings.txt.pl>.
=item C<$vars> - A hashref containing the replacement values for variables
inside of the string.
=back
=item B<Returns>: The appropriate string, with variables replaced.
=back
=item C<template_include_path>
Used by L<Bugzilla::Template> and L</install_string> to determine the
directories where templates are installed. Templates can be installed
in many places. They're listed here in the basic order that they're
searched:
=over
=item extensions/C<$extension>/template/C<$language>/C<$project>
=item extensions/C<$extension>/template/C<$language>/custom
=item extensions/C<$extension>/template/C<$language>/default
=item template/C<$language>/C<$project>
=item template/C<$language>/custom
=item template/C<$language>/default
=back
C<$project> has to do with installations that are using the C<$ENV{PROJECT}>
variable to have different "views" on a single Bugzilla.
The F<default> directory includes templates shipped with Bugzilla.
The F<custom> directory is a directory for local installations to override
the F<default> templates. Any individual template in F<custom> will
override a template of the same name and path in F<default>.
C<$language> is a language code, C<en> being the default language shipped
with Bugzilla. Localizers ship other languages.
C<$extension> is the name of any directory in the F<extensions/> directory.
Each extension has its own directory.
Note that languages are sorted by the user's preference (as specified
in their browser, usually), and extensions are sorted alphabetically.
=item C<include_languages>
Used by L<Bugzilla::Template> to determine the languages' list which
are compiled with the browser's I<Accept-Language> and the languages
of installed templates.
=item C<vers_cmp>
=over
=item B<Description>
This is a comparison function, like you would use in C<sort>, except that
it compares two version numbers. So, for example, 2.10 would be greater
than 2.2.
It's based on versioncmp from L<Sort::Versions>, with some Bugzilla-specific
fixes.
=item B<Params>: C<$a> and C<$b> - The versions you want to compare.
=item B<Returns>
C<-1> if C<$a> is less than C<$b>, C<0> if they are equal, or C<1> if C<$a>
is greater than C<$b>.
=back
=back

View File

@@ -1,57 +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 Mozilla Corporation.
# Portions created by the Initial Developer are Copyright (C) 2008
# Mozilla Corporation. All Rights Reserved.
#
# Contributor(s):
# Mark Smith <mark@mozilla.com>
# Max Kanat-Alexander <mkanat@bugzilla.org>
package Bugzilla::Job::Mailer;
use strict;
use Bugzilla::Mailer;
BEGIN { eval "use base qw(TheSchwartz::Worker)"; }
# The longest we expect a job to possibly take, in seconds.
use constant grab_for => 300;
# We don't want email to fail permanently very easily. Retry for 30 days.
use constant max_retries => 725;
# The first few retries happen quickly, but after that we wait an hour for
# each retry.
sub retry_delay {
my $num_retries = shift;
if ($num_retries < 5) {
return (10, 30, 60, 300, 600)[$num_retries];
}
# One hour
return 60*60;
}
sub work {
my ($class, $job) = @_;
my $msg = $job->arg->{msg};
my $success = eval { MessageToMTA($msg, 1); 1; };
if (!$success) {
$job->failed($@);
undef $@;
}
else {
$job->completed;
}
}
1;

View File

@@ -1,108 +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 Mozilla Corporation.
# Portions created by the Initial Developer are Copyright (C) 2008
# Mozilla Corporation. All Rights Reserved.
#
# Contributor(s):
# Mark Smith <mark@mozilla.com>
# Max Kanat-Alexander <mkanat@bugzilla.org>
package Bugzilla::JobQueue;
use strict;
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Install::Util qw(install_string);
BEGIN { eval "use base qw(TheSchwartz)"; }
# This maps job names for Bugzilla::JobQueue to the appropriate modules.
# If you add new types of jobs, you should add a mapping here.
use constant JOB_MAP => {
send_mail => 'Bugzilla::Job::Mailer',
};
sub new {
my $class = shift;
if (!eval { require TheSchwartz; }) {
ThrowCodeError('jobqueue_not_configured');
}
my $lc = Bugzilla->localconfig;
my $self = $class->SUPER::new(
databases => [{
dsn => Bugzilla->dbh->{private_bz_dsn},
user => $lc->{db_user},
pass => $lc->{db_pass},
prefix => 'ts_',
}],
);
return $self;
}
# A way to get access to the underlying databases directly.
sub bz_databases {
my $self = shift;
my @hashes = keys %{ $self->{databases} };
return map { $self->driver_for($_) } @hashes;
}
# inserts a job into the queue to be processed and returns immediately
sub insert {
my $self = shift;
my $job = shift;
my $mapped_job = JOB_MAP->{$job};
ThrowCodeError('jobqueue_no_job_mapping', { job => $job })
if !$mapped_job;
unshift(@_, $mapped_job);
my $retval = $self->SUPER::insert(@_);
# XXX Need to get an error message here if insert fails, but
# I don't see any way to do that in TheSchwartz.
ThrowCodeError('jobqueue_insert_failed', { job => $job, errmsg => $@ })
if !$retval;
return $retval;
}
1;
__END__
=head1 NAME
Bugzilla::JobQueue - Interface between Bugzilla and TheSchwartz.
=head1 SYNOPSIS
use Bugzilla;
my $obj = Bugzilla->job_queue();
$obj->insert('send_mail', { msg => $message });
=head1 DESCRIPTION
Certain tasks should be done asyncronously. The job queue system allows
Bugzilla to use some sort of service to schedule jobs to happen asyncronously.
=head2 Inserting a Job
See the synopsis above for an easy to follow example on how to insert a
job into the queue. Give it a name and some arguments and the job will
be sent away to be done later.

View File

@@ -1,99 +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 Mozilla Corporation.
# Portions created by the Initial Developer are Copyright (C) 2008
# Mozilla Corporation. All Rights Reserved.
#
# Contributor(s):
# Mark Smith <mark@mozilla.com>
# Max Kanat-Alexander <mkanat@bugzilla.org>
# XXX In order to support Windows, we have to make gd_redirect_output
# use Log4Perl or something instead of calling "logger". We probably
# also need to use Win32::Daemon or something like that to daemonize.
package Bugzilla::JobQueue::Runner;
use strict;
use File::Basename;
use Pod::Usage;
use Bugzilla::Constants;
use Bugzilla::JobQueue;
use Bugzilla::Util qw(get_text);
BEGIN { eval "use base qw(Daemon::Generic)"; }
# Required because of a bug in Daemon::Generic where it won't use the
# "version" key from DAEMON_CONFIG.
our $VERSION = BUGZILLA_VERSION;
use constant DAEMON_CONFIG => (
progname => basename($0),
pidfile => bz_locations()->{datadir} . '/' . basename($0) . '.pid',
version => BUGZILLA_VERSION,
);
sub gd_preconfig {
return DAEMON_CONFIG;
}
sub gd_usage {
pod2usage({ -verbose => 0, -exitval => 'NOEXIT' });
return 0
}
sub gd_check {
my $self = shift;
# Get a count of all the jobs currently in the queue.
my $jq = Bugzilla->job_queue();
my @dbs = $jq->bz_databases();
my $count = 0;
foreach my $driver (@dbs) {
$count += $driver->select_one('SELECT COUNT(*) FROM ts_job', []);
}
print get_text('job_queue_depth', { count => $count }) . "\n";
}
sub gd_run {
my $self = shift;
my $jq = Bugzilla->job_queue();
$jq->set_verbose($self->{debug});
foreach my $module (values %{ Bugzilla::JobQueue::JOB_MAP() }) {
eval "use $module";
$jq->can_do($module);
}
$jq->work;
}
1;
__END__
=head1 NAME
Bugzilla::JobQueue::Runner - A class representing the daemon that runs the
job queue.
=head1 SYNOPSIS
use Bugzilla::JobQueue::Runner;
Bugzilla::JobQueue::Runner->new();
=head1 DESCRIPTION
This is a subclass of L<Daemon::Generic> that is used by L<jobqueue>
to run the Bugzilla job queue.

View File

@@ -1,189 +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): Max Kanat-Alexander <mkanat@bugzilla.org>
use strict;
package Bugzilla::Keyword;
use base qw(Bugzilla::Object);
use Bugzilla::Error;
use Bugzilla::Util;
###############################
#### Initialization ####
###############################
use constant DB_COLUMNS => qw(
keyworddefs.id
keyworddefs.name
keyworddefs.description
);
use constant DB_TABLE => 'keyworddefs';
use constant REQUIRED_CREATE_FIELDS => qw(name description);
use constant VALIDATORS => {
name => \&_check_name,
description => \&_check_description,
};
use constant UPDATE_COLUMNS => qw(
name
description
);
###############################
#### Accessors ######
###############################
sub description { return $_[0]->{'description'}; }
sub bug_count {
my ($self) = @_;
return $self->{'bug_count'} if defined $self->{'bug_count'};
($self->{'bug_count'}) =
Bugzilla->dbh->selectrow_array(
'SELECT COUNT(*) FROM keywords WHERE keywordid = ?',
undef, $self->id);
return $self->{'bug_count'};
}
###############################
#### Mutators #####
###############################
sub set_name { $_[0]->set('name', $_[1]); }
sub set_description { $_[0]->set('description', $_[1]); }
###############################
#### Subroutines ######
###############################
sub keyword_count {
my ($count) =
Bugzilla->dbh->selectrow_array('SELECT COUNT(*) FROM keyworddefs');
return $count;
}
sub get_all_with_bug_count {
my $class = shift;
my $dbh = Bugzilla->dbh;
my $keywords =
$dbh->selectall_arrayref('SELECT ' . join(', ', DB_COLUMNS) . ',
COUNT(keywords.bug_id) AS bug_count
FROM keyworddefs
LEFT JOIN keywords
ON keyworddefs.id = keywords.keywordid ' .
$dbh->sql_group_by('keyworddefs.id',
'keyworddefs.name,
keyworddefs.description') . '
ORDER BY keyworddefs.name', {'Slice' => {}});
if (!$keywords) {
return [];
}
foreach my $keyword (@$keywords) {
bless($keyword, $class);
}
return $keywords;
}
###############################
### Validators ###
###############################
sub _check_name {
my ($self, $name) = @_;
$name = trim($name);
$name eq "" && ThrowUserError("keyword_blank_name");
if ($name =~ /[\s,]/) {
ThrowUserError("keyword_invalid_name");
}
# We only want to validate the non-existence of the name if
# we're creating a new Keyword or actually renaming the keyword.
if (!ref($self) || $self->name ne $name) {
my $keyword = new Bugzilla::Keyword({ name => $name });
ThrowUserError("keyword_already_exists", { name => $name }) if $keyword;
}
return $name;
}
sub _check_description {
my ($self, $desc) = @_;
$desc = trim($desc);
$desc eq '' && ThrowUserError("keyword_blank_description");
return $desc;
}
1;
__END__
=head1 NAME
Bugzilla::Keyword - A Keyword that can be added to a bug.
=head1 SYNOPSIS
use Bugzilla::Keyword;
my $count = Bugzilla::Keyword::keyword_count;
my $description = $keyword->description;
my $keywords = Bugzilla::Keyword->get_all_with_bug_count();
=head1 DESCRIPTION
Bugzilla::Keyword represents a keyword that can be added to a bug.
This implements all standard C<Bugzilla::Object> methods. See
L<Bugzilla::Object> for more details.
=head1 SUBROUTINES
This is only a list of subroutines specific to C<Bugzilla::Keyword>.
See L<Bugzilla::Object> for more subroutines that this object
implements.
=over
=item C<keyword_count()>
Description: A utility function to get the total number
of keywords defined. Mostly used to see
if there are any keywords defined at all.
Params: none
Returns: An integer, the count of keywords.
=item C<get_all_with_bug_count()>
Description: Returns all defined keywords. This is an efficient way
to get the associated bug counts, as only one SQL query
is executed with this method, instead of one per keyword
when calling get_all and then bug_count.
Params: none
Returns: A reference to an array of Keyword objects, or an empty
arrayref if there are no keywords.
=back
=cut

View File

@@ -1,224 +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>
# Frédéric Buclin <LpSolit@gmail.com>
# Max Kanat-Alexander <mkanat@bugzilla.org>
package Bugzilla::Mailer;
use strict;
use base qw(Exporter);
@Bugzilla::Mailer::EXPORT = qw(MessageToMTA build_thread_marker);
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Hook;
use Bugzilla::Util;
use Date::Format qw(time2str);
use Encode qw(encode);
use Encode::MIME::Header;
use Email::Address;
use Email::MIME;
# Loading this gives us encoding_set.
use Email::MIME::Modifier;
use Email::Send;
sub MessageToMTA {
my ($msg, $send_now) = (@_);
my $method = Bugzilla->params->{'mail_delivery_method'};
return if $method eq 'None';
if (Bugzilla->params->{'use_mailer_queue'} and !$send_now) {
Bugzilla->job_queue->insert('send_mail', { msg => $msg });
return;
}
my $email;
if (ref $msg) {
$email = $msg;
}
else {
# RFC 2822 requires us to have CRLF for our line endings and
# Email::MIME doesn't do this for us. We use \015 (CR) and \012 (LF)
# directly because Perl translates "\n" depending on what platform
# you're running on. See http://perldoc.perl.org/perlport.html#Newlines
# We check for multiple CRs because of this Template-Toolkit bug:
# https://rt.cpan.org/Ticket/Display.html?id=43345
$msg =~ s/(?:\015+)?\012/\015\012/msg;
$email = new Email::MIME($msg);
}
# We add this header to uniquely identify all email that we
# send as coming from this Bugzilla installation.
#
# We don't use correct_urlbase, because we want this URL to
# *always* be the same for this Bugzilla, in every email,
# and some emails we send when we're logged out (in which case
# some emails might get urlbase while the logged-in emails might
# get sslbase). Also, we want this to stay the same even if
# the admin changes the "ssl" parameter.
$email->header_set('X-Bugzilla-URL', Bugzilla->params->{'urlbase'});
# We add this header to mark the mail as "auto-generated" and
# thus to hopefully avoid auto replies.
$email->header_set('Auto-Submitted', 'auto-generated');
$email->walk_parts(sub {
my ($part) = @_;
return if $part->parts > 1; # Top-level
my $content_type = $part->content_type || '';
if ($content_type !~ /;/) {
my $body = $part->body;
if (Bugzilla->params->{'utf8'}) {
$part->charset_set('UTF-8');
# encoding_set works only with bytes, not with utf8 strings.
my $raw = $part->body_raw;
if (utf8::is_utf8($raw)) {
utf8::encode($raw);
$part->body_set($raw);
}
}
$part->encoding_set('quoted-printable') if !is_7bit_clean($body);
}
});
# MIME-Version must be set otherwise some mailsystems ignore the charset
$email->header_set('MIME-Version', '1.0') if !$email->header('MIME-Version');
# Encode the headers correctly in quoted-printable
foreach my $header ($email->header_names) {
my @values = $email->header($header);
# We don't recode headers that happen multiple times.
next if scalar(@values) > 1;
if (my $value = $values[0]) {
if (Bugzilla->params->{'utf8'} && !utf8::is_utf8($value)) {
utf8::decode($value);
}
# avoid excessive line wrapping done by Encode.
local $Encode::Encoding{'MIME-Q'}->{'bpl'} = 998;
my $encoded = encode('MIME-Q', $value);
$email->header_set($header, $encoded);
}
}
my $from = $email->header('From');
my ($hostname, @args);
if ($method eq "Sendmail") {
if (ON_WINDOWS) {
$Email::Send::Sendmail::SENDMAIL = SENDMAIL_EXE;
}
push @args, "-i";
# We want to make sure that we pass *only* an email address.
if ($from) {
my ($email_obj) = Email::Address->parse($from);
if ($email_obj) {
my $from_email = $email_obj->address;
push(@args, "-f$from_email") if $from_email;
}
}
push(@args, "-ODeliveryMode=deferred")
if !Bugzilla->params->{"sendmailnow"};
}
else {
# Sendmail will automatically append our hostname to the From
# address, but other mailers won't.
my $urlbase = Bugzilla->params->{'urlbase'};
$urlbase =~ m|//([^:/]+)[:/]?|;
$hostname = $1;
$from .= "\@$hostname" if $from !~ /@/;
$email->header_set('From', $from);
# Sendmail adds a Date: header also, but others may not.
if (!defined $email->header('Date')) {
$email->header_set('Date', time2str("%a, %e %b %Y %T %z", time()));
}
}
if ($method eq "SMTP") {
push @args, Host => Bugzilla->params->{"smtpserver"},
username => Bugzilla->params->{"smtp_username"},
password => Bugzilla->params->{"smtp_password"},
Hello => $hostname,
Debug => Bugzilla->params->{'smtp_debug'};
}
Bugzilla::Hook::process('mailer-before_send', { email => $email });
if ($method eq "Test") {
my $filename = bz_locations()->{'datadir'} . '/mailer.testfile';
open TESTFILE, '>>', $filename;
# From - <date> is required to be a valid mbox file.
print TESTFILE "\n\nFrom - " . $email->header('Date') . "\n" . $email->as_string;
close TESTFILE;
}
else {
# This is useful for both Sendmail and Qmail, so we put it out here.
local $ENV{PATH} = SENDMAIL_PATH;
my $mailer = Email::Send->new({ mailer => $method,
mailer_args => \@args });
my $retval = $mailer->send($email);
ThrowCodeError('mail_send_error', { msg => $retval, mail => $email })
if !$retval;
}
}
# Builds header suitable for use as a threading marker in email notifications
sub build_thread_marker {
my ($bug_id, $user_id, $is_new) = @_;
if (!defined $user_id) {
$user_id = Bugzilla->user->id;
}
my $sitespec = '@' . Bugzilla->params->{'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 '@'
}
my $threadingmarker;
if ($is_new) {
$threadingmarker = "Message-ID: <bug-$bug_id-$user_id$sitespec>";
}
else {
$threadingmarker = "In-Reply-To: <bug-$bug_id-$user_id$sitespec>" .
"\nReferences: <bug-$bug_id-$user_id$sitespec>";
}
return $threadingmarker;
}
1;

View File

@@ -1,375 +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>
# Max Kanat-Alexander <mkanat@bugzilla.org>
# Frédéric Buclin <LpSolit@gmail.com>
use strict;
package Bugzilla::Milestone;
use base qw(Bugzilla::Object);
use Bugzilla::Constants;
use Bugzilla::Util;
use Bugzilla::Error;
################################
##### Initialization #####
################################
use constant DEFAULT_SORTKEY => 0;
use constant DB_TABLE => 'milestones';
use constant NAME_FIELD => 'value';
use constant LIST_ORDER => 'sortkey, value';
use constant DB_COLUMNS => qw(
id
value
product_id
sortkey
);
use constant REQUIRED_CREATE_FIELDS => qw(
name
product
);
use constant UPDATE_COLUMNS => qw(
value
sortkey
);
use constant VALIDATORS => {
product => \&_check_product,
sortkey => \&_check_sortkey,
};
use constant UPDATE_VALIDATORS => {
value => \&_check_value,
};
################################
sub new {
my $class = shift;
my $param = shift;
my $dbh = Bugzilla->dbh;
my $product;
if (ref $param) {
$product = $param->{product};
my $name = $param->{name};
if (!defined $product) {
ThrowCodeError('bad_arg',
{argument => 'product',
function => "${class}::new"});
}
if (!defined $name) {
ThrowCodeError('bad_arg',
{argument => 'name',
function => "${class}::new"});
}
my $condition = 'product_id = ? AND value = ?';
my @values = ($product->id, $name);
$param = { condition => $condition, values => \@values };
}
unshift @_, $param;
return $class->SUPER::new(@_);
}
sub run_create_validators {
my $class = shift;
my $params = $class->SUPER::run_create_validators(@_);
my $product = delete $params->{product};
$params->{product_id} = $product->id;
$params->{value} = $class->_check_value($params->{name}, $product);
delete $params->{name};
return $params;
}
sub update {
my $self = shift;
my $changes = $self->SUPER::update(@_);
if (exists $changes->{value}) {
my $dbh = Bugzilla->dbh;
# The milestone value is stored in the bugs table instead of its ID.
$dbh->do('UPDATE bugs SET target_milestone = ?
WHERE target_milestone = ? AND product_id = ?',
undef, ($self->name, $changes->{value}->[0], $self->product_id));
# The default milestone also stores the value instead of the ID.
$dbh->do('UPDATE products SET defaultmilestone = ?
WHERE id = ? AND defaultmilestone = ?',
undef, ($self->name, $self->product_id, $changes->{value}->[0]));
}
return $changes;
}
sub remove_from_db {
my $self = shift;
my $dbh = Bugzilla->dbh;
# The default milestone cannot be deleted.
if ($self->name eq $self->product->default_milestone) {
ThrowUserError('milestone_is_default', { milestone => $self });
}
if ($self->bug_count) {
# We don't want to delete bugs when deleting a milestone.
# Bugs concerned are reassigned to the default milestone.
my $bug_ids =
$dbh->selectcol_arrayref('SELECT bug_id FROM bugs
WHERE product_id = ? AND target_milestone = ?',
undef, ($self->product->id, $self->name));
my $timestamp = $dbh->selectrow_array('SELECT NOW()');
$dbh->do('UPDATE bugs SET target_milestone = ?, delta_ts = ?
WHERE ' . $dbh->sql_in('bug_id', $bug_ids),
undef, ($self->product->default_milestone, $timestamp));
require Bugzilla::Bug;
import Bugzilla::Bug qw(LogActivityEntry);
foreach my $bug_id (@$bug_ids) {
LogActivityEntry($bug_id, 'target_milestone',
$self->name,
$self->product->default_milestone,
Bugzilla->user->id, $timestamp);
}
}
$dbh->do('DELETE FROM milestones WHERE id = ?', undef, $self->id);
}
################################
# Validators
################################
sub _check_value {
my ($invocant, $name, $product) = @_;
$name = trim($name);
$name || ThrowUserError('milestone_blank_name');
if (length($name) > MAX_MILESTONE_SIZE) {
ThrowUserError('milestone_name_too_long', {name => $name});
}
$product = $invocant->product if (ref $invocant);
my $milestone = new Bugzilla::Milestone({product => $product, name => $name});
if ($milestone && (!ref $invocant || $milestone->id != $invocant->id)) {
ThrowUserError('milestone_already_exists', { name => $milestone->name,
product => $product->name });
}
return $name;
}
sub _check_sortkey {
my ($invocant, $sortkey) = @_;
# Keep a copy in case detaint_signed() clears the sortkey
my $stored_sortkey = $sortkey;
if (!detaint_signed($sortkey) || $sortkey < MIN_SMALLINT || $sortkey > MAX_SMALLINT) {
ThrowUserError('milestone_sortkey_invalid', {sortkey => $stored_sortkey});
}
return $sortkey;
}
sub _check_product {
my ($invocant, $product) = @_;
return Bugzilla->user->check_can_admin_product($product->name);
}
################################
# Methods
################################
sub set_name { $_[0]->set('value', $_[1]); }
sub set_sortkey { $_[0]->set('sortkey', $_[1]); }
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'}; }
sub product {
my $self = shift;
require Bugzilla::Product;
$self->{'product'} ||= new Bugzilla::Product($self->product_id);
return $self->{'product'};
}
1;
__END__
=head1 NAME
Bugzilla::Milestone - Bugzilla product milestone class.
=head1 SYNOPSIS
use Bugzilla::Milestone;
my $milestone = new Bugzilla::Milestone({ name => $name, product => $product });
my $name = $milestone->name;
my $product_id = $milestone->product_id;
my $product = $milestone->product;
my $sortkey = $milestone->sortkey;
my $milestone = Bugzilla::Milestone->create(
{ name => $name, product => $product, sortkey => $sortkey });
$milestone->set_name($new_name);
$milestone->set_sortkey($new_sortkey);
$milestone->update();
$milestone->remove_from_db;
=head1 DESCRIPTION
Milestone.pm represents a Product Milestone object.
=head1 METHODS
=over
=item C<new({name => $name, product => $product})>
Description: The constructor is used to load an existing milestone
by passing a product object and a milestone name.
Params: $product - a Bugzilla::Product object.
$name - the name of a milestone (string).
Returns: A Bugzilla::Milestone object.
=item C<name()>
Description: Name (value) of the milestone.
Params: none.
Returns: The name of the milestone.
=item C<product_id()>
Description: ID of the product the milestone belongs to.
Params: none.
Returns: The ID of a product.
=item C<product()>
Description: The product object of the product the milestone belongs to.
Params: none.
Returns: A Bugzilla::Product object.
=item C<sortkey()>
Description: Sortkey of the milestone.
Params: none.
Returns: The sortkey of the milestone.
=item C<bug_count()>
Description: Returns the total of bugs that belong to the milestone.
Params: none.
Returns: Integer with the number of bugs.
=item C<set_name($new_name)>
Description: Changes the name of the milestone.
Params: $new_name - new name of the milestone (string). This name
must be unique within the product.
Returns: Nothing.
=item C<set_sortkey($new_sortkey)>
Description: Changes the sortkey of the milestone.
Params: $new_sortkey - new sortkey of the milestone (signed integer).
Returns: Nothing.
=item C<update()>
Description: Writes the new name and/or the new sortkey into the DB.
Params: none.
Returns: A hashref with changes made to the milestone object.
=item C<remove_from_db()>
Description: Deletes the current milestone from the DB. The object itself
is not destroyed.
Params: none.
Returns: Nothing.
=back
=head1 CLASS METHODS
=over
=item C<create({name => $name, product => $product, sortkey => $sortkey})>
Description: Create a new milestone for the given product.
Params: $name - name of the new milestone (string). This name
must be unique within the product.
$product - a Bugzilla::Product object.
$sortkey - the sortkey of the new milestone (signed integer)
Returns: A Bugzilla::Milestone object.
=back

View File

@@ -1,894 +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 Everything Solved.
# Portions created by Everything Solved are Copyright (C) 2006
# Everything Solved. All Rights Reserved.
#
# Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org>
# Frédéric Buclin <LpSolit@gmail.com>
use strict;
package Bugzilla::Object;
use Bugzilla::Constants;
use Bugzilla::Util;
use Bugzilla::Error;
use Date::Parse;
use constant NAME_FIELD => 'name';
use constant ID_FIELD => 'id';
use constant LIST_ORDER => NAME_FIELD;
use constant UPDATE_VALIDATORS => {};
use constant NUMERIC_COLUMNS => ();
use constant DATE_COLUMNS => ();
###############################
#### Initialization ####
###############################
sub new {
my $invocant = shift;
my $class = ref($invocant) || $invocant;
my $object = $class->_init(@_);
bless($object, $class) if $object;
return $object;
}
# Note: Because this uses sql_istrcmp, if you make a new object use
# Bugzilla::Object, make sure that you modify bz_setup_database
# in Bugzilla::DB::Pg appropriately, to add the right LOWER
# index. You can see examples already there.
sub _init {
my $class = shift;
my ($param) = @_;
my $dbh = Bugzilla->dbh;
my $columns = join(',', $class->DB_COLUMNS);
my $table = $class->DB_TABLE;
my $name_field = $class->NAME_FIELD;
my $id_field = $class->ID_FIELD;
my $id = $param;
if (ref $param eq 'HASH') {
$id = $param->{id};
}
my $object;
if (defined $id) {
# We special-case if somebody specifies an ID, so that we can
# validate it as numeric.
detaint_natural($id)
|| ThrowCodeError('param_must_be_numeric',
{function => $class . '::_init'});
$object = $dbh->selectrow_hashref(qq{
SELECT $columns FROM $table
WHERE $id_field = ?}, undef, $id);
} else {
unless (defined $param->{name} || (defined $param->{'condition'}
&& defined $param->{'values'}))
{
ThrowCodeError('bad_arg', { argument => 'param',
function => $class . '::new' });
}
my ($condition, @values);
if (defined $param->{name}) {
$condition = $dbh->sql_istrcmp($name_field, '?');
push(@values, $param->{name});
}
elsif (defined $param->{'condition'} && defined $param->{'values'}) {
caller->isa('Bugzilla::Object')
|| ThrowCodeError('protection_violation',
{ caller => caller,
function => $class . '::new',
argument => 'condition/values' });
$condition = $param->{'condition'};
push(@values, @{$param->{'values'}});
}
map { trick_taint($_) } @values;
$object = $dbh->selectrow_hashref(
"SELECT $columns FROM $table WHERE $condition", undef, @values);
}
return $object;
}
sub check {
my ($invocant, $param) = @_;
my $class = ref($invocant) || $invocant;
# If we were just passed a name, then just use the name.
if (!ref $param) {
$param = { name => $param };
}
# Don't allow empty names or ids.
my $check_param = exists $param->{id} ? $param->{id} : $param->{name};
$check_param = trim($check_param);
$check_param || ThrowUserError('object_not_specified', { class => $class });
my $obj = $class->new($param)
|| ThrowUserError('object_does_not_exist', {%$param, class => $class});
return $obj;
}
sub new_from_list {
my $invocant = shift;
my $class = ref($invocant) || $invocant;
my ($id_list) = @_;
my $id_field = $class->ID_FIELD;
my @detainted_ids;
foreach my $id (@$id_list) {
detaint_natural($id) ||
ThrowCodeError('param_must_be_numeric',
{function => $class . '::new_from_list'});
push(@detainted_ids, $id);
}
# We don't do $invocant->match because some classes have
# their own implementation of match which is not compatible
# with this one. However, match() still needs to have the right $invocant
# in order to do $class->DB_TABLE and so on.
return match($invocant, { $id_field => \@detainted_ids });
}
# Note: Future extensions to this could be:
# * Add a MATCH_JOIN constant so that we can join against
# certain other tables for the WHERE criteria.
sub match {
my ($invocant, $criteria) = @_;
my $class = ref($invocant) || $invocant;
my $dbh = Bugzilla->dbh;
return [$class->get_all] if !$criteria;
my (@terms, @values, $postamble);
foreach my $field (keys %$criteria) {
my $value = $criteria->{$field};
# allow for LIMIT and OFFSET expressions via the criteria.
next if $field eq 'OFFSET';
if ( $field eq 'LIMIT' ) {
next unless defined $value;
$postamble = $dbh->sql_limit( $value, $criteria->{OFFSET} );
next;
}
elsif ( $field eq 'WHERE' ) {
# the WHERE value is a hashref where the keys are
# "column_name operator ?" and values are the placeholder's
# value (either a scalar or an array of values).
foreach my $k (keys %$value) {
push(@terms, $k);
my @this_value = ref($value->{$k}) ? @{ $value->{$k} }
: ($value->{$k});
push(@values, @this_value);
}
next;
}
if (ref $value eq 'ARRAY') {
# IN () is invalid SQL, and if we have an empty list
# to match against, we're just returning an empty
# array anyhow.
return [] if !scalar @$value;
my @qmarks = ("?") x @$value;
push(@terms, $dbh->sql_in($field, \@qmarks));
push(@values, @$value);
}
elsif ($value eq NOT_NULL) {
push(@terms, "$field IS NOT NULL");
}
elsif ($value eq IS_NULL) {
push(@terms, "$field IS NULL");
}
else {
push(@terms, "$field = ?");
push(@values, $value);
}
}
my $where = join(' AND ', @terms) if scalar @terms;
return $class->_do_list_select($where, \@values, $postamble);
}
sub _do_list_select {
my ($class, $where, $values, $postamble) = @_;
my $table = $class->DB_TABLE;
my $cols = join(',', $class->DB_COLUMNS);
my $order = $class->LIST_ORDER;
my $sql = "SELECT $cols FROM $table";
if (defined $where) {
$sql .= " WHERE $where ";
}
$sql .= " ORDER BY $order";
$sql .= " $postamble" if $postamble;
my $dbh = Bugzilla->dbh;
my $objects = $dbh->selectall_arrayref($sql, {Slice=>{}}, @$values);
bless ($_, $class) foreach @$objects;
return $objects
}
###############################
#### Accessors ######
###############################
sub id { return $_[0]->{$_[0]->ID_FIELD}; }
sub name { return $_[0]->{$_[0]->NAME_FIELD}; }
###############################
#### Methods ####
###############################
sub set {
my ($self, $field, $value) = @_;
# This method is protected. It's used to help implement set_ functions.
caller->isa('Bugzilla::Object')
|| ThrowCodeError('protection_violation',
{ caller => caller,
superclass => __PACKAGE__,
function => 'Bugzilla::Object->set' });
my %validators = (%{$self->VALIDATORS}, %{$self->UPDATE_VALIDATORS});
if (exists $validators{$field}) {
my $validator = $validators{$field};
$value = $self->$validator($value, $field);
trick_taint($value) if (defined $value && !ref($value));
if ($self->can('_set_global_validator')) {
$self->_set_global_validator($value, $field);
}
}
$self->{$field} = $value;
}
sub set_all {
my ($self, $params) = @_;
foreach my $key (keys %$params) {
my $method = "set_$key";
$self->$method($params->{$key});
}
}
sub update {
my $self = shift;
my $dbh = Bugzilla->dbh;
my $table = $self->DB_TABLE;
my $id_field = $self->ID_FIELD;
$dbh->bz_start_transaction();
my $old_self = $self->new($self->id);
my %numeric = map { $_ => 1 } $self->NUMERIC_COLUMNS;
my %date = map { $_ => 1 } $self->DATE_COLUMNS;
my (@update_columns, @values, %changes);
foreach my $column ($self->UPDATE_COLUMNS) {
my ($old, $new) = ($old_self->{$column}, $self->{$column});
# This has to be written this way in order to allow us to set a field
# from undef or to undef, and avoid warnings about comparing an undef
# with the "eq" operator.
if (!defined $new || !defined $old) {
next if !defined $new && !defined $old;
}
elsif ( ($numeric{$column} && $old == $new)
|| ($date{$column} && str2time($old) == str2time($new))
|| $old eq $new ) {
next;
}
trick_taint($new) if defined $new;
push(@values, $new);
push(@update_columns, $column);
# We don't use $new because we don't want to detaint this for
# the caller.
$changes{$column} = [$old, $self->{$column}];
}
my $columns = join(', ', map {"$_ = ?"} @update_columns);
$dbh->do("UPDATE $table SET $columns WHERE $id_field = ?", undef,
@values, $self->id) if @values;
$dbh->bz_commit_transaction();
if (wantarray) {
return (\%changes, $old_self);
}
return \%changes;
}
sub remove_from_db {
my $self = shift;
my $table = $self->DB_TABLE;
my $id_field = $self->ID_FIELD;
Bugzilla->dbh->do("DELETE FROM $table WHERE $id_field = ?",
undef, $self->id);
undef $self;
}
###############################
#### Subroutines ######
###############################
sub create {
my ($class, $params) = @_;
my $dbh = Bugzilla->dbh;
$dbh->bz_start_transaction();
$class->check_required_create_fields($params);
my $field_values = $class->run_create_validators($params);
my $object = $class->insert_create_data($field_values);
$dbh->bz_commit_transaction();
return $object;
}
sub check_required_create_fields {
my ($class, $params) = @_;
foreach my $field ($class->REQUIRED_CREATE_FIELDS) {
ThrowCodeError('param_required',
{ function => "${class}->create", param => $field })
if !exists $params->{$field};
}
}
sub run_create_validators {
my ($class, $params) = @_;
my $validators = $class->VALIDATORS;
my %field_values;
# We do the sort just to make sure that validation always
# happens in a consistent order.
foreach my $field (sort keys %$params) {
my $value;
if (exists $validators->{$field}) {
my $validator = $validators->{$field};
$value = $class->$validator($params->{$field}, $field);
}
else {
$value = $params->{$field};
}
# We want people to be able to explicitly set fields to NULL,
# and that means they can be set to undef.
trick_taint($value) if defined $value && !ref($value);
$field_values{$field} = $value;
}
return \%field_values;
}
sub insert_create_data {
my ($class, $field_values) = @_;
my $dbh = Bugzilla->dbh;
my (@field_names, @values);
while (my ($field, $value) = each %$field_values) {
push(@field_names, $field);
push(@values, $value);
}
my $qmarks = '?,' x @field_names;
chop($qmarks);
my $table = $class->DB_TABLE;
$dbh->do("INSERT INTO $table (" . join(', ', @field_names)
. ") VALUES ($qmarks)", undef, @values);
my $id = $dbh->bz_last_key($table, $class->ID_FIELD);
return $class->new($id);
}
sub get_all {
my $class = shift;
return @{$class->_do_list_select()};
}
###############################
#### Validators ######
###############################
sub check_boolean { return $_[1] ? 1 : 0 }
1;
__END__
=head1 NAME
Bugzilla::Object - A base class for objects in Bugzilla.
=head1 SYNOPSIS
my $object = new Bugzilla::Object(1);
my $object = new Bugzilla::Object({name => 'TestProduct'});
my $id = $object->id;
my $name = $object->name;
=head1 DESCRIPTION
Bugzilla::Object is a base class for Bugzilla objects. You never actually
create a Bugzilla::Object directly, you only make subclasses of it.
Basically, Bugzilla::Object exists to allow developers to create objects
more easily. All you have to do is define C<DB_TABLE>, C<DB_COLUMNS>,
and sometimes C<LIST_ORDER> and you have a whole new object.
You should also define accessors for any columns other than C<name>
or C<id>.
=head1 CONSTANTS
Frequently, these will be the only things you have to define in your
subclass in order to have a fully-functioning object. C<DB_TABLE>
and C<DB_COLUMNS> are required.
=over
=item C<DB_TABLE>
The name of the table that these objects are stored in. For example,
for C<Bugzilla::Keyword> this would be C<keyworddefs>.
=item C<DB_COLUMNS>
The names of the columns that you want to read out of the database
and into this object. This should be an array.
=item C<NAME_FIELD>
The name of the column that should be considered to be the unique
"name" of this object. The 'name' is a B<string> that uniquely identifies
this Object in the database. Defaults to 'name'. When you specify
C<{name => $name}> to C<new()>, this is the column that will be
matched against in the DB.
=item C<ID_FIELD>
The name of the column that represents the unique B<integer> ID
of this object in the database. Defaults to 'id'.
=item C<LIST_ORDER>
The order that C<new_from_list> and C<get_all> should return objects
in. This should be the name of a database column. Defaults to
L</NAME_FIELD>.
=item C<REQUIRED_CREATE_FIELDS>
The list of fields that B<must> be specified when the user calls
C<create()>. This should be an array.
=item C<VALIDATORS>
A hashref that points to a function that will validate each param to
L</create>.
Validators are called both by L</create> and L</set>. When
they are called by L</create>, the first argument will be the name
of the class (what we normally call C<$class>).
When they are called by L</set>, the first argument will be
a reference to the current object (what we normally call C<$self>).
The second argument will be the value passed to L</create> or
L</set>for that field.
The third argument will be the name of the field being validated.
This may be required by validators which validate several distinct fields.
These functions should call L<Bugzilla::Error/ThrowUserError> if they fail.
The validator must return the validated value.
=item C<UPDATE_VALIDATORS>
This is just like L</VALIDATORS>, but these validators are called only
when updating an object, not when creating it. Any validator that appears
here must not appear in L</VALIDATORS>.
L<Bugzilla::Bug> has good examples in its code of when to use this.
=item C<UPDATE_COLUMNS>
A list of columns to update when L</update> is called.
If a field can't be changed, it shouldn't be listed here. (For example,
the L</ID_FIELD> usually can't be updated.)
=item C<NUMERIC_COLUMNS>
When L</update> is called, it compares each column in the object to its
current value in the database. It only updates columns that have changed.
Any column listed in NUMERIC_COLUMNS is treated as a number, not as a string,
during these comparisons.
=item C<DATE_COLUMNS>
This is much like L</NUMERIC_COLUMNS>, except that it treats strings as
dates when being compared. So, for example, C<2007-01-01> would be
equal to C<2007-01-01 00:00:00>.
=back
=head1 METHODS
=head2 Constructors
=over
=item C<new>
=over
=item B<Description>
The constructor is used to load an existing object from the database,
by id or by name.
=item B<Params>
If you pass an integer, the integer is the id of the object,
from the database, that we want to read in. (id is defined
as the value in the L</ID_FIELD> column).
If you pass in a hashref, you can pass a C<name> key. The
value of the C<name> key is the case-insensitive name of the object
(from L</NAME_FIELD>) in the DB. You can also pass in an C<id> key
which will be interpreted as the id of the object you want (overriding the
C<name> key).
B<Additional Parameters Available for Subclasses>
If you are a subclass of C<Bugzilla::Object>, you can pass
C<condition> and C<values> as hash keys, instead of the above.
C<condition> is a set of SQL conditions for the WHERE clause, which contain
placeholders.
C<values> is a reference to an array. The array contains the values
for each placeholder in C<condition>, in order.
This is to allow subclasses to have complex parameters, and then to
translate those parameters into C<condition> and C<values> when they
call C<$self->SUPER::new> (which is this function, usually).
If you try to call C<new> outside of a subclass with the C<condition>
and C<values> parameters, Bugzilla will throw an error. These parameters
are intended B<only> for use by subclasses.
=item B<Returns>
A fully-initialized object, or C<undef> if there is no object in the
database matching the parameters you passed in.
=back
=item C<check>
=over
=item B<Description>
Checks if there is an object in the database with the specified name, and
throws an error if you specified an empty name, or if there is no object
in the database with that name.
=item B<Params>
The parameters are the same as for L</new>, except that if you don't pass
a hashref, the single argument is the I<name> of the object, not the id.
=item B<Returns>
A fully initialized object, guaranteed.
=item B<Notes For Implementors>
If you implement this in your subclass, make sure that you also update
the C<object_name> block at the bottom of the F<global/user-error.html.tmpl>
template.
=back
=item C<new_from_list(\@id_list)>
Description: Creates an array of objects, given an array of ids.
Params: \@id_list - A reference to an array of numbers, database ids.
If any of these are not numeric, the function
will throw an error. If any of these are not
valid ids in the database, they will simply
be skipped.
Returns: A reference to an array of objects.
=item C<match>
=over
=item B<Description>
Gets a list of objects from the database based on certain criteria.
Basically, a simple way of doing a sort of "SELECT" statement (like SQL)
to get objects.
All criteria are joined by C<AND>, so adding more criteria will give you
a smaller set of results, not a larger set.
=item B<Params>
A hashref, where the keys are column names of the table, pointing to the
value that you want to match against for that column.
There are two special values, the constants C<NULL> and C<NOT_NULL>,
which means "give me objects where this field is NULL or NOT NULL,
respectively."
In addition to the column keys, there are a few special keys that
can be used to rig the underlying database queries. These are
C<LIMIT>, C<OFFSET>, and C<WHERE>.
The value for the C<LIMIT> key is expected to be an integer defining
the number of objects to return, while the value for C<OFFSET> defines
the position, relative to the number of objects the query would normally
return, at which to begin the result set. If C<OFFSET> is defined without
a corresponding C<LIMIT> it is silently ignored.
The C<WHERE> key provides a mechanism for adding arbitrary WHERE
clauses to the underlying query. Its value is expected to a hash
reference whose keys are the columns, operators and placeholders, and the
values are the placeholders' bind value. For example:
WHERE => { 'some_column >= ?' => $some_value }
would constrain the query to only those objects in the table whose
'some_column' column has a value greater than or equal to $some_value.
If you don't specify any criteria, calling this function is the same
as doing C<[$class-E<gt>get_all]>.
=item B<Returns>
An arrayref of objects, or an empty arrayref if there are no matches.
=back
=back
=head2 Database Manipulation
=over
=item C<create>
Description: Creates a new item in the database.
Throws a User Error if any of the passed-in params
are invalid.
Params: C<$params> - hashref - A value to put in each database
field for this object. Certain values must be set (the
ones specified in L</REQUIRED_CREATE_FIELDS>), and
the function will throw a Code Error if you don't set
them.
Returns: The Object just created in the database.
Notes: In order for this function to work in your subclass,
your subclass's L</ID_FIELD> must be of C<SERIAL>
type in the database. Your subclass also must
define L</REQUIRED_CREATE_FIELDS> and L</VALIDATORS>.
Subclass Implementors: This function basically just
calls L</check_required_create_fields>, then
L</run_create_validators>, and then finally
L</insert_create_data>. So if you have a complex system that
you need to implement, you can do it by calling these
three functions instead of C<SUPER::create>.
=item C<check_required_create_fields>
=over
=item B<Description>
Part of L</create>. Throws an error if any of the L</REQUIRED_CREATE_FIELDS>
have not been specified in C<$params>
=item B<Params>
=over
=item C<$params> - The same as C<$params> from L</create>.
=back
=item B<Returns> (nothing)
=back
=item C<run_create_validators>
Description: Runs the validation of input parameters for L</create>.
This subroutine exists so that it can be overridden
by subclasses who need to do special validations
of their input parameters. This method is B<only> called
by L</create>.
Params: The same as L</create>.
Returns: A hash, in a similar format as C<$params>, except that
these are the values to be inserted into the database,
not the values that were input to L</create>.
=item C<insert_create_data>
Part of L</create>.
Takes the return value from L</run_create_validators> and inserts the
data into the database. Returns a newly created object.
=item C<update>
=over
=item B<Description>
Saves the values currently in this object to the database.
Only the fields specified in L</UPDATE_COLUMNS> will be
updated, and they will only be updated if their values have changed.
=item B<Params> (none)
=item B<Returns>
B<In scalar context:>
A hashref showing what changed during the update. The keys are the column
names from L</UPDATE_COLUMNS>. If a field was not changed, it will not be
in the hash at all. If the field was changed, the key will point to an arrayref.
The first item of the arrayref will be the old value, and the second item
will be the new value.
If there were no changes, we return a reference to an empty hash.
B<In array context:>
Returns a list, where the first item is the above hashref. The second item
is the object as it was in the database before update() was called. (This
is mostly useful to subclasses of C<Bugzilla::Object> that are implementing
C<update>.)
=back
=item C<remove_from_db>
Removes this object from the database. Will throw an error if you can't
remove it for some reason. The object will then be destroyed, as it is
not safe to use the object after it has been removed from the database.
=back
=head2 Mutators
These are used for updating the values in objects, before calling
C<update>.
=over
=item C<set>
=over
=item B<Description>
Sets a certain hash member of this class to a certain value.
Used for updating fields. Calls the validator for this field,
if it exists. Subclasses should use this function
to implement the various C<set_> mutators for their different
fields.
If your class defines a method called C<_set_global_validator>,
C<set> will call it with C<($value, $field)> as arguments, after running
the validator for this particular field. C<_set_global_validator> does not
return anything.
See L</VALIDATORS> for more information.
B<NOTE>: This function is intended only for use by subclasses. If
you call it from anywhere else, it will throw a C<CodeError>.
=item B<Params>
=over
=item C<$field> - The name of the hash member to update. This should
be the same as the name of the field in L</VALIDATORS>, if it exists there.
=item C<$value> - The value that you're setting the field to.
=back
=item B<Returns> (nothing)
=back
=item C<set_all>
=over
=item B<Description>
This is a convenience function which is simpler than calling many different
C<set_> functions in a row. You pass a hashref of parameters and it calls
C<set_$key($value)> for every item in the hashref.
=item B<Params>
Takes a hashref of the fields that need to be set, pointing to the value
that should be passed to the C<set_> function that is called.
=item B<Returns> (nothing)
=back
=back
=head2 Simple Validators
You can use these in your subclass L</VALIDATORS> or L</UPDATE_VALIDATORS>.
Note that you have to reference them like C<\&Bugzilla::Object::check_boolean>,
you can't just write C<\&check_boolean>.
=over
=item C<check_boolean>
Returns C<1> if the passed-in value is true, C<0> otherwise.
=back
=head1 CLASS FUNCTIONS
=over
=item C<get_all>
Description: Returns all objects in this table from the database.
Params: none.
Returns: A list of objects, or an empty list if there are none.
Notes: Note that you must call this as C<$class->get_all>. For
example, C<Bugzilla::Keyword->get_all>.
C<Bugzilla::Keyword::get_all> will not work.
=back
=cut

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,530 +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::Error;
use Bugzilla::Constants;
use Bugzilla::Keyword;
use Bugzilla::Status;
use Bugzilla::Field;
use Bugzilla::Util;
use base qw(Exporter);
@Bugzilla::Search::Quicksearch::EXPORT = qw(quicksearch);
# Word renamings
use constant MAPPINGS => {
# Status, Resolution, Platform, OS, Priority, Severity
"status" => "bug_status",
"resolution" => "resolution", # no change
"platform" => "rep_platform",
"os" => "op_sys",
"opsys" => "op_sys",
"priority" => "priority", # no change
"pri" => "priority",
"severity" => "bug_severity",
"sev" => "bug_severity",
# People: AssignedTo, Reporter, QA Contact, CC, Added comment (?)
"owner" => "assigned_to", # deprecated since bug 76507
"assignee" => "assigned_to",
"assignedto" => "assigned_to",
"reporter" => "reporter", # no change
"rep" => "reporter",
"qa" => "qa_contact",
"qacontact" => "qa_contact",
"cc" => "cc", # no change
# Product, Version, Component, Target Milestone
"product" => "product", # no change
"prod" => "product",
"version" => "version", # no change
"ver" => "version",
"component" => "component", # no change
"comp" => "component",
"milestone" => "target_milestone",
"target" => "target_milestone",
"targetmilestone" => "target_milestone",
# Summary, Description, URL, Status whiteboard, Keywords
"summary" => "short_desc",
"shortdesc" => "short_desc",
"desc" => "longdesc",
"description" => "longdesc",
#"comment" => "longdesc", # ???
# reserve "comment" for "added comment" email search?
"longdesc" => "longdesc",
"url" => "bug_file_loc",
"whiteboard" => "status_whiteboard",
"statuswhiteboard" => "status_whiteboard",
"sw" => "status_whiteboard",
"keywords" => "keywords", # no change
"kw" => "keywords",
"group" => "bug_group",
"flag" => "flagtypes.name",
"requestee" => "requestees.login_name",
"req" => "requestees.login_name",
"setter" => "setters.login_name",
"set" => "setters.login_name",
# Attachments
"attachment" => "attachments.description",
"attachmentdesc" => "attachments.description",
"attachdesc" => "attachments.description",
"attachmentdata" => "attach_data.thedata",
"attachdata" => "attach_data.thedata",
"attachmentmimetype" => "attachments.mimetype",
"attachmimetype" => "attachments.mimetype"
};
# We might want to put this into localconfig or somewhere
use constant PLATFORMS => ('pc', 'sun', 'macintosh', 'mac');
use constant OPSYSTEMS => ('windows', 'win', 'linux');
use constant PRODUCT_EXCEPTIONS => (
'row', # [Browser]
# ^^^
'new', # [MailNews]
# ^^^
);
use constant COMPONENT_EXCEPTIONS => (
'hang' # [Bugzilla: Component/Keyword Changes]
# ^^^^
);
# Quicksearch-wide globals for boolean charts.
our ($chart, $and, $or);
sub quicksearch {
my ($searchstring) = (@_);
my $cgi = Bugzilla->cgi;
my $urlbase = correct_urlbase();
$chart = 0;
$and = 0;
$or = 0;
# 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.
my $legal_statuses = get_legal_field_values('bug_status');
my $legal_resolutions = get_legal_field_values('resolution');
# Globally translate " AND ", " OR ", " NOT " to space, pipe, dash.
$searchstring =~ s/\s+AND\s+/ /g;
$searchstring =~ s/\s+OR\s+/|/g;
$searchstring =~ s/\s+NOT\s+/ -/g;
my @words = splitString($searchstring);
my $searchComments =
$#words < Bugzilla->params->{'quicksearch_comment_cutoff'};
my @openStates = BUG_STATE_OPEN;
my @closedStates;
my @unknownFields;
my (%states, %resolutions);
foreach (@$legal_statuses) {
push(@closedStates, $_) unless is_open_state($_);
}
foreach (@openStates) { $states{$_} = 1 }
if ($words[0] eq 'ALL') {
foreach (@$legal_statuses) { $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_resolutions)) {
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_statuses,
$legal_resolutions)) {
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', $_, $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 - 1, $negate);
}
elsif ($or_operand =~ /^(?:flag:)?([^\?]+\?)([^\?]*)$/) {
# Flag and requestee shortcut
addChart('flagtypes.name', 'substring', $1, $negate);
$chart++; $and = $or = 0; # Next chart for boolean AND
addChart('requestees.login_name', 'substring', $2, $negate);
}
elsif ($or_operand =~ /^([^:]+):([^:]+)$/) {
# generic field1,field2,field3:value1,value2 notation
my @fields = split(/,/, $1);
my @values = split(/,/, $2);
foreach my $field (@fields) {
# Skip and record any unknown fields
if (!defined(MAPPINGS->{$field})) {
push(@unknownFields, $field);
next;
}
$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 and operating system
if (grep({lc($word) eq $_} PLATFORMS)
|| grep({lc($word) eq $_} OPSYSTEMS)) {
addChart('rep_platform', 'substring',
$word, $negate);
addChart('op_sys', 'substring',
$word, $negate);
}
# Priority
elsif ($word =~ m/^[pP]([1-5](-[1-5])?)$/) {
addChart('priority', 'regexp',
"[$1]", $negate);
}
# Severity
elsif (grep({lc($word) eq substr($_, 0, 3)}
@{get_legal_field_values('bug_severity')})) {
addChart('bug_severity', 'substring',
$word, $negate);
}
# Votes (votes>xx)
elsif ($word =~ m/^votes>([0-9]+)$/) {
addChart('votes', 'greaterthan',
$1, $negate);
}
# Votes (votes>=xx, votes=>xx)
elsif ($word =~ m/^votes(>=|=>)([0-9]+)$/) {
addChart('votes', 'greaterthan',
$2-1, $negate);
}
else { # Default QuickSearch word
if (!grep({lc($word) eq $_}
PRODUCT_EXCEPTIONS) &&
length($word)>2
) {
addChart('product', 'substring',
$word, $negate);
}
if (!grep({lc($word) eq $_}
COMPONENT_EXCEPTIONS) &&
length($word)>2
) {
addChart('component', 'substring',
$word, $negate);
}
if (grep({lc($word) eq lc($_)}
map($_->name, Bugzilla::Keyword->get_all))) {
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)
# Inform user about any unknown fields
if (scalar(@unknownFields)) {
ThrowUserError("quicksearch_unknown_field",
{ fields => \@unknownFields });
}
# Make sure we have some query terms left
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&amp;"
. $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;
# Now split on quote sign; be tolerant about unclosed quotes
@quoteparts = split(/"/, $string);
foreach my $part (@quoteparts) {
# After every odd quote, quote special chars
$part = url_quote($part) if $i++ % 2;
}
# Join again
$string = join('"', @quoteparts);
# Now split on unescaped whitespace
@parts = split(/\s+/, $string);
foreach (@parts) {
# Protect plus signs from becoming a blank.
# If "+" appears as the first character, leave it alone
# as it has a special meaning. Strings which start with
# "+" must be quoted.
s/(?<!^)\+/%2B/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", url_decode($value));
}
1;

View File

@@ -1,314 +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 Everything Solved.
# Portions created by Everything Solved are Copyright (C) 2006
# Everything Solved. All Rights Reserved.
#
# Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org>
use strict;
package Bugzilla::Search::Saved;
use base qw(Bugzilla::Object);
use Bugzilla::CGI;
use Bugzilla::Constants;
use Bugzilla::Group;
use Bugzilla::Error;
use Bugzilla::Search qw(IsValidQueryType);
use Bugzilla::User;
use Bugzilla::Util;
#############
# Constants #
#############
use constant DB_TABLE => 'namedqueries';
use constant DB_COLUMNS => qw(
id
userid
name
query
query_type
);
use constant REQUIRED_CREATE_FIELDS => qw(name query);
use constant VALIDATORS => {
name => \&_check_name,
query => \&_check_query,
query_type => \&_check_query_type,
link_in_footer => \&_check_link_in_footer,
};
use constant UPDATE_COLUMNS => qw(name query query_type);
##############
# Validators #
##############
sub _check_link_in_footer { return $_[1] ? 1 : 0; }
sub _check_name {
my ($invocant, $name) = @_;
$name = trim($name);
$name || ThrowUserError("query_name_missing");
$name !~ /[<>&]/ || ThrowUserError("illegal_query_name");
if (length($name) > MAX_LEN_QUERY_NAME) {
ThrowUserError("query_name_too_long");
}
return $name;
}
sub _check_query {
my ($invocant, $query) = @_;
$query || ThrowUserError("buglist_parameters_required");
my $cgi = new Bugzilla::CGI($query);
$cgi->clean_search_url;
# Don't store the query name as a parameter.
$cgi->delete('known_name');
return $cgi->query_string;
}
sub _check_query_type {
my ($invocant, $type) = @_;
# Right now the only query type is LIST_OF_BUGS.
return $type ? LIST_OF_BUGS : QUERY_LIST;
}
#########################
# Database Manipulation #
#########################
sub create {
my $class = shift;
Bugzilla->login(LOGIN_REQUIRED);
my $dbh = Bugzilla->dbh;
$class->check_required_create_fields(@_);
$dbh->bz_start_transaction();
my $params = $class->run_create_validators(@_);
# Right now you can only create a Saved Search for the current user.
$params->{userid} = Bugzilla->user->id;
my $lif = delete $params->{link_in_footer};
my $obj = $class->insert_create_data($params);
if ($lif) {
$dbh->do('INSERT INTO namedqueries_link_in_footer
(user_id, namedquery_id) VALUES (?,?)',
undef, $params->{userid}, $obj->id);
}
$dbh->bz_commit_transaction();
return $obj;
}
sub preload {
my ($searches) = @_;
my $dbh = Bugzilla->dbh;
return unless scalar @$searches;
my @query_ids = map { $_->id } @$searches;
my $queries_in_footer = $dbh->selectcol_arrayref(
'SELECT namedquery_id
FROM namedqueries_link_in_footer
WHERE ' . $dbh->sql_in('namedquery_id', \@query_ids) . ' AND user_id = ?',
undef, Bugzilla->user->id);
my %links_in_footer = map { $_ => 1 } @$queries_in_footer;
foreach my $query (@$searches) {
$query->{link_in_footer} = ($links_in_footer{$query->id}) ? 1 : 0;
}
}
#####################
# Complex Accessors #
#####################
sub edit_link {
my ($self) = @_;
return $self->{edit_link} if defined $self->{edit_link};
my $cgi = new Bugzilla::CGI($self->url);
if (!$cgi->param('query_type')
|| !IsValidQueryType($cgi->param('query_type')))
{
$cgi->param('query_type', 'advanced');
}
$self->{edit_link} = $cgi->canonicalise_query;
return $self->{edit_link};
}
sub used_in_whine {
my ($self) = @_;
return $self->{used_in_whine} if exists $self->{used_in_whine};
($self->{used_in_whine}) = Bugzilla->dbh->selectrow_array(
'SELECT 1 FROM whine_events INNER JOIN whine_queries
ON whine_events.id = whine_queries.eventid
WHERE whine_events.owner_userid = ? AND query_name = ?', undef,
$self->{userid}, $self->name) || 0;
return $self->{used_in_whine};
}
sub link_in_footer {
my ($self, $user) = @_;
# We only cache link_in_footer for the current Bugzilla->user.
return $self->{link_in_footer} if exists $self->{link_in_footer} && !$user;
my $user_id = $user ? $user->id : Bugzilla->user->id;
my $link_in_footer = Bugzilla->dbh->selectrow_array(
'SELECT 1 FROM namedqueries_link_in_footer
WHERE namedquery_id = ? AND user_id = ?',
undef, $self->id, $user_id) || 0;
$self->{link_in_footer} = $link_in_footer if !$user;
return $link_in_footer;
}
sub shared_with_group {
my ($self) = @_;
return $self->{shared_with_group} if exists $self->{shared_with_group};
# Bugzilla only currently supports sharing with one group, even
# though the database backend allows for an infinite number.
my ($group_id) = Bugzilla->dbh->selectrow_array(
'SELECT group_id FROM namedquery_group_map WHERE namedquery_id = ?',
undef, $self->id);
$self->{shared_with_group} = $group_id ? new Bugzilla::Group($group_id)
: undef;
return $self->{shared_with_group};
}
sub shared_with_users {
my $self = shift;
my $dbh = Bugzilla->dbh;
if (!exists $self->{shared_with_users}) {
$self->{shared_with_users} =
$dbh->selectrow_array('SELECT COUNT(*)
FROM namedqueries_link_in_footer
INNER JOIN namedqueries
ON namedquery_id = id
WHERE namedquery_id = ?
AND user_id != userid',
undef, $self->id);
}
return $self->{shared_with_users};
}
####################
# Simple Accessors #
####################
sub bug_ids_only { return ($_[0]->{'query_type'} == LIST_OF_BUGS) ? 1 : 0; }
sub url { return $_[0]->{'query'}; }
sub user {
my ($self) = @_;
return $self->{user} if defined $self->{user};
$self->{user} = new Bugzilla::User($self->{userid});
return $self->{user};
}
############
# Mutators #
############
sub set_name { $_[0]->set('name', $_[1]); }
sub set_url { $_[0]->set('query', $_[1]); }
sub set_query_type { $_[0]->set('query_type', $_[1]); }
1;
__END__
=head1 NAME
Bugzilla::Search::Saved - A saved search
=head1 SYNOPSIS
use Bugzilla::Search::Saved;
my $query = new Bugzilla::Search::Saved($query_id);
my $edit_link = $query->edit_link;
my $search_url = $query->url;
my $owner = $query->user;
my $num_subscribers = $query->shared_with_users;
=head1 DESCRIPTION
This module exists to represent a L<Bugzilla::Search> that has been
saved to the database.
This is an implementation of L<Bugzilla::Object>, and so has all the
same methods available as L<Bugzilla::Object>, in addition to what is
documented below.
=head1 METHODS
=head2 Constructors and Database Manipulation
=over
=item C<new>
Does not accept a bare C<name> argument. Instead, accepts only an id.
See also: L<Bugzilla::Object/new>.
=item C<preload>
Sets C<link_in_footer> for all given saved searches at once, for the
currently logged in user. This is much faster than calling this method
for each saved search individually.
=back
=head2 Accessors
These return data about the object, without modifying the object.
=over
=item C<edit_link>
A url with which you can edit the search.
=item C<url>
The CGI parameters for the search, as a string.
=item C<link_in_footer>
Whether or not this search should be displayed in the footer for the
I<current user> (not the owner of the search, but the person actually
using Bugzilla right now).
=item C<bug_ids_only>
True if the search contains only a list of Bug IDs.
=item C<shared_with_group>
The L<Bugzilla::Group> that this search is shared with. C<undef> if
this search isn't shared.
=item C<shared_with_users>
Returns how many users (besides the author of the saved search) are
using the saved search, i.e. have it displayed in their footer.
=back

View File

@@ -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;
# 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::Error;
use Bugzilla::Util;
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'}) = @_;
# If the first parameter is undefined, check if this series already
# exists and update it series_id accordingly
$self->{'series_id'} ||= $self->existsInDatabase();
}
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 Bugzilla->user->in_group('admin');
}
sub writeToDatabase {
my $self = shift;
my $dbh = Bugzilla->dbh;
$dbh->bz_start_transaction();
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 " .
"(?, ?, ?, ?, ?, ?, ?)", undef,
$self->{'creator'}, $category_id, $subcategory_id, $self->{'name'},
$self->{'frequency'}, $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_commit_transaction();
}
# 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;

View File

@@ -1,342 +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 Frédéric Buclin.
# Portions created by Frédéric Buclin are Copyright (C) 2007
# Frédéric Buclin. All Rights Reserved.
#
# Contributor(s): Frédéric Buclin <LpSolit@gmail.com>
use strict;
package Bugzilla::Status;
use Bugzilla::Error;
use base qw(Bugzilla::Field::Choice Exporter);
@Bugzilla::Status::EXPORT = qw(
BUG_STATE_OPEN
SPECIAL_STATUS_WORKFLOW_ACTIONS
is_open_state
closed_bug_statuses
);
################################
##### Initialization #####
################################
use constant SPECIAL_STATUS_WORKFLOW_ACTIONS => qw(
none
duplicate
change_resolution
clearresolution
);
use constant DB_TABLE => 'bug_status';
# This has all the standard Bugzilla::Field::Choice columns plus "is_open"
sub DB_COLUMNS {
return ($_[0]->SUPER::DB_COLUMNS, 'is_open');
}
sub VALIDATORS {
my $invocant = shift;
my $validators = $invocant->SUPER::VALIDATORS;
$validators->{is_open} = \&Bugzilla::Object::check_boolean;
$validators->{value} = \&_check_value;
return $validators;
}
#########################
# Database Manipulation #
#########################
sub create {
my $class = shift;
my $self = $class->SUPER::create(@_);
add_missing_bug_status_transitions();
return $self;
}
sub remove_from_db {
my $self = shift;
my $dbh = Bugzilla->dbh;
my $id = $self->id;
$dbh->bz_start_transaction();
$self->SUPER::remove_from_db();
$dbh->do('DELETE FROM status_workflow
WHERE old_status = ? OR new_status = ?',
undef, $id, $id);
$dbh->bz_commit_transaction();
}
###############################
##### Accessors ####
###############################
sub is_active { return $_[0]->{'isactive'}; }
sub is_open { return $_[0]->{'is_open'}; }
sub is_static {
my $self = shift;
if ($self->name eq 'UNCONFIRMED'
|| $self->name eq Bugzilla->params->{'duplicate_or_move_bug_status'})
{
return 1;
}
return 0;
}
##############
# Validators #
##############
sub _check_value {
my $invocant = shift;
my $value = $invocant->SUPER::_check_value(@_);
if (grep { lc($value) eq lc($_) } SPECIAL_STATUS_WORKFLOW_ACTIONS) {
ThrowUserError('fieldvalue_reserved_word',
{ field => $invocant->field, value => $value });
}
return $value;
}
###############################
##### Methods ####
###############################
sub BUG_STATE_OPEN {
# XXX - We should cache this list.
my $dbh = Bugzilla->dbh;
return @{$dbh->selectcol_arrayref('SELECT value FROM bug_status WHERE is_open = 1')};
}
# Tells you whether or not the argument is a valid "open" state.
sub is_open_state {
my ($state) = @_;
return (grep($_ eq $state, BUG_STATE_OPEN) ? 1 : 0);
}
sub closed_bug_statuses {
my @bug_statuses = Bugzilla::Status->get_all;
@bug_statuses = grep { !$_->is_open } @bug_statuses;
return @bug_statuses;
}
sub can_change_to {
my $self = shift;
my $dbh = Bugzilla->dbh;
if (!ref($self) || !defined $self->{'can_change_to'}) {
my ($cond, @args, $self_exists);
if (ref($self)) {
$cond = '= ?';
push(@args, $self->id);
$self_exists = 1;
}
else {
$cond = 'IS NULL';
# Let's do it so that the code below works in all cases.
$self = {};
}
my $new_status_ids = $dbh->selectcol_arrayref("SELECT new_status
FROM status_workflow
INNER JOIN bug_status
ON id = new_status
WHERE isactive = 1
AND old_status $cond
ORDER BY sortkey",
undef, @args);
# Allow the bug status to remain unchanged.
push(@$new_status_ids, $self->id) if $self_exists;
$self->{'can_change_to'} = Bugzilla::Status->new_from_list($new_status_ids);
}
return $self->{'can_change_to'};
}
sub can_change_from {
my $self = shift;
my $dbh = Bugzilla->dbh;
if (!defined $self->{'can_change_from'}) {
my $old_status_ids = $dbh->selectcol_arrayref('SELECT old_status
FROM status_workflow
INNER JOIN bug_status
ON id = old_status
WHERE isactive = 1
AND new_status = ?
AND old_status IS NOT NULL',
undef, $self->id);
# Allow the bug status to remain unchanged.
push(@$old_status_ids, $self->id);
$self->{'can_change_from'} = Bugzilla::Status->new_from_list($old_status_ids);
}
return $self->{'can_change_from'};
}
sub comment_required_on_change_from {
my ($self, $old_status) = @_;
my ($cond, $values) = $self->_status_condition($old_status);
my ($require_comment) = Bugzilla->dbh->selectrow_array(
"SELECT require_comment FROM status_workflow
WHERE $cond", undef, @$values);
return $require_comment;
}
# Used as a helper for various functions that have to deal with old_status
# sometimes being NULL and sometimes having a value.
sub _status_condition {
my ($self, $old_status) = @_;
my @values;
my $cond = 'old_status IS NULL';
# For newly-filed bugs
if ($old_status) {
$cond = 'old_status = ?';
push(@values, $old_status->id);
}
$cond .= " AND new_status = ?";
push(@values, $self->id);
return ($cond, \@values);
}
sub add_missing_bug_status_transitions {
my $bug_status = shift || Bugzilla->params->{'duplicate_or_move_bug_status'};
my $dbh = Bugzilla->dbh;
my $new_status = new Bugzilla::Status({name => $bug_status});
# Silently discard invalid bug statuses.
$new_status || return;
my $missing_statuses = $dbh->selectcol_arrayref('SELECT id
FROM bug_status
LEFT JOIN status_workflow
ON old_status = id
AND new_status = ?
WHERE old_status IS NULL',
undef, $new_status->id);
my $sth = $dbh->prepare('INSERT INTO status_workflow
(old_status, new_status) VALUES (?, ?)');
foreach my $old_status_id (@$missing_statuses) {
next if ($old_status_id == $new_status->id);
$sth->execute($old_status_id, $new_status->id);
}
}
1;
__END__
=head1 NAME
Bugzilla::Status - Bug status class.
=head1 SYNOPSIS
use Bugzilla::Status;
my $bug_status = new Bugzilla::Status({name => 'ASSIGNED'});
my $bug_status = new Bugzilla::Status(4);
my @closed_bug_statuses = closed_bug_statuses();
Bugzilla::Status::add_missing_bug_status_transitions($bug_status);
=head1 DESCRIPTION
Status.pm represents a bug status object. It is an implementation
of L<Bugzilla::Object>, and thus provides all methods that
L<Bugzilla::Object> provides.
The methods that are specific to C<Bugzilla::Status> are listed
below.
=head1 METHODS
=over
=item C<closed_bug_statuses>
Description: Returns a list of C<Bugzilla::Status> objects which can have
a resolution associated with them ("closed" bug statuses).
Params: none.
Returns: A list of Bugzilla::Status objects.
=item C<can_change_to>
Description: Returns the list of active statuses a bug can be changed to
given the current bug status. If this method is called as a
class method, then it returns all bug statuses available on
bug creation.
Params: none.
Returns: A list of Bugzilla::Status objects.
=item C<can_change_from>
Description: Returns the list of active statuses a bug can be changed from
given the new bug status. If the bug status is available on
bug creation, this method doesn't return this information.
You have to call C<can_change_to> instead.
Params: none.
Returns: A list of Bugzilla::Status objects.
=item C<comment_required_on_change_from>
=over
=item B<Description>
Checks if a comment is required to change to this status from another
status, according to the current settings in the workflow.
Note that this doesn't implement the checks enforced by the various
C<commenton> parameters--those are checked by internal checks in
L<Bugzilla::Bug>.
=item B<Params>
C<$old_status> - The status you're changing from.
=item B<Returns>
C<1> if a comment is required on this change, C<0> if not.
=back
=item C<add_missing_bug_status_transitions>
Description: Insert all missing transitions to a given bug status.
Params: $bug_status - The value (name) of a bug status.
Returns: nothing.
=back
=cut

View File

@@ -1,953 +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>
# David D. Kilzer <ddkilzer@kilzer.net>
package Bugzilla::Template;
use strict;
use Bugzilla::Constants;
use Bugzilla::Install::Requirements;
use Bugzilla::Install::Util qw(install_string template_include_path include_languages);
use Bugzilla::Util;
use Bugzilla::User;
use Bugzilla::Error;
use Bugzilla::Status;
use Bugzilla::Token;
use Bugzilla::Template::Parser;
use Cwd qw(abs_path);
use MIME::Base64;
use Date::Format ();
use File::Basename qw(dirname);
use File::Find;
use File::Path qw(rmtree mkpath);
use File::Spec;
use IO::Dir;
use base qw(Template);
# As per the Template::Base documentation, the _init() method is being called
# by the new() constructor. We take advantage of this in order to plug our
# UTF-8-aware Parser object in neatly after the original _init() method has
# happened, in particular, after having set up the constants namespace.
# See bug 413121 for details.
sub _init {
my $self = shift;
my $config = $_[0];
$self->SUPER::_init(@_) || return undef;
$self->{PARSER} = $config->{PARSER}
= new Bugzilla::Template::Parser($config);
# Now we need to re-create the default Service object, making it aware
# of our Parser object.
$self->{SERVICE} = $config->{SERVICE}
= Template::Config->service($config);
return $self;
}
# 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 and ignoring the rest
# (which, if Constants.pm exports only constants, as it should, will be nothing else).
sub _load_constants {
my %constants;
foreach my $constant (@Bugzilla::Constants::EXPORT,
@Bugzilla::Constants::EXPORT_OK)
{
if (ref Bugzilla::Constants->$constant) {
$constants{$constant} = Bugzilla::Constants->$constant;
}
else {
my @list = (Bugzilla::Constants->$constant);
$constants{$constant} = (scalar(@list) == 1) ? $list[0] : \@list;
}
}
return \%constants;
}
# 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
# Templates may also be found in the extensions/ tree
sub getTemplateIncludePath {
my $cache = Bugzilla->request_cache;
my $lang = $cache->{'language'} || '';
$cache->{"template_include_path_$lang"} ||= template_include_path({
use_languages => Bugzilla->languages,
only_language => $lang });
return $cache->{"template_include_path_$lang"};
}
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 it's 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}
};
}
# This routine quoteUrls contains inspirations from the HTML::FromText CPAN
# module by Gareth Rees <garethr@cre.canon.co.uk>. It has been heavily hacked,
# all that is really recognizable from the original is bits of the regular
# expressions.
# This has been rewritten to be faster, mainly by substituting 'as we go'.
# If you want to modify this routine, read the comments carefully
sub quoteUrls {
my ($text, $curr_bugid, $already_wrapped) = (@_);
return $text unless $text;
# We use /g for speed, but uris can have other things inside them
# (http://foo/bug#3 for example). Filtering that out filters valid
# bug refs out, so we have to do replacements.
# mailto can't contain space or #, so we don't have to bother for that
# Do this by escaping \0 to \1\0, and replacing matches with \0\0$count\0\0
# \0 is used because it's unlikely to occur in the text, so the cost of
# doing this should be very small
# escape the 2nd escape char we're using
my $chr1 = chr(1);
$text =~ s/\0/$chr1\0/g;
# If the comment is already wrapped, we should ignore newlines when
# looking for matching regexps. Else we should take them into account.
my $s = $already_wrapped ? qr/\s/ : qr/[[:blank:]]/;
# However, note that adding the title (for buglinks) can affect things
# In particular, attachment matches go before bug titles, so that titles
# with 'attachment 1' don't double match.
# Dupe checks go afterwards, because that uses ^ and \Z, which won't occur
# if it was substituted as a bug title (since that always involve leading
# and trailing text)
# Because of entities, it's easier (and quicker) to do this before escaping
my @things;
my $count = 0;
my $tmp;
# Provide tooltips for full bug links (Bug 74355)
my $urlbase_re = '(' . join('|',
map { qr/$_/ } grep($_, Bugzilla->params->{'urlbase'},
Bugzilla->params->{'sslbase'})) . ')';
$text =~ s~\b(${urlbase_re}\Qshow_bug.cgi?id=\E([0-9]+)(\#c([0-9]+))?)\b
~($things[$count++] = get_bug_link($3, $1, { comment_num => $5 })) &&
("\0\0" . ($count-1) . "\0\0")
~egox;
# non-mailto protocols
my $safe_protocols = join('|', SAFE_PROTOCOLS);
my $protocol_re = qr/($safe_protocols)/i;
$text =~ s~\b(${protocol_re}: # The protocol:
[^\s<>\"]+ # Any non-whitespace
[\w\/]) # so that we end in \w or /
~($tmp = html_quote($1)) &&
($things[$count++] = "<a href=\"$tmp\">$tmp</a>") &&
("\0\0" . ($count-1) . "\0\0")
~egox;
# We have to quote now, otherwise the html itself is escaped
# THIS MEANS THAT A LITERAL ", <, >, ' MUST BE ESCAPED FOR A MATCH
$text = html_quote($text);
# Color quoted text
$text =~ s~^(&gt;.+)$~<span class="quote">$1</span >~mg;
$text =~ s~</span >\n<span class="quote">~\n~g;
# mailto:
# Use |<nothing> so that $1 is defined regardless
$text =~ s~\b(mailto:|)?([\w\.\-\+\=]+\@[\w\-]+(?:\.[\w\-]+)+)\b
~<a href=\"mailto:$2\">$1$2</a>~igx;
# attachment links - handle both cases separately for simplicity
$text =~ s~((?:^Created\ an\ |\b)attachment\s*\(id=(\d+)\)(\s\[edit\])?)
~($things[$count++] = get_attachment_link($2, $1)) &&
("\0\0" . ($count-1) . "\0\0")
~egmx;
$text =~ s~\b(attachment$s*\#?$s*(\d+))
~($things[$count++] = get_attachment_link($2, $1)) &&
("\0\0" . ($count-1) . "\0\0")
~egmxi;
# Current bug ID this comment belongs to
my $current_bugurl = $curr_bugid ? "show_bug.cgi?id=$curr_bugid" : "";
# This handles bug a, comment b type stuff. Because we're using /g
# we have to do this in one pattern, and so this is semi-messy.
# Also, we can't use $bug_re?$comment_re? because that will match the
# empty string
my $bug_word = get_text('term', { term => 'bug' });
my $bug_re = qr/\Q$bug_word\E$s*\#?$s*(\d+)/i;
my $comment_re = qr/comment$s*\#?$s*(\d+)/i;
$text =~ s~\b($bug_re(?:$s*,?$s*$comment_re)?|$comment_re)
~ # We have several choices. $1 here is the link, and $2-4 are set
# depending on which part matched
(defined($2) ? get_bug_link($2, $1, { comment_num => $3 }) :
"<a href=\"$current_bugurl#c$4\">$1</a>")
~egox;
# Old duplicate markers. These don't use $bug_word because they are old
# and were never customizable.
$text =~ s~(?<=^\*\*\*\ This\ bug\ has\ been\ marked\ as\ a\ duplicate\ of\ )
(\d+)
(?=\ \*\*\*\Z)
~get_bug_link($1, $1)
~egmx;
# Now remove the encoding hacks
$text =~ s/\0\0(\d+)\0\0/$things[$1]/eg;
$text =~ s/$chr1\0/\0/g;
return $text;
}
# Creates a link to an attachment, including its title.
sub get_attachment_link {
my ($attachid, $link_text) = @_;
my $dbh = Bugzilla->dbh;
detaint_natural($attachid)
|| die "get_attachment_link() called with non-integer attachment number";
my ($bugid, $isobsolete, $desc) =
$dbh->selectrow_array('SELECT bug_id, isobsolete, description
FROM attachments WHERE attach_id = ?',
undef, $attachid);
if ($bugid) {
my $title = "";
my $className = "";
if (Bugzilla->user->can_see_bug($bugid)) {
$title = $desc;
}
if ($isobsolete) {
$className = "bz_obsolete";
}
# Prevent code injection in the title.
$title = html_quote(clean_text($title));
$link_text =~ s/ \[details\]$//;
my $linkval = "attachment.cgi?id=$attachid";
# Whitespace matters here because these links are in <pre> tags.
return qq|<span class="$className">|
. qq|<a href="${linkval}" name="attach_${attachid}" title="$title">$link_text</a>|
. qq| <a href="${linkval}&amp;action=edit" title="$title">[details]</a>|
. qq|</span>|;
}
else {
return qq{$link_text};
}
}
# Creates a link to a bug, including its title.
# It takes either two or three parameters:
# - The bug number
# - The link text, to place between the <a>..</a>
# - An optional comment number, for linking to a particular
# comment in the bug
sub get_bug_link {
my ($bug_num, $link_text, $options) = @_;
my $dbh = Bugzilla->dbh;
if (!defined($bug_num) || ($bug_num eq "")) {
return "&lt;missing bug number&gt;";
}
my $quote_bug_num = html_quote($bug_num);
detaint_natural($bug_num) || return "&lt;invalid bug number: $quote_bug_num&gt;";
my ($bug_alias, $bug_state, $bug_res, $bug_desc) =
$dbh->selectrow_array('SELECT bugs.alias, bugs.bug_status, bugs.resolution, bugs.short_desc
FROM bugs WHERE bugs.bug_id = ?',
undef, $bug_num);
if ($options->{use_alias} && $link_text =~ /^\d+$/ && $bug_alias) {
$link_text = $bug_alias;
}
if ($bug_state) {
# Initialize these variables to be "" so that we don't get warnings
# if we don't change them below (which is highly likely).
my ($pre, $title, $post) = ("", "", "");
$title = get_text('get_status', {status => $bug_state});
if ($bug_state eq 'UNCONFIRMED') {
$pre = "<i>";
$post = "</i>";
}
elsif (!is_open_state($bug_state)) {
$pre = '<span class="bz_closed">';
$title .= ' ' . get_text('get_resolution', {resolution => $bug_res});
$post = '</span>';
}
if (Bugzilla->user->can_see_bug($bug_num)) {
$title .= " - $bug_desc";
}
# Prevent code injection in the title.
$title = html_quote(clean_text($title));
my $linkval = "show_bug.cgi?id=$bug_num";
if ($options->{comment_num}) {
$linkval .= "#c" . $options->{comment_num};
}
return qq{$pre<a href="$linkval" title="$title">$link_text</a>$post};
}
else {
return qq{$link_text};
}
}
###############################################################################
# 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;
};
# Clone the array reference to leave the original one unaltered.
$Template::Stash::LIST_OPS->{ clone } =
sub {
my $list = shift;
return [@$list];
};
# 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'}) {
delete Bugzilla->request_cache->{template_include_path_};
}
# 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 => bz_locations()->{'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 &#013;
# See bugs 4928, 22983 and 32000 for more details
html_linebreak => sub {
my ($var) = @_;
$var =~ s/\r\n/\&#013;/g;
$var =~ s/\n\r/\&#013;/g;
$var =~ s/\r/\&#013;/g;
$var =~ s/\n/\&#013;/g;
return $var;
},
# Prevents line break on hyphens and whitespaces.
no_break => sub {
my ($var) = @_;
$var =~ s/ /\&nbsp;/g;
$var =~ s/-/\&#8209;/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, $already_wrapped) = @_;
return sub {
my $text = shift;
return quoteUrls($text, $bug, $already_wrapped);
};
},
1
],
bug_link => [ sub {
my ($context, $bug, $options) = @_;
return sub {
my $text = shift;
return get_bug_link($bug, $text, $options);
};
},
1
],
bug_list_link => sub
{
my $buglist = shift;
return join(", ", map(get_bug_link($_, $_), split(/ *, */, $buglist)));
},
# 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 => [ sub {
my ($context, $format, $timezone) = @_;
return sub {
my $time = shift;
return format_time($time, $format, $timezone);
};
},
1
],
# Bug 120030: Override html filter to obscure the '@' in user
# visible strings.
# Bug 319331: Handle BiDi disruptions.
html => sub {
my ($var) = Template::Filters::html_filter(@_);
# Obscure '@'.
$var =~ s/\@/\&#64;/g;
if (Bugzilla->params->{'utf8'}) {
# Remove the following characters because they're
# influencing BiDi:
# --------------------------------------------------------
# |Code |Name |UTF-8 representation|
# |------|--------------------------|--------------------|
# |U+202a|Left-To-Right Embedding |0xe2 0x80 0xaa |
# |U+202b|Right-To-Left Embedding |0xe2 0x80 0xab |
# |U+202c|Pop Directional Formatting|0xe2 0x80 0xac |
# |U+202d|Left-To-Right Override |0xe2 0x80 0xad |
# |U+202e|Right-To-Left Override |0xe2 0x80 0xae |
# --------------------------------------------------------
#
# The following are characters influencing BiDi, too, but
# they can be spared from filtering because they don't
# influence more than one character right or left:
# --------------------------------------------------------
# |Code |Name |UTF-8 representation|
# |------|--------------------------|--------------------|
# |U+200e|Left-To-Right Mark |0xe2 0x80 0x8e |
# |U+200f|Right-To-Left Mark |0xe2 0x80 0x8f |
# --------------------------------------------------------
$var =~ s/[\x{202a}-\x{202e}]//g;
}
return $var;
},
html_light => \&Bugzilla::Util::html_light_quote,
email => \&Bugzilla::Util::email_filter,
# 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
],
# Note that using this filter is even more dangerous than
# using "none," and you should only use it when you're SURE
# the output won't be displayed directly to a web browser.
txt => sub {
my ($var) = @_;
# Trivial HTML tag remover
$var =~ s/<[^>]*>//g;
# And this basically reverses the html filter.
$var =~ s/\&#64;/@/g;
$var =~ s/\&lt;/</g;
$var =~ s/\&gt;/>/g;
$var =~ s/\&quot;/\"/g;
$var =~ s/\&amp;/\&/g;
# Now remove extra whitespace, and wrap it to 72 characters.
my $collapse_filter = $Template::Filters::FILTERS->{collapse};
$var = $collapse_filter->($var);
$var = wrap_comment($var, 72);
return $var;
},
# Wrap a displayed comment to the appropriate length
wrap_comment => [
sub {
my ($context, $cols) = @_;
return sub { wrap_comment($_[0], $cols) }
}, 1],
# 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 => _load_constants(),
# Default variables for all templates
VARIABLES => {
# Function for retrieving global parameters.
'Param' => sub { return Bugzilla->params->{$_[0]}; },
# 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; },
# SendBugMail - sends mail about a bug, using Bugzilla::BugMail.pm
'SendBugMail' => sub {
my ($id, $mailrecipients) = (@_);
require Bugzilla::BugMail;
Bugzilla::BugMail::Send($id, $mailrecipients);
},
# Allow templates to access the "corect" URLBase value
'urlbase' => sub { return Bugzilla::Util::correct_urlbase(); },
# Allow templates to access docs url with users' preferred language
'docs_urlbase' => sub {
my ($language) = include_languages();
my $docs_urlbase = Bugzilla->params->{'docs_urlbase'};
$docs_urlbase =~ s/\%lang\%/$language/;
return $docs_urlbase;
},
# Allow templates to generate a token themselves.
'issue_hash_token' => \&Bugzilla::Token::issue_hash_token,
# A way for all templates to get at Field data, cached.
'bug_fields' => sub {
my $cache = Bugzilla->request_cache;
$cache->{template_bug_fields} ||=
{ map { $_->name => $_ } Bugzilla->get_fields() };
return $cache->{template_bug_fields};
},
# These don't work as normal constants.
DB_MODULE => \&Bugzilla::Constants::DB_MODULE,
REQUIRED_MODULES =>
\&Bugzilla::Install::Requirements::REQUIRED_MODULES,
OPTIONAL_MODULES => sub {
my @optional = @{OPTIONAL_MODULES()};
@optional = sort {$a->{feature} cmp $b->{feature}}
@optional;
return \@optional;
},
},
}) || die("Template creation failed: " . $class->error());
}
# Used as part of the two subroutines below.
our (%_templates_to_precompile, $_current_path);
sub precompile_templates {
my ($output) = @_;
# Remove the compiled templates.
my $datadir = bz_locations()->{'datadir'};
if (-e "$datadir/template") {
print install_string('template_removing_dir') . "\n" if $output;
# XXX This frequently fails if the webserver made the files, because
# then the webserver owns the directories. We could fix that by
# doing a chmod/chown on all the directories here.
rmtree("$datadir/template");
# Check that the directory was really removed
if(-e "$datadir/template") {
print "\n\n";
print "The directory '$datadir/template' could not be removed.\n";
print "Please remove it manually and rerun checksetup.pl.\n\n";
exit;
}
}
print install_string('template_precompile') if $output;
my $templatedir = bz_locations()->{'templatedir'};
# Don't hang on templates which use the CGI library
eval("use CGI qw(-no_debug)");
my $dir_reader = new IO::Dir($templatedir) || die "$templatedir: $!";
my @language_dirs = grep { /^[a-z-]+$/i } $dir_reader->read;
$dir_reader->close;
foreach my $dir (@language_dirs) {
next if ($dir eq 'CVS');
-d "$templatedir/$dir/default" || -d "$templatedir/$dir/custom"
|| next;
local $ENV{'HTTP_ACCEPT_LANGUAGE'} = $dir;
my $template = Bugzilla::Template->create(clean_cache => 1);
# Precompile all the templates found in all the directories.
%_templates_to_precompile = ();
foreach my $subdir (qw(custom extension default), bz_locations()->{'project'}) {
next unless $subdir; # If 'project' is empty.
$_current_path = File::Spec->catdir($templatedir, $dir, $subdir);
next unless -d $_current_path;
# Traverse the template hierarchy.
find({ wanted => \&_precompile_push, no_chdir => 1 }, $_current_path);
}
# The sort isn't totally necessary, but it makes debugging easier
# by making the templates always be compiled in the same order.
foreach my $file (sort keys %_templates_to_precompile) {
# Compile the template but throw away the result. This has the side-
# effect of writing the compiled version to disk.
$template->context->template($file);
}
}
# Under mod_perl, we look for templates using the absolute path of the
# template directory, which causes Template Toolkit to look for their
# *compiled* versions using the full absolute path under the data/template
# directory. (Like data/template/var/www/html/mod_perl/.) To avoid
# re-compiling templates under mod_perl, we symlink to the
# already-compiled templates. This doesn't work on Windows.
if (!ON_WINDOWS) {
my $abs_root = dirname(abs_path($templatedir));
my $todir = "$datadir/template$abs_root";
mkpath($todir);
# We use abs2rel so that the symlink will look like
# "../../../../template" which works, while just
# "data/template/template/" doesn't work.
my $fromdir = File::Spec->abs2rel("$datadir/template/template", $todir);
# We eval for systems that can't symlink at all, where "symlink"
# throws a fatal error.
eval { symlink($fromdir, "$todir/template")
or warn "Failed to symlink from $fromdir to $todir: $!" };
}
# If anything created a Template object before now, clear it out.
delete Bugzilla->request_cache->{template};
# This is the single variable used to precompile templates,
# which needs to be cleared as well.
delete Bugzilla->request_cache->{template_include_path_};
print install_string('done') . "\n" if $output;
}
# Helper for precompile_templates
sub _precompile_push {
my $name = $File::Find::name;
return if (-d $name);
return if ($name =~ /\/CVS\//);
return if ($name !~ /\.tmpl$/);
$name =~ s/\Q$_current_path\E\///;
$_templates_to_precompile{$name} = 1;
}
1;
__END__
=head1 NAME
Bugzilla::Template - Wrapper around the Template Toolkit C<Template> object
=head1 SYNOPSIS
my $template = Bugzilla::Template->create;
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 SUBROUTINES
=over
=item C<precompile_templates($output)>
Description: Compiles all of Bugzilla's templates in every language.
Used mostly by F<checksetup.pl>.
Params: C<$output> - C<true> if you want the function to print
out information about what it's doing.
Returns: nothing
=back
=head1 METHODS
=over
=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>

View File

@@ -1,66 +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 Marc Schumann.
# Portions created by Marc Schumann are Copyright (C) 2008 Marc Schumann.
# All Rights Reserved.
#
# Contributor(s): Marc Schumann <wurblzap@gmail.com>
package Bugzilla::Template::Parser;
use strict;
use base qw(Template::Parser);
sub parse {
my ($self, $text, @params) = @_;
if (Bugzilla->params->{'utf8'}) {
utf8::is_utf8($text) || utf8::decode($text);
}
return $self->SUPER::parse($text, @params);
}
1;
__END__
=head1 NAME
Bugzilla::Template::Parser - Wrapper around the Template Toolkit
C<Template::Parser> object
=head1 DESCRIPTION
This wrapper makes the Template Toolkit aware of UTF-8 templates.
=head1 SUBROUTINES
=over
=item C<parse($options)>
Description: Parses template text using Template::Parser::parse(),
converting the text to UTF-8 encoding before, if necessary.
Params: C<$text> - Text to pass to Template::Parser::parse().
Returns: Parsed text as returned by Template::Parser::parse().
=back
=head1 SEE ALSO
L<Template>

View File

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

View File

@@ -1,172 +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>
# Zach Lipton <zach@zachlipton.com>
# Elliotte Martin <everythingsolved.com>
#
package Bugzilla::Template::Plugin::Hook;
use strict;
use Bugzilla::Constants;
use Bugzilla::Install::Util qw(include_languages);
use Bugzilla::Template;
use Bugzilla::Util;
use Bugzilla::Error;
use File::Spec;
use base qw(Template::Plugin);
sub load {
my ($class, $context) = @_;
return $class;
}
sub new {
my ($class, $context) = @_;
return bless { _CONTEXT => $context }, $class;
}
sub process {
my ($self, $hook_name, $template) = @_;
$template ||= $self->{_CONTEXT}->stash->{component}->{name};
my @hooks;
# sanity check:
if (!$template =~ /[\w\.\/\-_\\]+/) {
ThrowCodeError('template_invalid', { name => $template});
}
# also get extension hook files that live in extensions/:
# parse out the parts of the template name
my ($vol, $subpath, $filename) = File::Spec->splitpath($template);
$subpath = $subpath || '';
$filename =~ m/(.*)\.(.*)\.tmpl/;
my $templatename = $1;
my $type = $2;
# munge the filename to create the extension hook filename:
my $extensiontemplate = $subpath.'/'.$templatename.'-'.$hook_name.'.'.$type.'.tmpl';
my @extensions = glob(bz_locations()->{'extensionsdir'} . "/*");
my @usedlanguages = include_languages({use_languages => Bugzilla->languages});
foreach my $extension (@extensions) {
next if -e "$extension/disabled";
foreach my $language (@usedlanguages) {
my $file = $extension.'/template/'.$language.'/'.$extensiontemplate;
if (-e $file) {
# tt is stubborn and won't take a template file not in its
# include path, so we open a filehandle and give it to process()
# so the hook gets invoked:
open (my $fh, $file);
push(@hooks, $fh);
}
}
}
my $paths = $self->{_CONTEXT}->{LOAD_TEMPLATES}->[0]->paths;
# we keep this too since you can still put hook templates in
# template/en/custom/hook
foreach my $path (@$paths) {
my @files = glob("$path/hook/$template/$hook_name/*.tmpl");
# 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 METHODS
=over
=item B<process>
=over
=item B<Description>
Processes hooks added into templates by extensions.
=item B<Params>
=over
=item C<hook_name>
The unique name of the template hook.
=item C<template> (optional)
The path of the calling template.
This is used as a work around to a bug which causes the path to the hook
to be incorrect when the hook is called from inside a block.
Example: If the hook C<lastrow> is added to the template
F<show-multiple.html.tmpl> and it is desired to force the correct template
path, the template hook would be:
[% Hook.process("lastrow", "bug/show-multiple.html.tmpl") %]
=back
=item B<Returns>
Output from processing template extension.
=back
=back
=head1 SEE ALSO
L<Template::Plugin>
L<http://www.bugzilla.org/docs/tip/html/customization.html>
L<http://bugzilla.mozilla.org/show_bug.cgi?id=229658>
L<http://bugzilla.mozilla.org/show_bug.cgi?id=298341>

View File

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

View File

@@ -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>
# 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::Constants;
use Bugzilla::Error;
use Bugzilla::Mailer;
use Bugzilla::Util;
use Bugzilla::User;
use Date::Format;
use Date::Parse;
use File::Basename;
use Digest::MD5 qw(md5_hex);
use base qw(Exporter);
@Bugzilla::Token::EXPORT = qw(issue_session_token check_token_data delete_token
issue_hash_token check_hash_token);
################################################################################
# Public Functions
################################################################################
# Creates and sends a token to create a new user account.
# It assumes that the login has the correct format and is not already in use.
sub issue_new_user_account_token {
my $login_name = shift;
my $dbh = Bugzilla->dbh;
my $template = Bugzilla->template;
my $vars = {};
# Is there already a pending request for this login name? If yes, do not throw
# an error because the user may have lost his email with the token inside.
# But to prevent using this way to mailbomb an email address, make sure
# the last request is at least 10 minutes old before sending a new email.
my $pending_requests =
$dbh->selectrow_array('SELECT COUNT(*)
FROM tokens
WHERE tokentype = ?
AND ' . $dbh->sql_istrcmp('eventdata', '?') . '
AND issuedate > NOW() - ' . $dbh->sql_interval(10, 'MINUTE'),
undef, ('account', $login_name));
ThrowUserError('too_soon_for_new_token', {'type' => 'account'}) if $pending_requests;
my ($token, $token_ts) = _create_token(undef, 'account', $login_name);
$vars->{'email'} = $login_name . Bugzilla->params->{'emailsuffix'};
$vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400);
$vars->{'token'} = $token;
my $message;
$template->process('account/email/request-new.txt.tmpl', $vars, \$message)
|| ThrowTemplateError($template->error());
# In 99% of cases, the user getting the confirmation email is the same one
# who made the request, and so it is reasonable to send the email in the same
# language used to view the "Create a New Account" page (we cannot use his
# user prefs as the user has no account yet!).
MessageToMTA($message);
}
sub IssueEmailChangeToken {
my ($user, $old_email, $new_email) = @_;
my $email_suffix = Bugzilla->params->{'emailsuffix'};
my ($token, $token_ts) = _create_token($user->id, 'emailold', $old_email . ":" . $new_email);
my $newtoken = _create_token($user->id, 'emailnew', $old_email . ":" . $new_email);
# Mail the user the token along with instructions for using it.
my $template = Bugzilla->template_inner($user->settings->{'lang'}->{'value'});
my $vars = {};
$vars->{'oldemailaddress'} = $old_email . $email_suffix;
$vars->{'newemailaddress'} = $new_email . $email_suffix;
$vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400);
$vars->{'token'} = $token;
$vars->{'emailaddress'} = $old_email . $email_suffix;
my $message;
$template->process("account/email/change-old.txt.tmpl", $vars, \$message)
|| ThrowTemplateError($template->error());
MessageToMTA($message);
$vars->{'token'} = $newtoken;
$vars->{'emailaddress'} = $new_email . $email_suffix;
$message = "";
$template->process("account/email/change-new.txt.tmpl", $vars, \$message)
|| ThrowTemplateError($template->error());
Bugzilla->template_inner("");
MessageToMTA($message);
}
# 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.
sub IssuePasswordToken {
my $user = shift;
my $dbh = Bugzilla->dbh;
my $too_soon =
$dbh->selectrow_array('SELECT 1 FROM tokens
WHERE userid = ?
AND tokentype = ?
AND issuedate > NOW() - ' .
$dbh->sql_interval(10, 'MINUTE'),
undef, ($user->id, 'password'));
ThrowUserError('too_soon_for_new_token', {'type' => 'password'}) if $too_soon;
my ($token, $token_ts) = _create_token($user->id, 'password', $::ENV{'REMOTE_ADDR'});
# Mail the user the token along with instructions for using it.
my $template = Bugzilla->template_inner($user->settings->{'lang'}->{'value'});
my $vars = {};
$vars->{'token'} = $token;
$vars->{'emailaddress'} = $user->email;
$vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400);
# The user is not logged in (else he wouldn't request a new password).
# So we have to pass this information to the template.
$vars->{'timezone'} = $user->timezone;
my $message = "";
$template->process("account/password/forgotten-password.txt.tmpl",
$vars, \$message)
|| ThrowTemplateError($template->error());
Bugzilla->template_inner("");
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 issue_hash_token {
my ($data, $time) = @_;
$data ||= [];
$time ||= time();
# The concatenated string is of the form
# token creation time + site-wide secret + user ID + data
my @args = ($time, Bugzilla->localconfig->{'site_wide_secret'}, Bugzilla->user->id, @$data);
my $token = join('*', @args);
# Wide characters cause md5_hex() to die.
if (Bugzilla->params->{'utf8'}) {
utf8::encode($token) if utf8::is_utf8($token);
}
$token = md5_hex($token);
# Prepend the token creation time, unencrypted, so that the token
# lifetime can be validated.
return $time . '-' . $token;
}
sub check_hash_token {
my ($token, $data) = @_;
$data ||= [];
my ($time, $expected_token);
if ($token) {
($time, undef) = split(/-/, $token);
# Regenerate the token based on the information we have.
$expected_token = issue_hash_token($data, $time);
}
if (!$token
|| $expected_token ne $token
|| time() - $time > MAX_TOKEN_AGE * 86400)
{
my $template = Bugzilla->template;
my $vars = {};
$vars->{'script_name'} = basename($0);
$vars->{'token'} = issue_hash_token($data);
$vars->{'reason'} = (!$token) ? 'missing_token' :
($expected_token ne $token) ? 'invalid_token' :
'expired_token';
print Bugzilla->cgi->header();
$template->process('global/confirm-action.html.tmpl', $vars)
|| ThrowTemplateError($template->error());
exit;
}
# If we come here, then the token is valid and not too old.
return 1;
}
sub CleanTokenTable {
my $dbh = Bugzilla->dbh;
$dbh->do('DELETE FROM tokens
WHERE ' . $dbh->sql_to_days('NOW()') . ' - ' .
$dbh->sql_to_days('issuedate') . ' >= ?',
undef, MAX_TOKEN_AGE);
}
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;
}
# Cancels a previously issued token and notifies the user.
# 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.
sub Cancel {
my ($token, $cancelaction, $vars) = @_;
my $dbh = Bugzilla->dbh;
$vars ||= {};
# Get information about the token being canceled.
trick_taint($token);
my ($issuedate, $tokentype, $eventdata, $userid) =
$dbh->selectrow_array('SELECT ' . $dbh->sql_date_format('issuedate') . ',
tokentype, eventdata, userid
FROM tokens
WHERE token = ?',
undef, $token);
# If we are canceling the creation of a new user account, then there
# is no entry in the 'profiles' table.
my $user = new Bugzilla::User($userid);
$vars->{'emailaddress'} = $userid ? $user->email : $eventdata;
$vars->{'remoteaddress'} = $::ENV{'REMOTE_ADDR'};
$vars->{'token'} = $token;
$vars->{'tokentype'} = $tokentype;
$vars->{'issuedate'} = $issuedate;
# The user is probably not logged in.
# So we have to pass this information to the template.
$vars->{'timezone'} = $user->timezone;
$vars->{'eventdata'} = $eventdata;
$vars->{'cancelaction'} = $cancelaction;
# Notify the user via email about the cancellation.
my $template = Bugzilla->template_inner($user->settings->{'lang'}->{'value'});
my $message;
$template->process("account/cancel-token.txt.tmpl", $vars, \$message)
|| ThrowTemplateError($template->error());
Bugzilla->template_inner("");
MessageToMTA($message);
# Delete the token from the database.
delete_token($token);
}
sub DeletePasswordTokens {
my ($userid, $reason) = @_;
my $dbh = Bugzilla->dbh;
detaint_natural($userid);
my $tokens = $dbh->selectcol_arrayref('SELECT token FROM tokens
WHERE userid = ? AND tokentype = ?',
undef, ($userid, 'password'));
foreach my $token (@$tokens) {
Bugzilla::Token::Cancel($token, $reason);
}
}
# Returns an email change token if the user has one.
sub HasEmailChangeToken {
my $userid = shift;
my $dbh = Bugzilla->dbh;
my $token = $dbh->selectrow_array('SELECT token FROM tokens
WHERE userid = ?
AND (tokentype = ? OR tokentype = ?) ' .
$dbh->sql_limit(1),
undef, ($userid, 'emailnew', 'emailold'));
return $token;
}
# Returns the userid, issuedate and eventdata for the specified token
sub GetTokenData {
my ($token) = @_;
my $dbh = Bugzilla->dbh;
return unless defined $token;
$token = clean_text($token);
trick_taint($token);
return $dbh->selectrow_array(
"SELECT userid, " . $dbh->sql_date_format('issuedate') . ", eventdata
FROM tokens
WHERE token = ?", undef, $token);
}
# Deletes specified token
sub delete_token {
my ($token) = @_;
my $dbh = Bugzilla->dbh;
return unless defined $token;
trick_taint($token);
$dbh->do("DELETE FROM tokens WHERE token = ?", undef, $token);
}
# 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, $alternate_script) = @_;
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);
$vars->{'alternate_script'} = $alternate_script || 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
################################################################################
# Generates a unique token and inserts it into the database
# Returns the token and the token timestamp
sub _create_token {
my ($userid, $tokentype, $eventdata) = @_;
my $dbh = Bugzilla->dbh;
detaint_natural($userid) if defined $userid;
trick_taint($tokentype);
trick_taint($eventdata);
$dbh->bz_start_transaction();
my $token = GenerateUniqueToken();
$dbh->do("INSERT INTO tokens (userid, issuedate, token, tokentype, eventdata)
VALUES (?, NOW(), ?, ?, ?)", undef, ($userid, $token, $tokentype, $eventdata));
$dbh->bz_commit_transaction();
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::issue_new_user_account_token($login_name);
Bugzilla::Token::IssueEmailChangeToken($user, $old_email, $new_email);
Bugzilla::Token::IssuePasswordToken($user);
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<issue_new_user_account_token($login_name)>
Description: Creates and sends a token per email to the email address
requesting a new user account. It doesn't check whether
the user account already exists. The user will have to
use this token to confirm the creation of his user account.
Params: $login_name - The new login name requested by the user.
Returns: Nothing. It throws an error if the same user made the same
request in the last few minutes.
=item C<sub IssueEmailChangeToken($user, $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. These tokens remain valid for the next MAX_TOKEN_AGE days.
Params: $user - User object of the user 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($user)>
Description: Sends a token per email to the given user. 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: $user - User object 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 canceled.
$reason: The reason why these tokens are canceled.
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

View File

@@ -1,221 +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): Frédéric Buclin <LpSolit@gmail.com>
package Bugzilla::Update;
use strict;
use Bugzilla::Constants;
use constant REMOTE_FILE => 'http://updates.bugzilla.org/bugzilla-update.xml';
use constant LOCAL_FILE => "/bugzilla-update.xml"; # Relative to datadir.
use constant TIME_INTERVAL => 86400; # Default is one day, in seconds.
use constant TIMEOUT => 5; # Number of seconds before timeout.
# Look for new releases and notify logged in administrators about them.
sub get_notifications {
return if (Bugzilla->params->{'upgrade_notification'} eq 'disabled');
# If the XML::Twig module is missing, we won't be able to parse
# the XML file. So there is no need to go further.
eval("require XML::Twig");
return if $@;
my $local_file = bz_locations()->{'datadir'} . LOCAL_FILE;
# Update the local XML file if this one doesn't exist or if
# the last modification time (stat[9]) is older than TIME_INTERVAL.
if (!-e $local_file || (time() - (stat($local_file))[9] > TIME_INTERVAL)) {
# Are we sure we didn't try to refresh this file already
# but we failed because we cannot modify its timestamp?
my $can_alter = (-e $local_file) ? utime(undef, undef, $local_file) : 1;
if ($can_alter) {
unlink $local_file; # Make sure the old copy is away.
my $error = _synchronize_data();
# If an error is returned, leave now.
return $error if $error;
}
else {
return {'error' => 'no_update', 'xml_file' => $local_file};
}
}
# If we cannot access the local XML file, ignore it.
return {'error' => 'no_access', 'xml_file' => $local_file} unless (-r $local_file);
my $twig = XML::Twig->new();
$twig->safe_parsefile($local_file);
# If the XML file is invalid, return.
return {'error' => 'corrupted', 'xml_file' => $local_file} if $@;
my $root = $twig->root;
my @releases;
foreach my $branch ($root->children('branch')) {
my $release = {
'branch_ver' => $branch->{'att'}->{'id'},
'latest_ver' => $branch->{'att'}->{'vid'},
'status' => $branch->{'att'}->{'status'},
'url' => $branch->{'att'}->{'url'},
'date' => $branch->{'att'}->{'date'}
};
push(@releases, $release);
}
# On which branch is the current installation running?
my @current_version =
(BUGZILLA_VERSION =~ m/^(\d+)\.(\d+)(?:(rc|\.)(\d+))?\+?$/);
my @release;
if (Bugzilla->params->{'upgrade_notification'} eq 'development_snapshot') {
@release = grep {$_->{'status'} eq 'development'} @releases;
# If there is no development snapshot available, then we are in the
# process of releasing a release candidate. That's the release we want.
unless (scalar(@release)) {
@release = grep {$_->{'status'} eq 'release-candidate'} @releases;
}
}
elsif (Bugzilla->params->{'upgrade_notification'} eq 'latest_stable_release') {
@release = grep {$_->{'status'} eq 'stable'} @releases;
}
elsif (Bugzilla->params->{'upgrade_notification'} eq 'stable_branch_release') {
# We want the latest stable version for the current branch.
# If we are running a development snapshot, we won't match anything.
my $branch_version = $current_version[0] . '.' . $current_version[1];
# We do a string comparison instead of a numerical one, because
# e.g. 2.2 == 2.20, but 2.2 ne 2.20 (and 2.2 is indeed much older).
@release = grep {$_->{'branch_ver'} eq $branch_version} @releases;
# If the branch is now closed, we should strongly suggest
# to upgrade to the latest stable release available.
if (scalar(@release) && $release[0]->{'status'} eq 'closed') {
@release = grep {$_->{'status'} eq 'stable'} @releases;
return {'data' => $release[0], 'deprecated' => $branch_version};
}
}
else {
# Unknown parameter.
return {'error' => 'unknown_parameter'};
}
# Return if no new release is available.
return unless scalar(@release);
# Only notify the administrator if the latest version available
# is newer than the current one.
my @new_version =
($release[0]->{'latest_ver'} =~ m/^(\d+)\.(\d+)(?:(rc|\.)(\d+))?\+?$/);
# We convert release candidates 'rc' to integers (rc ? 0 : 1) in order
# to compare versions easily.
$current_version[2] = ($current_version[2] && $current_version[2] eq 'rc') ? 0 : 1;
$new_version[2] = ($new_version[2] && $new_version[2] eq 'rc') ? 0 : 1;
my $is_newer = _compare_versions(\@current_version, \@new_version);
return ($is_newer == 1) ? {'data' => $release[0]} : undef;
}
sub _synchronize_data {
eval("require LWP::UserAgent");
return {'error' => 'missing_package', 'package' => 'LWP::UserAgent'} if $@;
my $local_file = bz_locations()->{'datadir'} . LOCAL_FILE;
my $ua = LWP::UserAgent->new();
$ua->timeout(TIMEOUT);
$ua->protocols_allowed(['http', 'https']);
# If the URL of the proxy is given, use it, else get this information
# from the environment variable.
my $proxy_url = Bugzilla->params->{'proxy_url'};
if ($proxy_url) {
$ua->proxy(['http', 'https'], $proxy_url);
}
else {
$ua->env_proxy;
}
$ua->mirror(REMOTE_FILE, $local_file);
# $ua->mirror() forces the modification time of the local XML file
# to match the modification time of the remote one.
# So we have to update it manually to reflect that a newer version
# of the file has effectively been requested. This will avoid
# any new download for the next TIME_INTERVAL.
if (-e $local_file) {
# Try to alter its last modification time.
my $can_alter = utime(undef, undef, $local_file);
# This error should never happen.
$can_alter || return {'error' => 'no_update', 'xml_file' => $local_file};
}
else {
# We have been unable to download the file.
return {'error' => 'cannot_download', 'xml_file' => $local_file};
}
# Everything went well.
return 0;
}
sub _compare_versions {
my ($old_ver, $new_ver) = @_;
while (scalar(@$old_ver) && scalar(@$new_ver)) {
my $old = shift(@$old_ver) || 0;
my $new = shift(@$new_ver) || 0;
return $new <=> $old if ($new <=> $old);
}
return scalar(@$new_ver) <=> scalar(@$old_ver);
}
1;
__END__
=head1 NAME
Bugzilla::Update - Update routines for Bugzilla
=head1 SYNOPSIS
use Bugzilla::Update;
# Get information about new releases
my $new_release = Bugzilla::Update::get_notifications();
=head1 DESCRIPTION
This module contains all required routines to notify you
about new releases. It downloads an XML file from bugzilla.org
and parses it, in order to display information based on your
preferences. Absolutely no information about the Bugzilla version
you are running is sent to bugzilla.org.
=head1 FUNCTIONS
=over
=item C<get_notifications()>
Description: This function informs you about new releases, if any.
Params: None.
Returns: On success, a reference to a hash with data about
new releases, if any.
On failure, a reference to a hash with the reason
of the failure and the name of the unusable XML file.
=back
=cut

File diff suppressed because it is too large Load Diff

View File

@@ -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.
#
# Contributor(s): Shane H. W. Travis <travis@sedsystems.ca>
# Max Kanat-Alexander <mkanat@bugzilla.org>
# Marc Schumann <wurblzap@gmail.com>
# Frédéric Buclin <LpSolit@gmail.com>
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 get_text);
###############################
### Module Initialization ###
###############################
sub new {
my $invocant = shift;
my $setting_name = shift;
my $user_id = shift;
my $class = ref($invocant) || $invocant;
my $subclass = '';
# Create a ref to an empty hash and bless it
my $self = {};
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);
($default, $is_enabled, $value, $subclass) =
$dbh->selectrow_array(
q{SELECT default_value, is_enabled, setting_value, subclass
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, $subclass) =
$dbh->selectrow_array(
q{SELECT default_value, is_enabled, subclass
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;
$subclass = shift;
}
if ($subclass) {
eval('require ' . $class . '::' . $subclass);
$@ && ThrowCodeError('setting_subclass_invalid',
{'subclass' => $subclass});
$class = $class . '::' . $subclass;
}
bless($self, $class);
$self->{'_setting_name'} = $setting_name;
$self->{'_user_id'} = $user_id;
return $self;
}
###############################
### Subroutine Definitions ###
###############################
sub add_setting {
my ($name, $values, $default_value, $subclass, $force_check) = @_;
my $dbh = Bugzilla->dbh;
my $exists = _setting_exists($name);
return if ($exists && !$force_check);
($name && $default_value)
|| ThrowCodeError("setting_info_invalid");
if ($exists) {
# If this setting exists, we delete it and regenerate it.
$dbh->do('DELETE FROM setting_value WHERE name = ?', undef, $name);
$dbh->do('DELETE FROM setting WHERE name = ?', undef, $name);
# Remove obsolete user preferences for this setting.
if (defined $values && scalar(@$values)) {
my $list = join(', ', map {$dbh->quote($_)} @$values);
$dbh->do("DELETE FROM profile_setting
WHERE setting_name = ? AND setting_value NOT IN ($list)",
undef, $name);
}
}
else {
print get_text('install_setting_new', { name => $name }) . "\n";
}
$dbh->do(q{INSERT INTO setting (name, default_value, is_enabled, subclass)
VALUES (?, ?, 1, ?)},
undef, ($name, $default_value, $subclass));
my $sth = $dbh->prepare(q{INSERT INTO setting_value (name, value, sortindex)
VALUES (?, ?, ?)});
my $sortindex = 5;
foreach my $key (@$values){
$sth->execute($name, $key, $sortindex);
$sortindex += 5;
}
}
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, subclass
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, $subclass)
= $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, $subclass);
}
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, subclass
FROM setting
ORDER BY name});
$sth->execute();
while (my ($name, $default_value, $is_enabled, $subclass)
= $sth->fetchrow_array())
{
$default_settings->{$name} = new Bugzilla::User::Setting(
$name, $user_id, $is_enabled, $default_value, $default_value, 1,
$subclass);
}
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;
return $dbh->selectrow_arrayref(
"SELECT 1 FROM setting WHERE name = ?", undef, $setting_name) || 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, $subclass, $force_check)>
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> - arrayref - contains the new choices
for the new Setting.
C<$default_value> - string - the site default
C<$subclass> - string - name of the module returning
the list of valid values. This means legal values are
not stored in the DB.
C<$force_check> - boolean - when true, the existing setting
and all its values are deleted and replaced by new data.
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

View File

@@ -1,60 +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 Marc Schumann.
# Portions created by Marc Schumann are Copyright (c) 2007 Marc Schumann.
# All rights reserved.
#
# Contributor(s): Marc Schumann <wurblzap@gmail.com>
package Bugzilla::User::Setting::Lang;
use strict;
use base qw(Bugzilla::User::Setting);
use Bugzilla::Constants;
sub legal_values {
my ($self) = @_;
return $self->{'legal_values'} if defined $self->{'legal_values'};
return $self->{'legal_values'} = Bugzilla->languages;
}
1;
__END__
=head1 NAME
Bugzilla::User::Setting::Lang - Object for a user preference setting for preferred language
=head1 DESCRIPTION
Lang.pm extends Bugzilla::User::Setting and implements a class specialized for
setting the preferred language.
=head1 METHODS
=over
=item C<legal_values()>
Description: Returns all legal languages
Params: none
Returns: A reference to an array containing the names of all legal languages
=back

View File

@@ -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.
#
# Contributor(s): Marc Schumann <wurblzap@gmail.com>
#
package Bugzilla::User::Setting::Skin;
use strict;
use base qw(Bugzilla::User::Setting);
use Bugzilla::Constants;
use File::Spec::Functions;
use File::Basename;
use constant BUILTIN_SKIN_NAMES => ['standard'];
sub legal_values {
my ($self) = @_;
return $self->{'legal_values'} if defined $self->{'legal_values'};
my $dirbase = bz_locations()->{'skinsdir'} . '/contrib';
# Avoid modification of the list BUILTIN_SKIN_NAMES points to by copying the
# list over instead of simply writing $legal_values = BUILTIN_SKIN_NAMES.
my @legal_values = @{(BUILTIN_SKIN_NAMES)};
foreach my $direntry (glob(catdir($dirbase, '*'))) {
if (-d $direntry) {
# Stylesheet set
next if basename($direntry) =~ /^cvs$/i;
push(@legal_values, basename($direntry));
}
elsif ($direntry =~ /\.css$/) {
# Single-file stylesheet
push(@legal_values, basename($direntry));
}
}
return $self->{'legal_values'} = \@legal_values;
}
1;
__END__
=head1 NAME
Bugzilla::User::Setting::Skin - Object for a user preference setting for skins
=head1 DESCRIPTION
Skin.pm extends Bugzilla::User::Setting and implements a class specialized for
skins settings.
=head1 METHODS
=over
=item C<legal_values()>
Description: Returns all legal skins
Params: none
Returns: A reference to an array containing the names of all legal skins
=back

View File

@@ -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 Frédéric Buclin.
# Portions created by Frédéric Buclin are Copyright (c) 2008 Frédéric Buclin.
# All rights reserved.
#
# Contributor(s): Frédéric Buclin <LpSolit@gmail.com>
package Bugzilla::User::Setting::Timezone;
use strict;
use DateTime::TimeZone;
use base qw(Bugzilla::User::Setting);
use Bugzilla::Constants;
sub legal_values {
my ($self) = @_;
return $self->{'legal_values'} if defined $self->{'legal_values'};
my @timezones = DateTime::TimeZone->all_names;
# Remove old formats, such as CST6CDT, EST, EST5EDT.
@timezones = grep { $_ =~ m#.+/.+#} @timezones;
# Append 'local' to the list, which will use the timezone
# given by the server.
push(@timezones, 'local');
return $self->{'legal_values'} = \@timezones;
}
1;
__END__
=head1 NAME
Bugzilla::User::Setting::Timezone - Object for a user preference setting for desired timezone
=head1 DESCRIPTION
Timezone.pm extends Bugzilla::User::Setting and implements a class specialized for
setting the desired timezone.
=head1 METHODS
=over
=item C<legal_values()>
Description: Returns all legal timezones
Params: none
Returns: A reference to an array containing the names of all legal timezones
=back

File diff suppressed because it is too large Load Diff

View File

@@ -1,263 +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>
# Max Kanat-Alexander <mkanat@bugzilla.org>
use strict;
package Bugzilla::Version;
use base qw(Bugzilla::Object);
use Bugzilla::Install::Util qw(vers_cmp);
use Bugzilla::Util;
use Bugzilla::Error;
################################
##### Initialization #####
################################
use constant DEFAULT_VERSION => 'unspecified';
use constant DB_TABLE => 'versions';
use constant DB_COLUMNS => qw(
id
value
product_id
);
use constant NAME_FIELD => 'value';
# This is "id" because it has to be filled in and id is probably the fastest.
# We do a custom sort in new_from_list below.
use constant LIST_ORDER => 'id';
sub new {
my $class = shift;
my $param = shift;
my $dbh = Bugzilla->dbh;
my $product;
if (ref $param) {
$product = $param->{product};
my $name = $param->{name};
if (!defined $product) {
ThrowCodeError('bad_arg',
{argument => 'product',
function => "${class}::new"});
}
if (!defined $name) {
ThrowCodeError('bad_arg',
{argument => 'name',
function => "${class}::new"});
}
my $condition = 'product_id = ? AND value = ?';
my @values = ($product->id, $name);
$param = { condition => $condition, values => \@values };
}
unshift @_, $param;
return $class->SUPER::new(@_);
}
sub new_from_list {
my $self = shift;
my $list = $self->SUPER::new_from_list(@_);
return [sort { vers_cmp(lc($a->name), lc($b->name)) } @$list];
}
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'};
}
sub remove_from_db {
my $self = shift;
my $dbh = Bugzilla->dbh;
# The version cannot be removed if there are bugs
# associated with it.
if ($self->bug_count) {
ThrowUserError("version_has_bugs", { nb => $self->bug_count });
}
$dbh->do(q{DELETE FROM versions WHERE product_id = ? AND value = ?},
undef, ($self->product_id, $self->name));
}
sub update {
my $self = shift;
my ($name, $product) = @_;
my $dbh = Bugzilla->dbh;
$name || ThrowUserError('version_not_specified');
# Remove unprintable characters
$name = clean_text($name);
return 0 if ($name eq $self->name);
my $version = new Bugzilla::Version({ product => $product, name => $name });
if ($version) {
ThrowUserError('version_already_exists',
{'name' => $version->name,
'product' => $product->name});
}
trick_taint($name);
$dbh->do("UPDATE bugs SET version = ?
WHERE version = ? AND product_id = ?", undef,
($name, $self->name, $self->product_id));
$dbh->do("UPDATE versions SET value = ?
WHERE product_id = ? AND value = ?", undef,
($name, $self->product_id, $self->name));
$self->{'value'} = $name;
return 1;
}
###############################
##### Accessors ####
###############################
sub name { return $_[0]->{'value'}; }
sub product_id { return $_[0]->{'product_id'}; }
###############################
##### Subroutines ###
###############################
sub create {
my ($name, $product) = @_;
my $dbh = Bugzilla->dbh;
# Cleanups and validity checks
$name || ThrowUserError('version_blank_name');
# Remove unprintable characters
$name = clean_text($name);
my $version = new Bugzilla::Version({ product => $product, name => $name });
if ($version) {
ThrowUserError('version_already_exists',
{'name' => $version->name,
'product' => $product->name});
}
# Add the new version
trick_taint($name);
$dbh->do(q{INSERT INTO versions (value, product_id)
VALUES (?, ?)}, undef, ($name, $product->id));
return new Bugzilla::Version($dbh->bz_last_key('versions', 'id'));
}
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;
$version->remove_from_db;
my $updated = $version->update($version_name, $product);
my $version = $hash_ref->{'version_value'};
my $version = Bugzilla::Version::create($version_name, $product);
=head1 DESCRIPTION
Version.pm represents a Product Version object.
=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.
=item C<remove_from_db()>
Description: Removes the version from the database.
Params: none.
Retruns: none.
=item C<update($name, $product)>
Description: Update the value of the version.
Params: $name - String with the new version value.
$product - Bugzilla::Product object the version belongs to.
Returns: An integer - 1 if the version has been updated, else 0.
=back
=head1 SUBROUTINES
=over
=item C<create($version_name, $product)>
Description: Create a new version for the given product.
Params: $version_name - String with a version value.
$product - A Bugzilla::Product object.
Returns: A Bugzilla::Version object.
=back
=cut

View File

@@ -1,292 +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): Marc Schumann <wurblzap@gmail.com>
# Max Kanat-Alexander <mkanat@bugzilla.org>
# Rosie Clarkson <rosie.clarkson@planningportal.gov.uk>
#
# Portions © Crown copyright 2009 - Rosie Clarkson (development@planningportal.gov.uk) for the Planning Portal
# This is the base class for $self in WebService method calls. For the
# actual RPC server, see Bugzilla::WebService::Server and its subclasses.
package Bugzilla::WebService;
use strict;
use Date::Parse;
use XMLRPC::Lite;
sub datetime_format {
my ($self, $date_string) = @_;
my $time = str2time($date_string);
my ($sec, $min, $hour, $mday, $mon, $year) = localtime $time;
# This format string was stolen from SOAP::Utils->format_datetime,
# which doesn't work but which has almost the right format string.
my $iso_datetime = sprintf('%d%02d%02dT%02d:%02d:%02d',
$year + 1900, $mon + 1, $mday, $hour, $min, $sec);
return $iso_datetime;
}
# For some methods, we shouldn't call Bugzilla->login before we call them
use constant LOGIN_EXEMPT => { };
sub login_exempt {
my ($class, $method) = @_;
return $class->LOGIN_EXEMPT->{$method};
}
sub type {
my ($self, $type, $value) = @_;
if ($type eq 'dateTime') {
$value = $self->datetime_format($value);
}
return XMLRPC::Data->type($type)->value($value);
}
1;
__END__
=head1 NAME
Bugzilla::WebService - The Web Service interface to Bugzilla
=head1 DESCRIPTION
This is the standard API for external programs that want to interact
with Bugzilla. It provides various methods in various modules.
Currently the only method of accessing the API is via XML-RPC. The XML-RPC
standard is described here: L<http://www.xmlrpc.com/spec>
The endpoint for Bugzilla WebServices is the C<xmlrpc.cgi> script in
your Bugzilla installation. For example, if your Bugzilla is at
C<bugzilla.yourdomain.com>, then your XML-RPC client would access the
API via: C<http://bugzilla.yourdomain.com/xmlrpc.cgi>
=head1 CALLING METHODS
Methods are called in the normal XML-RPC fashion. Bugzilla does not currently
implement any extensions to the standard method of XML-RPC method calling.
Methods are grouped into "packages", like C<Bug> for
L<Bugzilla::WebService::Bug>. So, for example,
L<Bugzilla::WebService::Bug/get>, is called as C<Bug.get> in XML-RPC.
=head1 PARAMETERS
In addition to the standard parameter types like C<int>, C<string>, etc.,
XML-RPC has two data structures, a C<< <struct> >> and an C<< <array> >>.
=head2 Structs
In Perl, we call a C<< <struct> >> a "hash" or a "hashref". You may see
us refer to it that way in the API documentation.
In example code, you will see the characters C<{> and C<}> used to represent
the beginning and end of structs.
For example, here's a struct in XML-RPC:
<struct>
<member>
<name>fruit</name>
<value><string>oranges</string></value>
</member>
<member>
<name>vegetable</name>
<value><string>lettuce</string></value>
</member>
</struct>
In our example code in these API docs, that would look like:
{ fruit => 'oranges', vegetable => 'lettuce' }
=head2 Arrays
In example code, you will see the characters C<[> and C<]> used to
represent the beginning and end of arrays.
For example, here's an array in XML-RPC:
<array>
<data>
<value><i4>1</i4></value>
<value><i4>2</i4></value>
<value><i4>3</i4></value>
</data>
</array>
In our example code in these API docs, that would look like:
[1, 2, 3]
=head2 How Bugzilla WebService Methods Take Parameters
B<All> Bugzilla WebServices functions take their parameters in
a C<< <struct> >>. Another way of saying this would be: All functions
take a single argument, a C<< <struct> >> that contains all parameters.
The names of the parameters listed in the API docs for each function are
the C<name> element for the struct C<member>s.
=head1 LOGGING IN
You can use L<Bugzilla::WebService::User/login> to log in as a Bugzilla
user. This issues standard HTTP cookies that you must then use in future
calls, so your XML-RPC client must be capable of receiving and transmitting
cookies.
=head1 STABLE, EXPERIMENTAL, and UNSTABLE
Methods are marked B<STABLE> if you can expect their parameters and
return values not to change between versions of Bugzilla. You are
best off always using methods marked B<STABLE>. We may add parameters
and additional items to the return values, but your old code will
always continue to work with any new changes we make. If we ever break
a B<STABLE> interface, we'll post a big notice in the Release Notes,
and it will only happen during a major new release.
Methods (or parts of methods) are marked B<EXPERIMENTAL> if
we I<believe> they will be stable, but there's a slight chance that
small parts will change in the future.
Certain parts of a method's description may be marked as B<UNSTABLE>,
in which case those parts are not guaranteed to stay the same between
Bugzilla versions.
=head1 ERRORS
If a particular webservice call fails, it will throw a standard XML-RPC
error. There will be a numeric error code, and then the description
field will contain descriptive text of the error. Each error that Bugzilla
can throw has a specific code that will not change between versions of
Bugzilla.
The various errors that functions can throw are specified by the
documentation of those functions.
If your code needs to know what error Bugzilla threw, use the numeric
code. Don't try to parse the description, because that may change
from version to version of Bugzilla.
Note that if you display the error to the user in an HTML program, make
sure that you properly escape the error, as it will not be HTML-escaped.
=head2 Transient vs. Fatal Errors
If the error code is a number greater than 0, the error is considered
"transient," which means that it was an error made by the user, not
some problem with Bugzilla itself.
If the error code is a number less than 0, the error is "fatal," which
means that it's some error in Bugzilla itself that probably requires
administrative attention.
Negative numbers and positive numbers don't overlap. That is, if there's
an error 302, there won't be an error -302.
=head2 Unknown Errors
Sometimes a function will throw an error that doesn't have a specific
error code. In this case, the code will be C<-32000> if it's a "fatal"
error, and C<32000> if it's a "transient" error.
=head1 COMMON PARAMETERS
Many Webservice methods take similar arguments. Instead of re-writing
the documentation for each method, we document the parameters here, once,
and then refer back to this documentation from the individual methods
where these parameters are used.
=head2 Limiting What Fields Are Returned
Many WebService methods return an array of structs with various
fields in the structs. (For example, L<Bugzilla::WebService::Bug/get>
returns a list of C<bugs> that have fields like C<id>, C<summary>,
C<creation_time>, etc.)
These parameters allow you to limit what fields are present in
the structs, to possibly improve performance or save some bandwidth.
=over
=item C<include_fields> (array)
An array of strings, representing the (case-sensitive) names of fields.
Only the fields specified in this hash will be returned, the rest will
not be included.
If you specify an empty array, then this function will return empty
hashes.
Invalid field names are ignored.
Example:
User.get( ids => [1], include_fields => ['id', 'name'] )
would return something like:
{ users => [{ id => 1, name => 'user@domain.com' }] }
=item C<exclude_fields> (array)
An array of strings, representing the (case-sensitive) names of fields.
The fields specified will not be included in the returned hashes.
If you specify all the fields, then this function will return empty
hashes.
Invalid field names are ignored.
Specifying fields here overrides C<include_fields>, so if you specify a
field in both, it will be excluded, not included.
Example:
User.get( ids => [1], exclude_fields => ['name'] )
would return something like:
{ users => [{ id => 1, real_name => 'John Smith' }] }
=back
=head1 EXTENSIONS TO THE XML-RPC STANDARD
=head2 Undefined Values
Normally, XML-RPC does not allow empty values for C<int>, C<double>, or
C<dateTime.iso8601> fields. Bugzilla does--it treats empty values as
C<undef> (called C<NULL> or C<None> in some programming languages).
Bugzilla also accepts an element called C<< <nil> >>, as specified by
the XML-RPC extension here: L<http://ontosys.com/xml-rpc/extensions.php>,
which is always considered to be C<undef>, no matter what it contains.
Bugzilla does not use C<< <nil> >> values in returned data, because currently
most clients do not support C<< <nil> >>. Instead, any fields with C<undef>
values will be stripped from the response completely. Therefore
B<the client must handle the fact that some expected fields may not be
returned>.
=begin private
nil is implemented by XMLRPC::Lite, in XMLRPC::Deserializer::decode_value
in the CPAN SVN since 14th Dec 2008
L<http://rt.cpan.org/Public/Bug/Display.html?id=20569> and in Fedora's
perl-SOAP-Lite package in versions 0.68-1 and above.
=end private

File diff suppressed because it is too large Load Diff

View File

@@ -1,251 +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): Marc Schumann <wurblzap@gmail.com>
# Max Kanat-Alexander <mkanat@bugzilla.org>
# Mads Bondo Dydensborg <mbd@dbc.dk>
package Bugzilla::WebService::Bugzilla;
use strict;
use base qw(Bugzilla::WebService);
use Bugzilla::Constants;
use Bugzilla::Hook;
use DateTime;
# Basic info that is needed before logins
use constant LOGIN_EXEMPT => {
timezone => 1,
version => 1,
};
sub version {
my $self = shift;
return { version => $self->type('string', BUGZILLA_VERSION) };
}
sub extensions {
my $self = shift;
my $extensions = Bugzilla::Hook::enabled_plugins();
foreach my $name (keys %$extensions) {
my $info = $extensions->{$name};
foreach my $data (keys %$info) {
$extensions->{$name}->{$data} =
$self->type('string', $info->{$data});
}
}
return { extensions => $extensions };
}
sub timezone {
my $self = shift;
my $offset = Bugzilla->local_timezone->offset_for_datetime(DateTime->now());
$offset = (($offset / 60) / 60) * 100;
$offset = sprintf('%+05d', $offset);
return { timezone => $self->type('string', $offset) };
}
sub time {
my ($self) = @_;
my $dbh = Bugzilla->dbh;
my $db_time = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
my $now_utc = DateTime->now();
my $tz = Bugzilla->local_timezone;
my $now_local = $now_utc->clone->set_time_zone($tz);
my $tz_offset = $tz->offset_for_datetime($now_local);
return {
db_time => $self->type('dateTime', $db_time),
web_time => $self->type('dateTime', $now_local),
web_time_utc => $self->type('dateTime', $now_utc),
tz_name => $self->type('string', $tz->name),
tz_offset => $self->type('string',
$tz->offset_as_string($tz_offset)),
tz_short_name => $self->type('string',
$now_local->time_zone_short_name),
};
}
1;
__END__
=head1 NAME
Bugzilla::WebService::Bugzilla - Global functions for the webservice interface.
=head1 DESCRIPTION
This provides functions that tell you about Bugzilla in general.
=head1 METHODS
See L<Bugzilla::WebService> for a description of how parameters are passed,
and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean.
=over
=item C<version>
B<STABLE>
=over
=item B<Description>
Returns the current version of Bugzilla.
=item B<Params> (none)
=item B<Returns>
A hash with a single item, C<version>, that is the version as a
string.
=item B<Errors> (none)
=back
=item C<extensions>
B<EXPERIMENTAL>
=over
=item B<Description>
Gets information about the extensions that are currently installed and enabled
in this Bugzilla.
=item B<Params> (none)
=item B<Returns>
A hash with a single item, C<extesions>. This points to a hash. I<That> hash
contains the names of extensions as keys, and information about the extension
as values. One of the values that must be returned is the 'version' of the
extension
=item B<History>
=over
=item Added in Bugzilla B<3.2>.
=back
=back
=item C<timezone>
B<DEPRECATED> This method may be removed in a future version of Bugzilla.
Use L</time> instead.
=over
=item B<Description>
Returns the timezone of the server Bugzilla is running on. This is
important because all dates/times that the webservice interface
returns will be in this timezone.
=item B<Params> (none)
=item B<Returns>
A hash with a single item, C<timezone>, that is the timezone offset as a
string in (+/-)XXXX (RFC 2822) format.
=back
=item C<time>
B<UNSTABLE>
=over
=item B<Description>
Gets information about what time the Bugzilla server thinks it is, and
what timezone it's running in.
=item B<Params> (none)
=item B<Returns>
A struct with the following items:
=over
=item C<db_time>
C<dateTime> The current time in Bugzilla's B<local time zone>, according
to the Bugzilla I<database server>.
Note that Bugzilla assumes that the database and the webserver are running
in the same time zone. However, if the web server and the database server
aren't synchronized for some reason, I<this> is the time that you should
rely on for doing searches and other input to the WebService.
=item C<web_time>
C<dateTime> This is the current time in Bugzilla's B<local time zone>,
according to Bugzilla's I<web server>.
This might be different by a second from C<db_time> since this comes from
a different source. If it's any more different than a second, then there is
likely some problem with this Bugzilla instance. In this case you should
rely on the C<db_time>, not the C<web_time>.
=item C<web_time_utc>
The same as C<web_time>, but in the B<UTC> time zone instead of the local
time zone.
=item C<tz_name>
C<string> The long name of the time zone that the Bugzilla web server is
in. Will usually look something like: C<America/Los Angeles>
=item C<tz_short_name>
C<string> The "short name" of the time zone that the Bugzilla web server
is in. This should only be used for display, and not relied on for your
programs, because different time zones can have the same short name.
(For example, there are two C<EST>s.)
This will look something like: C<PST>.
=item C<tz_offset>
C<string> The timezone offset as a string in (+/-)XXXX (RFC 2822) format.
=back
=item B<History>
=over
=item Added in Bugzilla B<3.4>.
=back
=back
=back

View File

@@ -1,139 +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): Marc Schumann <wurblzap@gmail.com>
# Max Kanat-Alexander <mkanat@bugzilla.org>
package Bugzilla::WebService::Constants;
use strict;
use base qw(Exporter);
our @EXPORT = qw(
WS_ERROR_CODE
ERROR_UNKNOWN_FATAL
ERROR_UNKNOWN_TRANSIENT
ERROR_AUTH_NODATA
WS_DISPATCH
);
# This maps the error names in global/*-error.html.tmpl to numbers.
# Generally, transient errors should have a number above 0, and
# fatal errors should have a number below 0.
#
# This hash should generally contain any error that could be thrown
# by the WebService interface. If it's extremely unlikely that the
# error could be thrown (like some CodeErrors), it doesn't have to
# be listed here.
#
# "Transient" means "If you resubmit that request with different data,
# it may work."
#
# "Fatal" means, "There's something wrong with Bugzilla, probably
# something an administrator would have to fix."
#
# NOTE: Numbers must never be recycled. If you remove a number, leave a
# comment that it was retired. Also, if an error changes its name, you'll
# have to fix it here.
use constant WS_ERROR_CODE => {
# Generic errors (Bugzilla::Object and others) are 50-99.
object_not_specified => 50,
param_required => 50,
params_required => 50,
object_does_not_exist => 51,
xmlrpc_invalid_value => 52,
# Bug errors usually occupy the 100-200 range.
improper_bug_id_field_value => 100,
bug_id_does_not_exist => 101,
bug_access_denied => 102,
bug_access_query => 102,
# These all mean "invalid alias"
alias_too_long => 103,
alias_in_use => 103,
alias_is_numeric => 103,
alias_has_comma_or_space => 103,
# Misc. bug field errors
illegal_field => 104,
freetext_too_long => 104,
# Component errors
require_component => 105,
component_name_too_long => 105,
# Invalid Product
no_products => 106,
entry_access_denied => 106,
product_access_denied => 106,
product_disabled => 106,
# Invalid Summary
require_summary => 107,
# Invalid field name
invalid_field_name => 108,
# Not authorized to edit the bug
product_edit_denied => 109,
# Comment-related errors
comment_is_private => 110,
comment_id_invalid => 111,
# See Also errors
bug_url_invalid => 112,
bug_url_too_long => 112,
# Insidergroup Errors
user_not_insider => 113,
# Authentication errors are usually 300-400.
invalid_username_or_password => 300,
account_disabled => 301,
auth_invalid_email => 302,
extern_id_conflict => -303,
auth_failure => 304,
# User errors are 500-600.
account_exists => 500,
illegal_email_address => 501,
account_creation_disabled => 501,
account_creation_restricted => 501,
password_too_short => 502,
password_too_long => 503,
invalid_username => 504,
# This is from strict_isolation, but it also basically means
# "invalid user."
invalid_user_group => 504,
user_access_by_id_denied => 505,
user_access_by_match_denied => 505,
};
# These are the fallback defaults for errors not in ERROR_CODE.
use constant ERROR_UNKNOWN_FATAL => -32000;
use constant ERROR_UNKNOWN_TRANSIENT => 32000;
use constant ERROR_AUTH_NODATA => 410;
use constant ERROR_GENERAL => 999;
sub WS_DISPATCH {
# We "require" here instead of "use" above to avoid a dependency loop.
require Bugzilla::Hook;
my %hook_dispatch;
Bugzilla::Hook::process('webservice', { dispatch => \%hook_dispatch });
my $dispatch = {
'Bugzilla' => 'Bugzilla::WebService::Bugzilla',
'Bug' => 'Bugzilla::WebService::Bug',
'User' => 'Bugzilla::WebService::User',
'Product' => 'Bugzilla::WebService::Product',
%hook_dispatch
};
return $dispatch;
};
1;

View File

@@ -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.
#
# Contributor(s): Marc Schumann <wurblzap@gmail.com>
# Mads Bondo Dydensborg <mbd@dbc.dk>
package Bugzilla::WebService::Product;
use strict;
use base qw(Bugzilla::WebService);
use Bugzilla::Product;
use Bugzilla::User;
use Bugzilla::WebService::Util qw(validate);
##################################################
# Add aliases here for method name compatibility #
##################################################
BEGIN { *get_products = \&get }
# Get the ids of the products the user can search
sub get_selectable_products {
return {ids => [map {$_->id} @{Bugzilla->user->get_selectable_products}]};
}
# Get the ids of the products the user can enter bugs against
sub get_enterable_products {
return {ids => [map {$_->id} @{Bugzilla->user->get_enterable_products}]};
}
# Get the union of the products the user can search and enter bugs against.
sub get_accessible_products {
return {ids => [map {$_->id} @{Bugzilla->user->get_accessible_products}]};
}
# Get a list of actual products, based on list of ids
sub get {
my ($self, $params) = validate(@_, 'ids');
# Only products that are in the users accessible products,
# can be allowed to be returned
my $accessible_products = Bugzilla->user->get_accessible_products;
# Create a hash with the ids the user wants
my %ids = map { $_ => 1 } @{$params->{ids}};
# Return the intersection of this, by grepping the ids from
# accessible products.
my @requested_accessible = grep { $ids{$_->id} } @$accessible_products;
# Now create a result entry for each.
my @products =
map {{
internals => $_,
id => $self->type('int', $_->id),
name => $self->type('string', $_->name),
description => $self->type('string', $_->description),
}
} @requested_accessible;
return { products => \@products };
}
1;
__END__
=head1 NAME
Bugzilla::Webservice::Product - The Product API
=head1 DESCRIPTION
This part of the Bugzilla API allows you to list the available Products and
get information about them.
=head1 METHODS
See L<Bugzilla::WebService> for a description of how parameters are passed,
and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean.
=head2 List Products
=over
=item C<get_selectable_products>
B<EXPERIMENTAL>
=over
=item B<Description>
Returns a list of the ids of the products the user can search on.
=item B<Params> (none)
=item B<Returns>
A hash containing one item, C<ids>, that contains an array of product
ids.
=item B<Errors> (none)
=back
=item C<get_enterable_products>
B<EXPERIMENTAL>
=over
=item B<Description>
Returns a list of the ids of the products the user can enter bugs
against.
=item B<Params> (none)
=item B<Returns>
A hash containing one item, C<ids>, that contains an array of product
ids.
=item B<Errors> (none)
=back
=item C<get_accessible_products>
B<UNSTABLE>
=over
=item B<Description>
Returns a list of the ids of the products the user can search or enter
bugs against.
=item B<Params> (none)
=item B<Returns>
A hash containing one item, C<ids>, that contains an array of product
ids.
=item B<Errors> (none)
=back
=item C<get>
B<EXPERIMENTAL>
=over
=item B<Description>
Returns a list of information about the products passed to it.
Note: Can also be called as "get_products" for compatibilty with Bugzilla 3.0 API.
=item B<Params>
A hash containing one item, C<ids>, that is an array of product ids.
=item B<Returns>
A hash containing one item, C<products>, that is an array of
hashes. Each hash describes a product, and has the following items:
C<id>, C<name>, C<description>, and C<internals>. The C<id> item is
the id of the product. The C<name> item is the name of the
product. The C<description> is the description of the
product. Finally, the C<internals> is an internal representation of
the product.
Note, that if the user tries to access a product that is not in the
list of accessible products for the user, or a product that does not
exist, that is silently ignored, and no information about that product
is returned.
=item B<Errors> (none)
=back
=back

View File

@@ -1,42 +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): Marc Schumann <wurblzap@gmail.com>
# Max Kanat-Alexander <mkanat@bugzilla.org>
package Bugzilla::WebService::Server;
use strict;
use Bugzilla::Util qw(ssl_require_redirect);
sub handle_login {
my ($self, $class, $method, $full_method) = @_;
eval "require $class";
return if $class->login_exempt($method);
Bugzilla->login();
# Even though we check for the need to redirect in
# Bugzilla->login() we check here again since Bugzilla->login()
# does not know what the current XMLRPC method is. Therefore
# ssl_require_redirect in Bugzilla->login() will have returned
# false if system was configured to redirect for authenticated
# sessions and the user was not yet logged in.
# So here we pass in the method name to ssl_require_redirect so
# it can then check for the extra case where the method equals
# User.login, which we would then need to redirect if not
# over a secure connection.
Bugzilla->cgi->require_https(Bugzilla->params->{'sslbase'})
if ssl_require_redirect($full_method);
}
1;

View File

@@ -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): Marc Schumann <wurblzap@gmail.com>
# Max Kanat-Alexander <mkanat@bugzilla.org>
# Rosie Clarkson <rosie.clarkson@planningportal.gov.uk>
#
# Portions © Crown copyright 2009 - Rosie Clarkson (development@planningportal.gov.uk) for the Planning Portal
package Bugzilla::WebService::Server::XMLRPC;
use strict;
use XMLRPC::Transport::HTTP;
use Bugzilla::WebService::Server;
our @ISA = qw(XMLRPC::Transport::HTTP::CGI Bugzilla::WebService::Server);
use Bugzilla::WebService::Constants;
sub initialize {
my $self = shift;
my %retval = $self->SUPER::initialize(@_);
$retval{'serializer'} = Bugzilla::XMLRPC::Serializer->new;
$retval{'deserializer'} = Bugzilla::XMLRPC::Deserializer->new;
$retval{'dispatch_with'} = WS_DISPATCH;
return %retval;
}
sub make_response {
my $self = shift;
$self->SUPER::make_response(@_);
# XMLRPC::Transport::HTTP::CGI doesn't know about Bugzilla carrying around
# its cookies in Bugzilla::CGI, so we need to copy them over.
foreach (@{Bugzilla->cgi->{'Bugzilla_cookie_list'}}) {
$self->response->headers->push_header('Set-Cookie', $_);
}
}
sub datetime_format {
my ($self, $date_string) = @_;
my $time = str2time($date_string);
my ($sec, $min, $hour, $mday, $mon, $year) = localtime $time;
# This format string was stolen from SOAP::Utils->format_datetime,
# which doesn't work but which has almost the right format string.
my $iso_datetime = sprintf('%d%02d%02dT%02d:%02d:%02d',
$year + 1900, $mon + 1, $mday, $hour, $min, $sec);
return $iso_datetime;
}
sub handle_login {
my ($self, $classes, $action, $uri, $method) = @_;
my $class = $classes->{$uri};
my $full_method = $uri . "." . $method;
$self->SUPER::handle_login($class, $method, $full_method);
return;
}
1;
# This exists to validate input parameters (which XMLRPC::Lite doesn't do)
# and also, in some cases, to more-usefully decode them.
package Bugzilla::XMLRPC::Deserializer;
use strict;
# We can't use "use base" because XMLRPC::Serializer doesn't return
# a true value.
eval { require XMLRPC::Lite; };
our @ISA = qw(XMLRPC::Deserializer);
use Bugzilla::Error;
# Some method arguments need to be converted in some way, when they are input.
sub decode_value {
my $self = shift;
my ($type) = @{ $_[0] };
my $value = $self->SUPER::decode_value(@_);
# We only validate/convert certain types here.
return $value if $type !~ /^(?:int|i4|boolean|double|dateTime\.iso8601)$/;
# Though the XML-RPC standard doesn't allow an empty <int>,
# <double>,or <dateTime.iso8601>, we do, and we just say
# "that's undef".
if (grep($type eq $_, qw(int double dateTime))) {
return undef if $value eq '';
}
my $validator = $self->_validation_subs->{$type};
if (!$validator->($value)) {
ThrowUserError('xmlrpc_invalid_value',
{ type => $type, value => $value });
}
# We convert dateTimes to a DB-friendly date format.
if ($type eq 'dateTime.iso8601') {
# We leave off the $ from the end of this regex to allow for possible
# extensions to the XML-RPC date standard.
$value =~ /^(\d{4})(\d{2})(\d{2})T(\d{2}):(\d{2}):(\d{2})/;
$value = "$1-$2-$3 $4:$5:$6";
}
return $value;
}
sub _validation_subs {
my $self = shift;
return $self->{_validation_subs} if $self->{_validation_subs};
# The only place that XMLRPC::Lite stores any sort of validation
# regex is in XMLRPC::Serializer. We want to re-use those regexes here.
my $lookup = Bugzilla::XMLRPC::Serializer->new->typelookup;
# $lookup is a hash whose values are arrayrefs, and whose keys are the
# names of types. The second item of each arrayref is a subroutine
# that will do our validation for us.
my %validators = map { $_ => $lookup->{$_}->[1] } (keys %$lookup);
# Add a boolean validator
$validators{'boolean'} = sub {$_[0] =~ /^[01]$/};
# Some types have multiple names, or have a different name in
# XMLRPC::Serializer than their standard XML-RPC name.
$validators{'dateTime.iso8601'} = $validators{'dateTime'};
$validators{'i4'} = $validators{'int'};
$self->{_validation_subs} = \%validators;
return \%validators;
}
1;
# This package exists to fix a UTF-8 bug in SOAP::Lite.
# See http://rt.cpan.org/Public/Bug/Display.html?id=32952.
package Bugzilla::XMLRPC::Serializer;
use Scalar::Util qw(blessed);
use strict;
# We can't use "use base" because XMLRPC::Serializer doesn't return
# a true value.
eval { require XMLRPC::Lite; };
our @ISA = qw(XMLRPC::Serializer);
sub new {
my $class = shift;
my $self = $class->SUPER::new(@_);
# This fixes UTF-8.
$self->{'_typelookup'}->{'base64'} =
[10, sub { !utf8::is_utf8($_[0]) && $_[0] =~ /[^\x09\x0a\x0d\x20-\x7f]/},
'as_base64'];
# This makes arrays work right even though we're a subclass.
# (See http://rt.cpan.org//Ticket/Display.html?id=34514)
$self->{'_encodingStyle'} = '';
return $self;
}
sub as_string {
my $self = shift;
my ($value) = @_;
# Something weird happens with XML::Parser when we have upper-ASCII
# characters encoded as UTF-8, and this fixes it.
utf8::encode($value) if utf8::is_utf8($value)
&& $value =~ /^[\x00-\xff]+$/;
return $self->SUPER::as_string($value);
}
# Here the XMLRPC::Serializer is extended to use the XMLRPC nil extension.
sub encode_object {
my $self = shift;
my @encoded = $self->SUPER::encode_object(@_);
return $encoded[0]->[0] eq 'nil'
? ['value', {}, [@encoded]]
: @encoded;
}
# Removes undefined values so they do not produce invalid XMLRPC.
sub envelope {
my $self = shift;
my ($type, $method, $data) = @_;
# If the type isn't a successful response we don't want to change the values.
if ($type eq 'response'){
$data = _strip_undefs($data);
}
return $self->SUPER::envelope($type, $method, $data);
}
# In an XMLRPC response we have to handle hashes of arrays, hashes, scalars,
# Bugzilla objects (reftype = 'HASH') and XMLRPC::Data objects.
# The whole XMLRPC::Data object must be removed if its value key is undefined
# so it cannot be recursed like the other hash type objects.
sub _strip_undefs {
my ($initial) = @_;
if (ref $initial eq "HASH" || (blessed $initial && $initial->isa("HASH"))) {
while (my ($key, $value) = each(%$initial)) {
if ( !defined $value
|| (blessed $value && $value->isa('XMLRPC::Data') && !defined $value->value) )
{
# If the value is undefined remove it from the hash.
delete $initial->{$key};
}
else {
$initial->{$key} = _strip_undefs($value);
}
}
}
if (ref $initial eq "ARRAY" || (blessed $initial && $initial->isa("ARRAY"))) {
for (my $count = 0; $count < scalar @{$initial}; $count++) {
my $value = $initial->[$count];
if ( !defined $value
|| (blessed $value && $value->isa('XMLRPC::Data') && !defined $value->value) )
{
# If the value is undefined remove it from the array.
splice(@$initial, $count, 1);
$count--;
}
else {
$initial->[$count] = _strip_undefs($value);
}
}
}
return $initial;
}
sub BEGIN {
no strict 'refs';
for my $type (qw(double i4 int dateTime)) {
my $method = 'as_' . $type;
*$method = sub {
my ($self, $value) = @_;
if (!defined($value)) {
return as_nil();
}
else {
my $super_method = "SUPER::$method";
return $self->$super_method($value);
}
}
}
}
sub as_nil {
return ['nil', {}];
}
1;

View File

@@ -1,568 +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): Marc Schumann <wurblzap@gmail.com>
# Max Kanat-Alexander <mkanat@bugzilla.org>
# Mads Bondo Dydensborg <mbd@dbc.dk>
# Noura Elhawary <nelhawar@redhat.com>
package Bugzilla::WebService::User;
use strict;
use base qw(Bugzilla::WebService);
use Bugzilla;
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::User;
use Bugzilla::Util qw(trim);
use Bugzilla::Token;
use Bugzilla::WebService::Util qw(filter validate);
# Don't need auth to login
use constant LOGIN_EXEMPT => {
login => 1,
offer_account_by_email => 1,
};
##############
# User Login #
##############
sub login {
my ($self, $params) = @_;
my $remember = $params->{remember};
# Username and password params are required
foreach my $param ("login", "password") {
defined $params->{$param}
|| ThrowCodeError('param_required', { param => $param });
}
# Convert $remember from a boolean 0/1 value to a CGI-compatible one.
if (defined($remember)) {
$remember = $remember? 'on': '';
}
else {
# Use Bugzilla's default if $remember is not supplied.
$remember =
Bugzilla->params->{'rememberlogin'} eq 'defaulton'? 'on': '';
}
# Make sure the CGI user info class works if necessary.
my $cgi = Bugzilla->cgi;
$cgi->param('Bugzilla_login', $params->{login});
$cgi->param('Bugzilla_password', $params->{password});
$cgi->param('Bugzilla_remember', $remember);
Bugzilla->login;
return { id => $self->type('int', Bugzilla->user->id) };
}
sub logout {
my $self = shift;
Bugzilla->logout;
return undef;
}
#################
# User Creation #
#################
sub offer_account_by_email {
my $self = shift;
my ($params) = @_;
my $email = trim($params->{email})
|| ThrowCodeError('param_required', { param => 'email' });
my $createexp = Bugzilla->params->{'createemailregexp'};
if (!$createexp) {
ThrowUserError("account_creation_disabled");
}
elsif ($email !~ /$createexp/) {
ThrowUserError("account_creation_restricted");
}
$email = Bugzilla::User->check_login_name_for_creation($email);
# Create and send a token for this new account.
Bugzilla::Token::issue_new_user_account_token($email);
return undef;
}
sub create {
my $self = shift;
my ($params) = @_;
Bugzilla->user->in_group('editusers')
|| ThrowUserError("auth_failure", { group => "editusers",
action => "add",
object => "users"});
my $email = trim($params->{email})
|| ThrowCodeError('param_required', { param => 'email' });
my $realname = trim($params->{full_name});
my $password = trim($params->{password}) || '*';
my $user = Bugzilla::User->create({
login_name => $email,
realname => $realname,
cryptpassword => $password
});
return { id => $self->type('int', $user->id) };
}
# function to return user information by passing either user ids or
# login names or both together:
# $call = $rpc->call( 'User.get', { ids => [1,2,3],
# names => ['testusera@redhat.com', 'testuserb@redhat.com'] });
sub get {
my ($self, $params) = validate(@_, 'names', 'ids');
my @user_objects;
@user_objects = map { Bugzilla::User->check($_) } @{ $params->{names} }
if $params->{names};
# start filtering to remove duplicate user ids
my %unique_users = map { $_->id => $_ } @user_objects;
@user_objects = values %unique_users;
my @users;
# If the user is not logged in: Return an error if they passed any user ids.
# Otherwise, return a limited amount of information based on login names.
if (!Bugzilla->user->id){
if ($params->{ids}){
ThrowUserError("user_access_by_id_denied");
}
if ($params->{match}) {
ThrowUserError('user_access_by_match_denied');
}
@users = map {filter $params, {
id => $self->type('int', $_->id),
real_name => $self->type('string', $_->name),
name => $self->type('string', $_->login),
}} @user_objects;
return { users => \@users };
}
my $obj_by_ids;
$obj_by_ids = Bugzilla::User->new_from_list($params->{ids}) if $params->{ids};
# obj_by_ids are only visible to the user if he can see
# the otheruser, for non visible otheruser throw an error
foreach my $obj (@$obj_by_ids) {
if (Bugzilla->user->can_see_user($obj)){
if (!$unique_users{$obj->id}) {
push (@user_objects, $obj);
$unique_users{$obj->id} = $obj;
}
}
else {
ThrowUserError('auth_failure', {reason => "not_visible",
action => "access",
object => "user",
userid => $obj->id});
}
}
# User Matching
my $limit;
if ($params->{'maxusermatches'}) {
$limit = $params->{'maxusermatches'} + 1;
}
foreach my $match_string (@{ $params->{'match'} || [] }) {
my $matched = Bugzilla::User::match($match_string, $limit);
foreach my $user (@$matched) {
if (!$unique_users{$user->id}) {
push(@user_objects, $user);
$unique_users{$user->id} = $user;
}
}
}
if (Bugzilla->user->in_group('editusers')) {
@users =
map {filter $params, {
id => $self->type('int', $_->id),
real_name => $self->type('string', $_->name),
name => $self->type('string', $_->login),
email => $self->type('string', $_->email),
can_login => $self->type('boolean', $_->is_disabled ? 0 : 1),
email_enabled => $self->type('boolean', $_->email_enabled),
login_denied_text => $self->type('string', $_->disabledtext),
}} @user_objects;
}
else {
@users =
map {filter $params, {
id => $self->type('int', $_->id),
real_name => $self->type('string', $_->name),
name => $self->type('string', $_->login),
email => $self->type('string', $_->email),
can_login => $self->type('boolean', $_->is_disabled ? 0 : 1),
}} @user_objects;
}
return { users => \@users };
}
1;
__END__
=head1 NAME
Bugzilla::Webservice::User - The User Account and Login API
=head1 DESCRIPTION
This part of the Bugzilla API allows you to create User Accounts and
log in/out using an existing account.
=head1 METHODS
See L<Bugzilla::WebService> for a description of how parameters are passed,
and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean.
=head2 Logging In and Out
=over
=item C<login>
B<STABLE>
=over
=item B<Description>
Logging in, with a username and password, is required for many
Bugzilla installations, in order to search for bugs, post new bugs,
etc. This method logs in an user.
=item B<Params>
=over
=item C<login> (string) - The user's login name.
=item C<password> (string) - The user's password.
=item C<remember> (bool) B<Optional> - if the cookies returned by the
call to login should expire with the session or not. In order for
this option to have effect the Bugzilla server must be configured to
allow the user to set this option - the Bugzilla parameter
I<rememberlogin> must be set to "defaulton" or
"defaultoff". Addionally, the client application must implement
management of cookies across sessions.
=back
=item B<Returns>
On success, a hash containing one item, C<id>, the numeric id of the
user that was logged in. A set of http cookies is also sent with the
response. These cookies must be sent along with any future requests
to the webservice, for the duration of the session.
=item B<Errors>
=over
=item 300 (Invalid Username or Password)
The username does not exist, or the password is wrong.
=item 301 (Account Disabled)
The account has been disabled. A reason may be specified with the
error.
=item 50 (Param Required)
A login or password parameter was not provided.
=back
=back
=item C<logout>
B<STABLE>
=over
=item B<Description>
Log out the user. Does nothing if there is no user logged in.
=item B<Params> (none)
=item B<Returns> (nothing)
=item B<Errors> (none)
=back
=back
=head2 Account Creation
=over
=item C<offer_account_by_email>
B<STABLE>
=over
=item B<Description>
Sends an email to the user, offering to create an account. The user
will have to click on a URL in the email, and choose their password
and real name.
This is the recommended way to create a Bugzilla account.
=item B<Param>
=over
=item C<email> (string) - the email to send the offer to.
=back
=item B<Returns> (nothing)
=item B<Errors>
=over
=item 500 (Illegal Email Address)
This Bugzilla does not allow you to create accounts with the format of
email address you specified. Account creation may be entirely disabled.
=item 501 (Account Already Exists)
An account with that email address already exists in Bugzilla.
=back
=back
=item C<create>
B<EXPERIMENTAL>
=over
=item B<Description>
Creates a user account directly in Bugzilla, password and all.
Instead of this, you should use L</offer_account_by_email> when
possible, because that makes sure that the email address specified can
actually receive an email. This function does not check that.
You must be logged in and have the C<editusers> privilege in order to
call this function.
=item B<Params>
=over
=item C<email> (string) - The email address for the new user.
=item C<full_name> (string) B<Optional> - The user's full name. Will
be set to empty if not specified.
=item C<password> (string) B<Optional> - The password for the new user
account, in plain text. It will be stripped of leading and trailing
whitespace. If blank or not specified, the newly created account will
exist in Bugzilla, but will not be allowed to log in using DB
authentication until a password is set either by the user (through
resetting their password) or by the administrator.
=back
=item B<Returns>
A hash containing one item, C<id>, the numeric id of the user that was
created.
=item B<Errors>
The same as L</offer_account_by_email>. If a password is specified,
the function may also throw:
=over
=item 502 (Password Too Short)
The password specified is too short. (Usually, this means the
password is under three characters.)
=item 503 (Password Too Long)
The password specified is too long. (Usually, this means the
password is over ten characters.)
=back
=back
=back
=head2 User Info
=over
=item C<get>
B<UNSTABLE>
=over
=item B<Description>
Gets information about user accounts in Bugzilla.
=item B<Params>
B<Note>: At least one of C<ids>, C<names>, or C<match> must be specified.
B<Note>: Users will not be returned more than once, so even if a user
is matched by more than one argument, only one user will be returned.
In addition to the parameters below, this method also accepts the
standard L<include_fields|Bugzilla::WebService/include_fields> and
L<exclude_fields|Bugzilla::WebService/exclude_fields> arguments.
=over
=item C<ids> (array)
An array of integers, representing user ids.
Logged-out users cannot pass this parameter to this function. If they try,
they will get an error. Logged-in users will get an error if they specify
the id of a user they cannot see.
=item C<names> (array) - An array of login names (strings).
=item C<match> (array)
An array of strings. This works just like "user matching" in
Bugzilla itself. Users will be returned whose real name or login name
contains any one of the specified strings. Users that you cannot see will
not be included in the returned list.
Some Bugzilla installations have user-matching turned off, in which
case you will only be returned exact matches.
Most installations have a limit on how many matches are returned for
each string, which defaults to 1000 but can be changed by the Bugzilla
administrator.
Logged-out users cannot use this argument, and an error will be thrown
if they try. (This is to make it harder for spammers to harvest email
addresses from Bugzilla, and also to enforce the user visibility
restrictions that are implemented on some Bugzillas.)
=back
=item B<Returns>
A hash containing one item, C<users>, that is an array of
hashes. Each hash describes a user, and has the following items:
=over
=item id
C<int> The unique integer ID that Bugzilla uses to represent this user.
Even if the user's login name changes, this will not change.
=item real_name
C<string> The actual name of the user. May be blank.
=item email
C<string> The email address of the user.
=item name
C<string> The login name of the user. Note that in some situations this is
different than their email.
=item can_login
C<boolean> A boolean value to indicate if the user can login into bugzilla.
=item email_enabled
C<boolean> A boolean value to indicate if bug-related mail will be sent
to the user or not.
=item login_denied_text
C<string> A text field that holds the reason for disabling a user from logging
into bugzilla, if empty then the user account is enabled. Otherwise it is
disabled/closed.
B<Note>: If you are not logged in to Bugzilla when you call this function, you
will only be returned the C<id>, C<name>, and C<real_name> items. If you are
logged in and not in editusers group, you will only be returned the C<id>, C<name>,
C<real_name>, C<email>, and C<can_login> items.
=back
=item B<Errors>
=over
=item 51 (Bad Login Name)
You passed an invalid login name in the "names" array.
=item 304 (Authorization Required)
You are logged in, but you are not authorized to see one of the users you
wanted to get information about by user id.
=item 505 (User Access By Id or User-Matching Denied)
Logged-out users cannot use the "ids" or "match" arguments to this
function.
=back
=item B<History>
=over
=item Added in Bugzilla B<3.4>.
=back
=back
=back

View File

@@ -1,101 +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 Everything Solved, Inc.
# Portions created by the Initial Developer are Copyright (C) 2008
# the Initial Developer. All Rights Reserved.
#
# Contributor(s):
# Max Kanat-Alexander <mkanat@bugzilla.org>
package Bugzilla::WebService::Util;
use strict;
use base qw(Exporter);
our @EXPORT_OK = qw(filter validate);
sub filter ($$) {
my ($params, $hash) = @_;
my %newhash = %$hash;
my %include = map { $_ => 1 } @{ $params->{'include_fields'} || [] };
my %exclude = map { $_ => 1 } @{ $params->{'exclude_fields'} || [] };
foreach my $key (keys %$hash) {
if (defined $params->{include_fields}) {
delete $newhash{$key} if !$include{$key};
}
if (defined $params->{exclude_fields}) {
delete $newhash{$key} if $exclude{$key};
}
}
return \%newhash;
}
sub validate {
my ($self, $params, @keys) = @_;
# If @keys is not empty then we convert any named
# parameters that have scalar values to arrayrefs
# that match.
foreach my $key (@keys) {
if (exists $params->{$key}) {
$params->{$key} = ref $params->{$key}
? $params->{$key}
: [ $params->{$key} ];
}
}
return ($self, $params);
}
__END__
=head1 NAME
Bugzilla::WebService::Util - Utility functions used inside of the WebService
code. These are B<not> functions that can be called via the WebService.
=head1 DESCRIPTION
This is somewhat like L<Bugzilla::Util>, but these functions are only used
internally in the WebService code.
=head1 SYNOPSIS
filter({ include_fields => ['id', 'name'],
exclude_fields => ['name'] }, $hash);
validate(@_, 'ids');
=head1 METHODS
=over
=item C<filter_fields>
This helps implement the C<include_fields> and C<exclude_fields> arguments
of WebService methods. Given a hash (the second argument to this subroutine),
this will remove any keys that are I<not> in C<include_fields> and then remove
any keys that I<are> in C<exclude_fields>.
=item C<validate>
This helps in the validation of parameters passed into the WebSerice
methods. Currently it converts listed parameters into an array reference
if the client only passed a single scalar value. It modifies the parameters
hash in place so other parameters should be unaltered.
=back

View File

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

Some files were not shown because too many files have changed in this diff Show More