Compare commits

..

2 Commits

Author SHA1 Message Date
ccooper%deadsquid.com
24722f4ba1 - adding GTK1 configs to public CVS
git-svn-id: svn://10.0.0.236/branches/GTK1@214827 18797224-902f-48f8-a5cc-f745e15eee43
2006-11-06 22:31:43 +00:00
(no author)
db284d5ca7 This commit was manufactured by cvs2svn to create branch 'GTK1'.
git-svn-id: svn://10.0.0.236/branches/GTK1@214801 18797224-902f-48f8-a5cc-f745e15eee43
2006-11-06 20:11:04 +00:00
780 changed files with 201 additions and 153954 deletions

View File

@@ -0,0 +1,25 @@
# Commenting out sourcing browser/config/mozconfig in favor of add'l client.mk
# features.
# . $topsrcdir/browser/config/mozconfig
mk_add_options MOZ_CO_PROJECT=browser
mk_add_options MOZ_CO_MODULE="mozilla/tools/trace-malloc"
ac_add_options --enable-application=browser
# Yeah, I want this to be a debug build.
ac_add_options --disable-optimize
ac_add_options --enable-debug
# I ought to do this, if this machine had the needed libraries...:
# ac_add_options --enable-xft
# ac_add_options --enable-default-toolkit=gtk2
ac_add_options --enable-default-toolkit=gtk
ac_add_options --enable-trace-malloc
#ac_add_options --enable-places
# Disable Cairo.
ac_add_options --disable-canvas
ac_add_options --disable-svg

View File

@@ -0,0 +1,176 @@
#- tinder-config.pl - Tinderbox configuration file.
#- Uncomment the variables you need to set.
#- The default values are the same as the commented variables.
$ENV{PATH} = "/usr/local/binutils-2.13.2.1/bin:/usr/local/gcc-3.4.3/bin:" . $ENV{PATH};
#- PLEASE FILL THIS IN WITH YOUR PROPER EMAIL ADDRESS
#$BuildAdministrator = "$ENV{USER}\@$ENV{HOST}";
#$BuildAdministrator = ($ENV{USER} || "cltbld") . "\@" . ($ENV{HOST} || "dhcp");
$BuildAdministrator = "dbaron\@dbaron.org";
#- You'll need to change these to suit your machine's needs
#$DisplayServer = ':0.0';
#- Default values of command-line opts
#-
#$BuildDepend = 1; # Depend or Clobber
#$BuildDebug = 0; # Debug or Opt (Darwin)
#$ReportStatus = 1; # Send results to server, or not
#$ReportFinalStatus = 1; # Finer control over $ReportStatus.
#$UseTimeStamp = 1; # Use the CVS 'pull-by-timestamp' option, or not
#$BuildOnce = 0; # Build once, don't send results to server
#$TestOnly = 0; # Only run tests, don't pull/build
#$BuildEmbed = 0; # After building seamonkey, go build embed app.
#$SkipMozilla = 0; # Use to debug post-mozilla.pl scripts.
# Tests
$CleanProfile = 1;
#$ResetHomeDirForTests = 1;
$ProductName = "Firefox";
$VendorName = "Mozilla";
$ENV{NO_EM_RESTART} = '1';
$ENV{XPCOM_DEBUG_BREAK} = 'abort';
#$RunMozillaTests = 1; # Allow turning off of all tests if needed.
#$RegxpcomTest = 1;
#$AliveTest = 1;
#$JavaTest = 0;
#$ViewerTest = 0;
$BloatTest = 1; # warren memory bloat test
$BloatTest2 = 1; # dbaron memory bloat test, require tracemalloc
#$DomToTextConversionTest = 0;
#$XpcomGlueTest = 0;
#$CodesizeTest = 0; # Z, require mozilla/tools/codesighs
#$EmbedCodesizeTest = 0; # mZ, require mozilla/tools/codesigns
#$MailBloatTest = 0;
#$EmbedTest = 0; # Assumes you wanted $BuildEmbed=1
#$LayoutPerformanceTest = 0; # Tp
#$QATest = 0;
#$XULWindowOpenTest = 0; # Txul
#$StartupPerformanceTest = 0; # Ts
$TestsPhoneHome = 1; # Should test report back to server?
#$results_server = "axolotl.mozilla.org"; # was tegu
#$pageload_server = "spider"; # localhost
#
# Timeouts, values are in seconds.
#
#$CreateProfileTimeout = 45;
$RegxpcomTestTimeout = 60;
#$AliveTestTimeout = 45;
#$ViewerTestTimeout = 45;
#$EmbedTestTimeout = 45;
$BloatTestTimeout = 240; # seconds
#$MailBloatTestTimeout = 120; # seconds
#$JavaTestTimeout = 45;
#$DomTestTimeout = 45; # seconds
#$XpcomGlueTestTimeout = 15;
#$CodesizeTestTimeout = 900; # seconds
#$CodesizeTestType = "auto"; # {"auto"|"base"}
#$LayoutPerformanceTestTimeout = 1200; # entire test, seconds
#$QATestTimeout = 1200; # entire test, seconds
#$LayoutPerformanceTestPageTimeout = 30000; # each page, ms
#$StartupPerformanceTestTimeout = 60; # seconds
#$XULWindowOpenTestTimeout = 150; # seconds
#$MozConfigFileName = 'mozconfig';
#$UseMozillaProfile = 1;
#$MozProfileName = 'default';
#- Set these to what makes sense for your system
#$Make = 'gmake'; # Must be GNU make
#$MakeOverrides = '';
#$mail = '/bin/mail';
#$CVS = 'cvs -q';
#$CVSCO = 'checkout -P';
# win32 usually doesn't have /bin/mail
#$blat = 'c:/nstools/bin/blat';
#$use_blat = 0;
# Set moz_cvsroot to something like:
# :pserver:$ENV{USER}%netscape.com\@cvs.mozilla.org:/cvsroot
# :pserver:anonymous\@cvs-mirror.mozilla.org:/cvsroot
#
# Note that win32 may not need \@, depends on ' or ".
# :pserver:$ENV{USER}%netscape.com@cvs.mozilla.org:/cvsroot
$moz_cvsroot = ':ext:cltbld@cvs.mozilla.org:/cvsroot';
#$moz_cvsroot = ":pserver:anonymous\@cvs-mirror.mozilla.org:/cvsroot";
#- Set these proper values for your tinderbox server
#$Tinderbox_server = 'tinderbox-daemon@tinderbox.mozilla.org';
# Allow for non-client builds, e.g. camino.
#$moz_client_mk = 'client.mk';
#- Set if you want to build in a separate object tree
$ObjDir = 'obj';
# Extra build name, if needed.
$BuildNameExtra = 'GTK1 (gcc 3.4)';
# User comment, eg. ip address for dhcp builds.
# ex: $UserComment = "ip = 208.12.36.108";
#$UserComment = 0;
#-
#- The rest should not need to be changed
#-
#- Minimum wait period from start of build to start of next build in minutes.
#$BuildSleep = 10;
#- Until you get the script working. When it works,
#- change to the tree you're actually building
$BuildTree = 'Firefox';
#$BuildName = '';
#$BuildTag = '';
#$BuildConfigDir = 'mozilla/config';
#$Topsrcdir = 'mozilla';
$BinaryName = 'firefox-bin';
#
# For embedding app, use:
#$EmbedBinaryName = 'TestGtkEmbed';
#$EmbedDistDir = 'dist/bin'
#$ShellOverride = ''; # Only used if the default shell is too stupid
#$ConfigureArgs = '';
#$ConfigureEnvArgs = '';
#$Compiler = 'gcc';
#$NSPRArgs = '';
#$ShellOverride = '';
# allow override of timezone value (for win32 POSIX::strftime)
#$Timezone = '';
# Reboot the OS at the end of build-and-test cycle. This is primarily
# intended for Win9x, which can't last more than a few cycles before
# locking up (and testing would be suspect even after a couple of cycles).
# Right now, there is only code to force the reboot for Win9x, so even
# setting this to 1, will not have an effect on other platforms. Setting
# up win9x to automatically logon and begin running tinderbox is left
# as an exercise to the reader.
#$RebootSystem = 0;
# LogCompression specifies the type of compression used on the log file.
# Valid options are 'gzip', and 'bzip2'. Please make sure the binaries
# for 'gzip' or 'bzip2' are in the user's path before setting this
# option.
$LogCompression = 'bzip2';
# LogEncoding specifies the encoding format used for the logs. Valid
# options are 'base64', and 'uuencode'. If $LogCompression is set above,
# this needs to be set to 'base64' or 'uuencode' to ensure that the
# binary data is transferred properly.
$LogEncoding = 'uuencode';

View File

@@ -1,32 +0,0 @@
.htaccess
/lib/*
/template/en/custom
/docs/bugzilla.ent
/docs/en/xml/bugzilla.ent
/docs/en/txt
/docs/en/html
/docs/en/pdf
/skins/custom
/graphs
/data
/localconfig
/index.html
/skins/contrib/Dusk/IE-fixes.css
/skins/contrib/Dusk/admin.css
/skins/contrib/Dusk/attachment.css
/skins/contrib/Dusk/create_attachment.css
/skins/contrib/Dusk/dependency-tree.css
/skins/contrib/Dusk/duplicates.css
/skins/contrib/Dusk/editusers.css
/skins/contrib/Dusk/enter_bug.css
/skins/contrib/Dusk/help.css
/skins/contrib/Dusk/panel.css
/skins/contrib/Dusk/page.css
/skins/contrib/Dusk/params.css
/skins/contrib/Dusk/reports.css
/skins/contrib/Dusk/show_bug.css
/skins/contrib/Dusk/search_form.css
/skins/contrib/Dusk/show_multiple.css
/skins/contrib/Dusk/summarize-time.css
.DS_Store

View File

@@ -1 +0,0 @@
7819

View File

@@ -1,32 +0,0 @@
.htaccess
/lib/*
/template/en/custom
/docs/bugzilla.ent
/docs/en/xml/bugzilla.ent
/docs/en/txt
/docs/en/html
/docs/en/pdf
/skins/custom
/graphs
/data
/localconfig
/index.html
/skins/contrib/Dusk/IE-fixes.css
/skins/contrib/Dusk/admin.css
/skins/contrib/Dusk/attachment.css
/skins/contrib/Dusk/create_attachment.css
/skins/contrib/Dusk/dependency-tree.css
/skins/contrib/Dusk/duplicates.css
/skins/contrib/Dusk/editusers.css
/skins/contrib/Dusk/enter_bug.css
/skins/contrib/Dusk/help.css
/skins/contrib/Dusk/panel.css
/skins/contrib/Dusk/page.css
/skins/contrib/Dusk/params.css
/skins/contrib/Dusk/reports.css
/skins/contrib/Dusk/show_bug.css
/skins/contrib/Dusk/search_form.css
/skins/contrib/Dusk/show_multiple.css
/skins/contrib/Dusk/summarize-time.css
.DS_Store

View File

@@ -1 +0,0 @@
f80ce0c0974010ba97b1467e764e5f2f239fe514

View File

@@ -1,25 +0,0 @@
# Don't allow people to retrieve non-cgi executable files or our private data
<FilesMatch (\.pm|\.pl|\.tmpl|localconfig.*)$>
deny from all
</FilesMatch>
<IfModule mod_expires.c>
<IfModule mod_headers.c>
<IfModule mod_env.c>
<FilesMatch (\.js|\.css)$>
ExpiresActive On
# According to RFC 2616, "1 year in the future" means "never expire".
# We change the name of the file's URL whenever its modification date
# changes, so browsers can cache any individual JS or CSS URL forever.
# However, since all JS and CSS URLs involve a ? in them (for the changing
# name) we have to explicitly set an Expires header or browsers won't
# *ever* cache them.
ExpiresDefault "now plus 1 years"
Header append Cache-Control "public"
</FilesMatch>
# This lets Bugzilla know that we are properly sending Cache-Control
# and Expires headers for CSS and JS files.
SetEnv BZ_CACHE_CONTROL 1
</IfModule>
</IfModule>
</IfModule>

View File

@@ -1,56 +0,0 @@
language: perl
addons:
postgresql: "9.1"
perl:
- 5.10
- 5.12
env:
- TEST_SUITE=sanity
- TEST_SUITE=docs
- TEST_SUITE=webservices DB=mysql
- TEST_SUITE=selenium DB=mysql
- TEST_SUITE=webservices DB=pg
- TEST_SUITE=selenium DB=pg
matrix:
exclude:
- perl: 5.12
env: TEST_SUITE=docs
- perl: 5.10
env: TEST_SUITE=webservices DB=mysql
- perl: 5.12
env: TEST_SUITE=selenium DB=mysql
- perl: 5.10
env: TEST_SUITE=webservices DB=pg
- perl: 5.12
env: TEST_SUITE=selenium DB=pg
before_install:
- git clone https://github.com/bugzilla/qa.git -b 4.0 qa
install: true
before_script:
- mysql -u root mysql -e "GRANT ALL PRIVILEGES ON *.* TO bugs@localhost IDENTIFIED BY 'bugs'; FLUSH PRIVILEGES;"
- psql -c "CREATE USER bugs WITH PASSWORD 'bugs' CREATEDB;" -U postgres
script: ./qa/travis.sh
after_failure:
- sudo cat /var/log/apache2/error.log
notifications:
irc:
channels:
- "irc.mozilla.org#qa-bugzilla"
- "irc.mozilla.org#bugzilla"
template:
- "Bugzilla %{branch} : %{author} : %{message}"
- "Commit Message : %{commit_message}"
- "Commit Link : %{compare_url}"
- "Build Link : %{build_url}"
on_success: change
on_failure: always

View File

@@ -1,871 +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::Extension;
use Bugzilla::DB;
use Bugzilla::Install::Localconfig qw(read_localconfig);
use Bugzilla::Install::Requirements qw(OPTIONAL_MODULES);
use Bugzilla::Install::Util qw(init_console);
use Bugzilla::Template;
use Bugzilla::User;
use Bugzilla::Error;
use Bugzilla::Util;
use Bugzilla::Field;
use Bugzilla::Flag;
use Bugzilla::Token;
use File::Basename;
use File::Spec::Functions;
use DateTime::TimeZone;
use Date::Parse;
use Safe;
#####################################################################
# Constants
#####################################################################
# Scripts that are not stopped by shutdownhtml being in effect.
use constant SHUTDOWNHTML_EXEMPT => qw(
editparams.cgi
checksetup.pl
migrate.pl
recode.pl
);
# Non-cgi scripts that should silently exit.
use constant SHUTDOWNHTML_EXIT_SILENTLY => qw(
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 {
if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
init_console();
}
elsif (Bugzilla->params->{'utf8'}) {
binmode STDOUT, ':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'} = '';
}
# Because this function is run live from perl "use" commands of
# other scripts, we're skipping the rest of this function if we get here
# during a perl syntax check (perl -c, like we do during the
# 001compile.t test).
return if $^C;
# IIS prints out warnings to the webpage, so ignore them, or log them
# to a file if the file exists.
if ($ENV{SERVER_SOFTWARE} && $ENV{SERVER_SOFTWARE} =~ /microsoft-iis/i) {
$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();
}
};
}
my $script = basename($0);
# Because of attachment_base, attachment.cgi handles this itself.
if ($script ne 'attachment.cgi') {
do_ssl_redirect_if_required();
}
# If Bugzilla is shut down, do not allow anything to run, just display a
# message to the user about the downtime and log out. Scripts listed in
# SHUTDOWNHTML_EXEMPT are exempt from this message.
#
# This code must go here. It cannot go anywhere in Bugzilla::CGI, because
# it uses Template, and that causes various dependency loops.
if (Bugzilla->params->{"shutdownhtml"}
&& !grep { $_ eq $script } SHUTDOWNHTML_EXEMPT)
{
# 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 (!i_am_cgi()
&& grep { $_ eq $script } SHUTDOWNHTML_EXIT_SILENTLY)
{
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;
}
}
#####################################################################
# Subroutines and Methods
#####################################################################
sub template {
my $class = shift;
$class->request_cache->{template} ||= Bugzilla::Template->create();
return $class->request_cache->{template};
}
sub template_inner {
my ($class, $lang) = @_;
my $cache = $class->request_cache;
my $current_lang = $cache->{template_current_lang}->[0];
$lang ||= $current_lang || '';
$class->request_cache->{"template_inner_$lang"}
||= Bugzilla::Template->create(language => $lang);
return $class->request_cache->{"template_inner_$lang"};
}
our $extension_packages;
sub extensions {
my ($class) = @_;
my $cache = $class->request_cache;
if (!$cache->{extensions}) {
# Under mod_perl, mod_perl.pl populates $extension_packages for us.
if (!$extension_packages) {
$extension_packages = Bugzilla::Extension->load_all();
}
my @extensions;
foreach my $package (@$extension_packages) {
my $extension = $package->new();
if ($extension->enabled) {
push(@extensions, $extension);
}
}
$cache->{extensions} = \@extensions;
}
return $cache->{extensions};
}
sub feature {
my ($class, $feature) = @_;
my $cache = $class->request_cache;
return $cache->{feature}->{$feature}
if exists $cache->{feature}->{$feature};
my $feature_map = $cache->{feature_map};
if (!$feature_map) {
foreach my $package (@{ OPTIONAL_MODULES() }) {
foreach my $f (@{ $package->{feature} }) {
$feature_map->{$f} ||= [];
push(@{ $feature_map->{$f} }, $package->{module});
}
}
$cache->{feature_map} = $feature_map;
}
if (!$feature_map->{$feature}) {
ThrowCodeError('invalid_feature', { feature => $feature });
}
my $success = 1;
foreach my $module (@{ $feature_map->{$feature} }) {
# We can't use a string eval and "use" here (it kills Template-Toolkit,
# see https://rt.cpan.org/Public/Bug/Display.html?id=47929), so we have
# to do a block eval.
$module =~ s{::}{/}g;
$module .= ".pm";
eval { require $module; 1; } or $success = 0;
}
$cache->{feature}->{$feature} = $success;
return $success;
}
sub cgi {
my $class = shift;
$class->request_cache->{cgi} ||= new Bugzilla::CGI();
return $class->request_cache->{cgi};
}
sub input_params {
my ($class, $params) = @_;
my $cache = $class->request_cache;
# This is how the WebService and other places set input_params.
if (defined $params) {
$cache->{input_params} = $params;
}
return $cache->{input_params} if defined $cache->{input_params};
# Making this scalar makes it a tied hash to the internals of $cgi,
# so if a variable is changed, then it actually changes the $cgi object
# as well.
$cache->{input_params} = $class->cgi->Vars;
return $cache->{input_params};
}
sub localconfig {
my $class = shift;
$class->request_cache->{localconfig} ||= read_localconfig();
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 $token = $class->cgi->cookie('sudo');
if (defined $authenticated_user && $token) {
my ($user_id, $date, $sudo_target_id) = Bugzilla::Token::GetTokenData($token);
if (!$user_id
|| $user_id != $authenticated_user->id
|| !detaint_natural($sudo_target_id)
|| (time() - str2time($date) > MAX_SUDO_TOKEN_AGE))
{
$class->cgi->remove_cookie('sudo');
ThrowUserError('sudo_invalid_cookie');
}
my $sudo_target = new Bugzilla::User($sudo_target_id);
if ($authenticated_user->in_group('bz_sudoers')
&& 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 {
delete_token($token);
$class->cgi->remove_cookie('sudo');
ThrowUserError('sudo_illegal_action', { sudoer => $authenticated_user,
target_user => $sudo_target });
}
}
else {
$class->set_user($authenticated_user);
}
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;
require Bugzilla::JobQueue;
$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->dbh_main;
return $class->request_cache->{dbh};
}
sub dbh_main {
my $class = shift;
$class->request_cache->{dbh_main} ||= Bugzilla::DB::connect_main();
return $class->request_cache->{dbh_main};
}
sub languages {
my $class = shift;
return Bugzilla::Install::Util::supported_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);
}
# This is used only by Bugzilla::Error to throw errors.
sub _json_server {
my ($class, $newval) = @_;
if (defined $newval) {
$class->request_cache->{_json_server} = $newval;
}
return $class->request_cache->{_json_server};
}
sub usage_mode {
my ($class, $newval) = @_;
if (defined $newval) {
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_XMLRPC) {
$class->error_mode(ERROR_MODE_DIE_SOAP_FAULT);
}
elsif ($newval == USAGE_MODE_JSON) {
$class->error_mode(ERROR_MODE_JSON_RPC);
}
elsif ($newval == USAGE_MODE_EMAIL) {
$class->error_mode(ERROR_MODE_DIE);
}
elsif ($newval == USAGE_MODE_TEST) {
$class->error_mode(ERROR_MODE_TEST);
}
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} = $class->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->dbh_main;
return $class->dbh_main;
}
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->any_exist;
}
return $class->request_cache->{has_flags};
}
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};
}
# This creates the request cache for non-mod_perl installations.
# This is identical to Install::Util::_cache so that things loaded
# into Install::Util::_cache during installation can be read out
# of request_cache later in installation.
our $_request_cache = $Bugzilla::Install::Util::_cache;
sub request_cache {
if ($ENV{MOD_PERL}) {
require Apache2::RequestUtil;
# Sometimes (for example, during mod_perl.pl), the request
# object isn't available, and we should use $_request_cache instead.
my $request = eval { Apache2::RequestUtil->request };
return $_request_cache if !$request;
return $request->pnotes();
}
return $_request_cache;
}
# 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};
}
init_page() if !$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<input_params>
When running under the WebService, this is a hashref containing the arguments
passed to the WebService method that was called. When running in a normal
script, this is a hashref containing the contents of the CGI parameters.
Modifying this hashref will modify the CGI parameters or the WebService
arguments (depending on what C<input_params> currently represents).
This should be used instead of L</cgi> in situations where your code
could be being called by either a normal CGI script or a WebService method,
such as during a code hook.
B<Note:> When C<input_params> represents the CGI parameters, any
parameter specified more than once (like C<foo=bar&foo=baz>) will appear
as an arrayref in the hash, but any value specified only once will appear
as a scalar. This means that even if a value I<can> appear multiple times,
if it only I<does> appear once, then it will be a scalar in C<input_params>,
not an arrayref.
=item C<user>
C<undef> if there is no currently logged in user or if the login code has not
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_XMLRPC)> near the
beginning of your script to change this flag's default of
C<Bugzilla::Constants::USAGE_MODE_BROWSER> and to indicate that Bugzilla is
being called in a non-interactive manner.
This influences error handling because on usage mode changes, C<usage_mode>
calls C<Bugzilla->error_mode> to set an error mode which makes sense for the
usage mode.
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<dbh_main>
The main 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<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.
=item C<feature>
Tells you whether or not a specific feature is enabled. For names
of features, see C<OPTIONAL_MODULES> in C<Bugzilla::Install::Requirements>.
=back

File diff suppressed because it is too large Load Diff

View File

@@ -1,297 +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',
-x_content_type_options => "nosniff",
-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',
-x_content_type_options => "nosniff",
-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,525 +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::Mailer;
use Bugzilla::Util qw(datetime_from);
use Bugzilla::User::Setting ();
use Bugzilla::Auth::Login::Stack;
use Bugzilla::Auth::Verify::Stack;
use Bugzilla::Auth::Persist::Cookie;
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) {
# We don't persist logins over GET requests in the WebService,
# because the persistance information can't be re-used again.
# (See Bugzilla::WebService::Server::JSONRPC for more info.)
if ($self->{_info_getter}->{successful}->requires_persistence
and !Bugzilla->request_cache->{auth_no_automatic_login})
{
$self->{_persister}->persist_login($user);
}
}
elsif ($fail_code == AUTH_ERROR) {
if ($result->{user_error}) {
ThrowUserError($result->{user_error}, $result->{details});
}
else {
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) {
my $remaining_attempts = MAX_LOGIN_ATTEMPTS
- ($result->{failure_count} || 0);
ThrowUserError("invalid_username_or_password",
{ remaining => $remaining_attempts });
}
# The account may be disabled
elsif ($fail_code == AUTH_DISABLED) {
$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});
}
elsif ($fail_code == AUTH_LOCKOUT) {
my $attempts = $user->account_ip_login_failures;
# We want to know when the account will be unlocked. This is
# determined by the 5th-from-last login failure (or more/less than
# 5th, if MAX_LOGIN_ATTEMPTS is not 5).
my $determiner = $attempts->[scalar(@$attempts) - MAX_LOGIN_ATTEMPTS];
my $unlock_at = datetime_from($determiner->{login_time},
Bugzilla->local_timezone);
$unlock_at->add(minutes => LOGIN_LOCKOUT_INTERVAL);
# If we were *just* locked out, notify the maintainer about the
# lockout.
if ($result->{just_locked_out}) {
# We're sending to the maintainer, who may be not a Bugzilla
# account, but just an email address. So we use the
# installation's default language for sending the email.
my $default_settings = Bugzilla::User::Setting::get_defaults();
my $template = Bugzilla->template_inner(
$default_settings->{lang}->{default_value});
my $vars = {
locked_user => $user,
attempts => $attempts,
unlock_at => $unlock_at,
};
my $message;
$template->process('email/lockout.txt.tmpl', $vars, \$message)
|| ThrowTemplateError($template->error);
MessageToMTA($message);
}
$unlock_at->set_time_zone($user->timezone);
ThrowUserError('account_locked',
{ ip_addr => $determiner->{ip_addr}, unlock_at => $unlock_at });
}
# If we get here, then we've run out of options, which shouldn't happen.
else {
ThrowCodeError("authres_unhandled", { value => $fail_code });
}
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.
The hashref may also contain a C<failure_count> element, which specifies
how many times the account has failed to log in within the lockout
period (see L</AUTH_LOCKOUT>). This is used to warn the user when
he is getting close to being locked out.
=head2 C<AUTH_NO_SUCH_USER>
This is an optional more-specific version of C<AUTH_LOGINFAILED>.
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>.
=head2 C<AUTH_LOCKOUT>
The user's account is locked out after having failed to log in too many
times within a certain period of time (as specified by
L<Bugzilla::Constants/LOGIN_LOCKOUT_INTERVAL>).
The hashref will also contain a C<user> element, representing the
L<Bugzilla::User> whose account is locked out.
=head1 LOGIN TYPES
The C<login> function (below) can do different types of login, depending
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,134 +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;
use constant is_automatic => 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>.
=item C<is_automatic>
True if this login method requires no interaction from the user within
Bugzilla. (For example, C<Env> auth is "automatic" because the webserver
just passes us an environment variable on most page requests, and does not
ask the user for authentication information directly in Bugzilla.) Defaults
to C<false>.
=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 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 $params = Bugzilla->input_params;
my $username = trim(delete $params->{"Bugzilla_login"});
my $password = delete $params->{"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->usage_mode != USAGE_MODE_BROWSER) {
ThrowUserError('login_required');
}
print $cgi->header();
$template->process("account/auth/login.html.tmpl",
{ 'target' => $cgi->url(-relative=>1) })
|| ThrowTemplateError($template->error());
exit;
}
1;

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.
#
# 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;
use constant is_automatic => 1;
# 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 = remote_ip();
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 $db_cookie =
$dbh->selectrow_array('SELECT cookie
FROM logincookies
WHERE cookie = ?
AND userid = ?
AND (ipaddr = ? OR ipaddr IS NULL)',
undef, ($login_cookie, $user_id, $ip_addr));
# If the cookie is valid, return a valid username.
if (defined $db_cookie && $login_cookie eq $db_cookie) {
# 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,54 +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_persistence => 0;
use constant requires_verification => 0;
use constant is_automatic => 1;
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,100 +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}}) {
# See Bugzilla::WebService::Server::JSONRPC for where and why
# auth_no_automatic_login is used.
if (Bugzilla->request_cache->{auth_no_automatic_login}) {
next if $object->is_automatic;
}
$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,163 +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 $input_params = Bugzilla->input_params;
my $ip_addr;
if ($input_params->{'Bugzilla_restrictlogin'}) {
$ip_addr = remote_ip();
# The IP address is valid, at least for comparing with itself in a
# subsequent login
trick_taint($ip_addr);
}
$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' &&
$input_params->{'Bugzilla_remember'} &&
$input_params->{'Bugzilla_remember'} eq 'on') )
{
# Not a session cookie, so set an infinite expiry
$cookieargs{'-expires'} = 'Fri, 01-Jan-2038 00:00:00 GMT';
}
if (Bugzilla->params->{'ssl_redirect'}) {
# Make these cookies only be sent to us by the browser during
# HTTPS sessions, if we're using SSL.
$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,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>
# 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 = new Bugzilla::User({ name => $username });
return { failure => AUTH_NO_SUCH_USER } unless $user;
$login_data->{user} = $user;
$login_data->{bz_username} = $user->login;
if ($user->account_is_locked_out) {
return { failure => AUTH_LOCKOUT, user => $user };
}
my $password = $login_data->{password};
my $real_password_crypted = $user->cryptpassword;
# Using the internal crypted password as the salt,
# crypt the password the user entered.
my $entered_password_crypted = bz_crypt($password, $real_password_crypted);
if ($entered_password_crypted ne $real_password_crypted) {
# Record the login failure
$user->note_login_failure();
# Immediately check if we are locked out
if ($user->account_is_locked_out) {
return { failure => AUTH_LOCKOUT, user => $user,
just_locked_out => 1 };
}
return { failure => AUTH_LOGINFAILED,
failure_count => scalar(@{ $user->account_ip_login_failures }),
};
}
# Force the user to type a longer password if it's too short.
if (length($password) < USER_PASSWORD_MIN_LENGTH) {
return { failure => AUTH_ERROR, user_error => 'password_current_too_short',
details => { locked_user => $user } };
}
# The user's credentials are okay, so delete any outstanding
# password tokens or login failures they may have generated.
Bugzilla::Token::DeletePasswordTokens($user->id, "user_logged_in");
$user->clear_login_failures();
# If their old password was using crypt() or some different hash
# than we're using now, convert the stored password to using
# whatever hashing system we're using now.
my $current_algorithm = PASSWORD_DIGEST_ALGORITHM;
if ($real_password_crypted !~ /{\Q$current_algorithm\E}$/) {
$user->set_password($password);
$user->update();
}
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,201 +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 Net::LDAP::Util qw(escape_filter_value);
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_for_search();
# 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.
# First try the search as the (already bound) user in question.
my $user_entry;
my $error_string;
my $detail_result = $self->ldap->search(_bz_search_params($username));
if ($detail_result->code) {
# Stash away the original error, just in case
$error_string = $detail_result->error;
} else {
$user_entry = $detail_result->shift_entry;
}
# If that failed (either because the search failed, or returned no
# results) then try re-binding as the initial search user, but only
# if the LDAPbinddn parameter is set.
if (!$user_entry && Bugzilla->params->{"LDAPbinddn"}) {
$self->_bind_ldap_for_search();
$detail_result = $self->ldap->search(_bz_search_params($username));
if (!$detail_result->code) {
$user_entry = $detail_result->shift_entry;
}
}
# If we *still* don't have anything in $user_entry then give up.
return { failure => AUTH_ERROR, error => "ldap_search_error",
details => {errstr => $error_string, username => $username}
} if !$user_entry;
my $mail_attr = Bugzilla->params->{"LDAPmailattribute"};
if ($mail_attr) {
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) = @_;
$username = escape_filter_value($username);
return (base => Bugzilla->params->{"LDAPBaseDN"},
scope => "sub",
filter => '(&(' . Bugzilla->params->{"LDAPuidattribute"}
. "=$username)"
. Bugzilla->params->{"LDAPfilter"} . ')');
}
sub _bind_ldap_for_search {
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,596 +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>
# Reed Loden <reed@reedloden.com>
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 Bugzilla::Hook;
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 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);
}
sub relationships {
my $ref = RELATIONSHIPS;
# Clone it so that we don't modify the constant;
my %relationships = %$ref;
Bugzilla::Hook::process('bugmail_relationships',
{ relationships => \%relationships });
return %relationships;
}
# 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
sub Send {
my ($id, $forced, $params) = (@_);
$params ||= {};
my $dbh = Bugzilla->dbh;
my $bug = new Bugzilla::Bug($id);
# Only used for headers in bugmail for new bugs
my @fields = Bugzilla->get_fields({obsolete => 0, mailhead => 1});
my $start = $bug->lastdiffed;
my $end = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
# Bugzilla::User objects 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 @assignees = ($bug->assigned_to);
my @qa_contacts = ($bug->qa_contact);
my @ccs = @{ $bug->cc_users };
# 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, Bugzilla::User->check($forced->{'owner'}));
}
if ($forced->{'qacontact'}) {
push (@qa_contacts, Bugzilla::User->check($forced->{'qacontact'}));
}
if ($forced->{'cc'}) {
foreach my $cc (@{$forced->{'cc'}}) {
push(@ccs, Bugzilla::User->check($cc));
}
}
my @args = ($bug->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,
bugs_activity.comment_id
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, $comment_id) = (@$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));
}
if ($fieldname eq 'longdescs.isprivate') {
my $comment = Bugzilla::Comment->new($comment_id);
my $comment_num = $comment->count;
$what =~ s/^(Comment )?/Comment #$comment_num /;
$diffpart->{'isprivate'} = $new;
}
$difftext = three_columns($what, $old, $new);
$diffpart->{'header'} = $diffheader;
$diffpart->{'fieldname'} = $fieldname;
$diffpart->{'text'} = $difftext;
push(@diffparts, $diffpart);
push(@changedfields, $what);
}
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,
fielddefs.description, 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, $fieldname, $what, $old, $new) = @$dependency_diff;
if ($depbug ne $lastbug) {
if ($interestingchange) {
$deptext .= $thisdiff;
}
$lastbug = $depbug;
$thisdiff =
"\nBug $id depends on bug $depbug, which changed state.\n\n" .
"Bug $depbug Summary: $summary\n" .
correct_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($what, $old, $new);
if ($fieldname 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($deptext);
push(@diffparts, $diffpart);
}
}
my $comments = $bug->comments({ after => $start, to => $end });
# Skip empty comments.
@$comments = grep { $_->type || $_->body =~ /\S/ } @$comments;
###########################################################################
# 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.
# CCs
$recipients{$_->id}->{+REL_CC} = BIT_DIRECT foreach (@ccs);
# Reporter (there's only ever one)
$recipients{$bug->reporter->id}->{+REL_REPORTER} = BIT_DIRECT;
# QA Contact
if (Bugzilla->params->{'useqacontact'}) {
foreach (@qa_contacts) {
# QA Contact can be blank; ignore it if so.
$recipients{$_->id}->{+REL_QA} = BIT_DIRECT if $_;
}
}
# Assignee
$recipients{$_->id}->{+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, so we don't check that
# relationship here.
# 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;
}
}
}
Bugzilla::Hook::process('bugmail_recipients',
{ bug => $bug, recipients => \%recipients,
diffs => $diffs });
# 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;
# The email client will display the Date: header in the desired timezone,
# so we can always use UTC here.
my $date = $params->{dep_only} ? $end : $bug->delta_ts;
$date = format_time($date, '%a, %d %b %Y %T %z', 'UTC');
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?
# 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 && $dep_ok) {
# OK, OK, if we must. Email the user.
$sent_mail = sendMail(
{ to => $user,
fields => \@fields,
bug => $bug,
comments => $comments,
is_new => !$start,
date => $date,
changer => $changer,
watchers => exists $watching{$user_id} ?
$watching{$user_id} : undef,
diff_parts => \@diffparts,
rels_which_want => \%rels_which_want,
changed_fields => \@changedfields,
});
}
}
if ($sent_mail) {
push(@sent, $user->login);
}
else {
push(@excluded, $user->login);
}
}
$dbh->do('UPDATE bugs SET lastdiffed = ? WHERE bug_id = ?',
undef, ($end, $id));
$bug->{lastdiffed} = $end;
return {'sent' => \@sent, 'excluded' => \@excluded};
}
sub sendMail {
my $params = shift;
my $user = $params->{to};
my @fields = @{ $params->{fields} };
my $bug = $params->{bug};
my @send_comments = @{ $params->{comments} };
my $isnew = $params->{is_new};
my $date = $params->{date};
my $changer = $params->{changer};
my $watchingRef = $params->{watchers};
my @diffparts = @{ $params->{diff_parts} };
my $relRef = $params->{rels_which_want};
my @changed_fields = @{ $params->{changed_fields} };
# 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 (!$user->is_insider) {
@send_comments = grep { !$_->is_private } @send_comments;
}
if ($difftext eq "" && !scalar(@send_comments) && !$isnew) {
# Whoops, no differences!
return 0;
}
my $diffs = $difftext;
# Remove extra newlines.
$diffs =~ s/^\n+//s; $diffs =~ s/\n+$//s;
if ($isnew) {
my $head = "";
foreach my $field (@fields) {
my $name = $field->name;
my $value = $bug->$name;
if (ref $value eq 'ARRAY') {
$value = join(', ', @$value);
}
elsif (ref $value && $value->isa('Bugzilla::User')) {
$value = $value->login;
}
elsif (ref $value && $value->isa('Bugzilla::Object')) {
$value = $value->name;
}
elsif ($name eq 'estimated_time') {
$value = ($value == 0) ? 0 : format_time_decimal($value);
}
elsif ($name eq 'deadline') {
$value = time2str("%Y-%m-%d", str2time($value)) if $value;
}
# 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 (($name ne 'estimated_time' && $name ne 'deadline') || $user->is_timetracker) {
my $desc = $field->description;
$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 %relationships = relationships();
my @headerrel = map { $relationships{$_} } @reasons;
my @watchingrel = map { $relationships{$_} } @reasons_watch;
push(@headerrel, 'None') unless @headerrel;
push(@watchingrel, 'None') unless @watchingrel;
push @watchingrel, map { user_id_to_login($_) } @$watchingRef;
my $vars = {
isnew => $isnew,
date => $date,
to_user => $user,
bug => $bug,
changedfields => \@changed_fields,
reasons => \@reasons,
reasons_watch => \@reasons_watch,
reasonsheader => join(" ", @headerrel),
reasonswatchheader => join(" ", @watchingrel),
changer => $changer,
diffs => $diffs,
new_comments => \@send_comments,
threadingmarker => build_thread_marker($bug->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());
MessageToMTA($msg);
return 1;
}
1;

View File

@@ -1,626 +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>
package Bugzilla::CGI;
use strict;
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Util;
use Bugzilla::Search::Recent;
use File::Basename;
BEGIN {
if (ON_WINDOWS) {
# Help CGI find the correct temp directory as the default list
# isn't Windows friendly (Bug 248988)
$ENV{'TMPDIR'} = $ENV{'TEMP'} || $ENV{'TMP'} || "$ENV{'WINDIR'}\\TEMP";
}
}
use CGI qw(-no_xhtml -oldstyle_urls :private_tempfiles
:unique_headers SERVER_PUSH);
use base qw(CGI);
# 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.
my $script = basename($0);
if ($self->url_is_attachment_base and $script ne 'attachment.cgi') {
$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) = @_;
$self->convert_old_params();
# 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 grep { $_ eq $key } @exclude;
# 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 convert_old_params {
my $self = shift;
# bugidtype is now bug_id_type.
if ($self->param('bugidtype')) {
my $value = $self->param('bugidtype') eq 'exclude' ? 'nowords' : 'anyexact';
$self->param('bug_id_type', $value);
$self->delete('bugidtype');
}
}
sub clean_search_url {
my $self = shift;
# Delete any empty URL parameter.
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 leftovers from the login form
$self->delete('Bugzilla_remember', 'GoAheadAndLogIn');
foreach my $num (1,2,3) {
# 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->param('chfieldto')
&& lc($self->param('chfieldto')) eq 'now')
{
$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');
}
# list_id is added in buglist.cgi after calling clean_search_url,
# and doesn't need to be saved in saved searches.
$self->delete('list_id');
# 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'}
|| '------- =_' . generate_random_password(16);
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;
# If there's only one parameter, then it's a Content-Type.
if (scalar(@_) == 1) {
# Since we're adding parameters below, we have to name it.
unshift(@_, '-type' => shift(@_));
}
# Add the cookies in if we have any
if (scalar(@{$self->{Bugzilla_cookie_list}})) {
unshift(@_, '-cookie' => $self->{Bugzilla_cookie_list});
}
# Add Strict-Transport-Security (STS) header if this response
# is over SSL and the strict_transport_security param is turned on.
if ($self->https && !$self->url_is_attachment_base
&& Bugzilla->params->{'strict_transport_security'} ne 'off')
{
my $sts_opts = 'max-age=' . MAX_STS_AGE;
if (Bugzilla->params->{'strict_transport_security'}
eq 'include_subdomains')
{
$sts_opts .= '; includeSubDomains';
}
unshift(@_, '-strict_transport_security' => $sts_opts);
}
# Add X-Frame-Options header to prevent framing and subsequent
# possible clickjacking problems.
unless ($self->url_is_attachment_base) {
unshift(@_, '-x_frame_options' => 'SAMEORIGIN');
}
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')
{
# Some servers fail to set the QUERY_STRING parameter, which
# causes undef issues
$ENV{'QUERY_STRING'} = '' unless exists $ENV{'QUERY_STRING'};
@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 && !ref $input && !utf8::is_utf8($input);
return $input;
}
sub should_set {
my ($self, $param) = @_;
my $set = (defined $self->param($param)
or defined $self->param("defined_$param"))
? 1 : 0;
return $set;
}
# 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');
}
# This helps implement Bugzilla::Search::Recent, and also shortens search
# URLs that get POSTed to buglist.cgi.
sub redirect_search_url {
my $self = shift;
# If we're retreiving an old list, we never need to redirect or
# do anything related to Bugzilla::Search::Recent.
return if $self->param('regetlastlist');
my $user = Bugzilla->user;
if ($user->id) {
# There are two conditions that could happen here--we could get a URL
# with no list id, and we could get a URL with a list_id that isn't
# ours.
my $list_id = $self->param('list_id');
my $last_search;
if ($list_id) {
# If we have a valid list_id, no need to redirect or clean.
return if Bugzilla::Search::Recent->check_quietly(
{ id => $list_id });
}
}
elsif ($self->request_method ne 'POST') {
# Logged-out users who do a GET don't get a list_id, don't get
# their URLs cleaned, and don't get redirected.
return;
}
$self->clean_search_url();
# Make sure we have params still after cleaning otherwise we
# do not want to store a list_id for an empty search.
if ($user->id && $self->param) {
# Insert a placeholder Bugzilla::Search::Recent, so that we know what
# the id of the resulting search will be. This is then pulled out
# of the Referer header when viewing show_bug.cgi to know what
# bug list we came from.
my $recent_search = Bugzilla::Search::Recent->create_placeholder;
$self->param('list_id', $recent_search->id);
}
# GET requests that lacked a list_id are always redirected. POST requests
# are only redirected if they're under the CGI_URI_LIMIT though.
my $uri_length = length($self->self_url());
if ($self->request_method() ne 'POST' or $uri_length < CGI_URI_LIMIT) {
print $self->redirect(-url => $self->self_url());
exit;
}
}
sub redirect_to_https {
my $self = shift;
my $sslbase = Bugzilla->params->{'sslbase'};
# If this is a POST, we don't want ?POSTDATA in the query string.
# We expect the client to re-POST, which may be a violation of
# the HTTP spec, but the only time we're expecting it often is
# in the WebService, and WebService clients usually handle this
# correctly.
$self->delete('POSTDATA');
my $url = $sslbase . $self->url('-path_info' => 1, '-query' => 1,
'-relative' => 1);
# XML-RPC clients (SOAP::Lite at least) require a 301 to redirect properly
# and do not work with 302. Our redirect really is permanent anyhow, so
# it doesn't hurt to make it a 301.
print $self->redirect(-location => $url, -status => 301);
# When using XML-RPC with mod_perl, we need the headers sent immediately.
$self->r->rflush if $ENV{MOD_PERL};
exit;
}
# Redirect to the urlbase version of the current URL.
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;
}
sub url_is_attachment_base {
my ($self, $id) = @_;
return 0 if !use_attachbase() or !i_am_cgi();
my $attach_base = Bugzilla->params->{'attachment_base'};
# If we're passed an id, we only want one specific attachment base
# for a particular bug. If we're not passed an ID, we just want to
# know if our current URL matches the attachment_base *pattern*.
my $regex;
if ($id) {
$attach_base =~ s/\%bugid\%/$id/;
$regex = quotemeta($attach_base);
}
else {
# In this circumstance we run quotemeta first because we need to
# insert an active regex meta-character afterward.
$regex = quotemeta($attach_base);
$regex =~ s/\\\%bugid\\\%/\\d+/;
}
$regex = "^$regex";
return ($self->self_url =~ $regex) ? 1 : 0;
}
##########################
# Vars TIEHASH Interface #
##########################
# Fix the TIEHASH interface (scalar $cgi->Vars) to return and accept
# arrayrefs.
sub STORE {
my $self = shift;
my ($param, $value) = @_;
if (defined $value and ref $value eq 'ARRAY') {
return $self->param(-name => $param, -value => $value);
}
return $self->SUPER::STORE(@_);
}
sub FETCH {
my ($self, $param) = @_;
return $self if $param eq 'CGI'; # CGI.pm did this, so we do too.
my @result = $self->param($param);
return undef if !scalar(@result);
return $result[0] if scalar(@result) == 1;
return \@result;
}
# For the Vars TIEHASH interface: the normal CGI.pm DELETE doesn't return
# the value deleted, but Perl's "delete" expects that value.
sub DELETE {
my ($self, $param) = @_;
my $value = $self->FETCH($param);
$self->delete($param);
return $value;
}
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<redirect_to_https>
This routine redirects the client to the https version of the page that
they're looking at, using the C<sslbase> parameter for the redirection.
Generally you should use L<Bugzilla::Util/do_ssl_redirect_if_required>
instead of calling this directly.
=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,444 +0,0 @@
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Netscape Communications
# Corporation. Portions created by Netscape are
# Copyright (C) 1998 Netscape Communications Corporation. All
# Rights Reserved.
#
# Contributor(s): Gervase Markham <gerv@gerv.net>
# Albert Ting <altlst@sonic.net>
# A. Karl Kornel <karl@kornel.name>
use strict;
# 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+/+CONFIRMED&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' => scalar $cgi->param('datefrom'),
'dateto' => scalar $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 = ? OR (is_public = 1 AND cgm.category_id IS NULL) " .
$dbh->sql_group_by('series.series_id', 'cc1.name, cc2.name, ' .
'series.name'),
undef, Bugzilla->user->id);
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 html_quote(Data::Dumper::Dumper($self));
print "</pre>";
}
1;

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): Tiago R. Mello <timello@async.com.br>
# Frédéric Buclin <LpSolit@gmail.com>
use strict;
package Bugzilla::Classification;
use Bugzilla::Constants;
use Bugzilla::Field;
use Bugzilla::Util;
use Bugzilla::Error;
use Bugzilla::Product;
use base qw(Bugzilla::Field::ChoiceInterface 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 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);
$self->SUPER::remove_from_db();
$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;
}
#####################################
# Implement Bugzilla::Field::Choice #
#####################################
use constant FIELD_NAME => 'classification';
use constant is_default => 0;
use constant is_active => 1;
###############################
#### 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,316 +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 James Robson.
# Portions created by James Robson are Copyright (c) 2009 James Robson.
# All rights reserved.
#
# Contributor(s): James Robson <arbingersys@gmail.com>
use strict;
package Bugzilla::Comment;
use base qw(Bugzilla::Object);
use Bugzilla::Attachment;
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::User;
use Bugzilla::Util;
use Scalar::Util qw(blessed);
###############################
#### Initialization ####
###############################
use constant DB_COLUMNS => qw(
comment_id
bug_id
who
bug_when
work_time
thetext
isprivate
already_wrapped
type
extra_data
);
use constant UPDATE_COLUMNS => qw(
type
extra_data
);
use constant DB_TABLE => 'longdescs';
use constant ID_FIELD => 'comment_id';
use constant LIST_ORDER => 'bug_when';
use constant VALIDATORS => {
extra_data => \&_check_extra_data,
type => \&_check_type,
};
use constant VALIDATOR_DEPENDENCIES => {
extra_data => ['type'],
};
#########################
# Database Manipulation #
#########################
sub update {
my $self = shift;
my $changes = $self->SUPER::update(@_);
$self->bug->_sync_fulltext();
return $changes;
}
# Speeds up displays of comment lists by loading all ->author objects
# at once for a whole list.
sub preload {
my ($class, $comments) = @_;
my %user_ids = map { $_->{who} => 1 } @$comments;
my $users = Bugzilla::User->new_from_list([keys %user_ids]);
my %user_map = map { $_->id => $_ } @$users;
foreach my $comment (@$comments) {
$comment->{author} = $user_map{$comment->{who}};
}
}
###############################
#### Accessors ######
###############################
sub already_wrapped { return $_[0]->{'already_wrapped'}; }
sub body { return $_[0]->{'thetext'}; }
sub bug_id { return $_[0]->{'bug_id'}; }
sub creation_ts { return $_[0]->{'bug_when'}; }
sub is_private { return $_[0]->{'isprivate'}; }
sub work_time { return $_[0]->{'work_time'}; }
sub type { return $_[0]->{'type'}; }
sub extra_data { return $_[0]->{'extra_data'} }
sub bug {
my $self = shift;
require Bugzilla::Bug;
$self->{bug} ||= new Bugzilla::Bug($self->bug_id);
return $self->{bug};
}
sub is_about_attachment {
my ($self) = @_;
return 1 if ($self->type == CMT_ATTACHMENT_CREATED
or $self->type == CMT_ATTACHMENT_UPDATED);
return 0;
}
sub attachment {
my ($self) = @_;
return undef if not $self->is_about_attachment;
$self->{attachment} ||= new Bugzilla::Attachment($self->extra_data);
return $self->{attachment};
}
sub author {
my $self = shift;
$self->{'author'} ||= new Bugzilla::User($self->{'who'});
return $self->{'author'};
}
sub body_full {
my ($self, $params) = @_;
$params ||= {};
my $template = Bugzilla->template_inner;
my $body;
if ($self->type) {
$template->process("bug/format_comment.txt.tmpl",
{ comment => $self, %$params }, \$body)
|| ThrowTemplateError($template->error());
$body =~ s/^X//;
}
else {
$body = $self->body;
}
if ($params->{wrap} and !$self->already_wrapped) {
$body = wrap_comment($body);
}
return $body;
}
############
# Mutators #
############
sub set_extra_data { $_[0]->set('extra_data', $_[1]); }
sub set_type {
my ($self, $type) = @_;
$self->set('type', $type);
}
##############
# Validators #
##############
sub _check_extra_data {
my ($invocant, $extra_data, undef, $params) = @_;
my $type = blessed($invocant) ? $invocant->type : $params->{type};
if ($type == CMT_NORMAL) {
if (defined $extra_data) {
ThrowCodeError('comment_extra_data_not_allowed',
{ type => $type, extra_data => $extra_data });
}
}
else {
if (!defined $extra_data) {
ThrowCodeError('comment_extra_data_required', { type => $type });
}
elsif ($type == CMT_ATTACHMENT_CREATED
or $type == CMT_ATTACHMENT_UPDATED)
{
my $attachment = Bugzilla::Attachment->check({
id => $extra_data });
$extra_data = $attachment->id;
}
else {
my $original = $extra_data;
detaint_natural($extra_data)
or ThrowCodeError('comment_extra_data_not_numeric',
{ type => $type, extra_data => $original });
}
}
return $extra_data;
}
sub _check_type {
my ($invocant, $type) = @_;
$type ||= CMT_NORMAL;
my $original = $type;
detaint_natural($type)
or ThrowCodeError('comment_type_invalid', { type => $original });
return $type;
}
sub count {
my ($self) = @_;
return $self->{'count'} if defined $self->{'count'};
my $dbh = Bugzilla->dbh;
($self->{'count'}) = $dbh->selectrow_array(
"SELECT COUNT(*)
FROM longdescs
WHERE bug_id = ?
AND bug_when <= ?",
undef, $self->bug_id, $self->creation_ts);
return --$self->{'count'};
}
1;
__END__
=head1 NAME
Bugzilla::Comment - A Comment for a given bug
=head1 SYNOPSIS
use Bugzilla::Comment;
my $comment = Bugzilla::Comment->new($comment_id);
my $comments = Bugzilla::Comment->new_from_list($comment_ids);
=head1 DESCRIPTION
Bugzilla::Comment represents a comment attached to a bug.
This implements all standard C<Bugzilla::Object> methods. See
L<Bugzilla::Object> for more details.
=head2 Accessors
=over
=item C<bug_id>
C<int> The ID of the bug to which the comment belongs.
=item C<creation_ts>
C<string> The comment creation timestamp.
=item C<body>
C<string> The body without any special additional text.
=item C<work_time>
C<string> Time spent as related to this comment.
=item C<is_private>
C<boolean> Comment is marked as private
=item C<already_wrapped>
If this comment is stored in the database word-wrapped, this will be C<1>.
C<0> otherwise.
=item C<author>
L<Bugzilla::User> who created the comment.
=item C<count>
C<int> The position this comment is located in the full list of comments for a bug starting from 0.
=item C<body_full>
=over
=item B<Description>
C<string> Body of the comment, including any special text (such as
"this bug was marked as a duplicate of...").
=item B<Params>
=over
=item C<is_bugmail>
C<boolean>. C<1> if this comment should be formatted specifically for
bugmail.
=item C<wrap>
C<boolean>. C<1> if the comment should be returned word-wrapped.
=back
=item B<Returns>
A string, the full text of the comment as it would be displayed to an end-user.
=back
=back
=cut

View File

@@ -1,657 +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>
package Bugzilla::Component;
use strict;
use base qw(Bugzilla::Field::ChoiceInterface Bugzilla::Object);
use Bugzilla::Constants;
use Bugzilla::Util;
use Bugzilla::Error;
use Bugzilla::User;
use Bugzilla::FlagType;
use Bugzilla::Series;
use Scalar::Util qw(blessed);
###############################
#### Initialization ####
###############################
use constant DB_TABLE => 'components';
# This is mostly for the editfields.cgi case where ->get_all is called.
use constant LIST_ORDER => 'product_id, name';
use constant DB_COLUMNS => qw(
id
name
product_id
initialowner
initialqacontact
description
);
use constant UPDATE_COLUMNS => qw(
name
initialowner
initialqacontact
description
);
use constant REQUIRED_FIELD_MAP => {
product_id => 'product',
};
use constant VALIDATORS => {
create_series => \&Bugzilla::Object::check_boolean,
product => \&_check_product,
initialowner => \&_check_initialowner,
initialqacontact => \&_check_initialqacontact,
description => \&_check_description,
initial_cc => \&_check_cc_list,
name => \&_check_name,
};
use constant VALIDATOR_DEPENDENCIES => {
name => ['product'],
};
###############################
sub new {
my $class = shift;
my $param = shift;
my $dbh = Bugzilla->dbh;
my $product;
if (ref $param and !defined $param->{id}) {
$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 $create_series = delete $params->{create_series};
my $product = delete $params->{product};
$params->{product_id} = $product->id;
my $component = $class->insert_create_data($params);
$component->{product} = $product;
# We still have to fill the component_cc table.
$component->_update_cc_list($cc_list) if $cc_list;
# Create series for the new component.
$component->_create_series() if $create_series;
$dbh->bz_commit_transaction();
return $component;
}
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;
$self->_check_if_controller(); # From ChoiceInterface
$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, undef, $params) = @_;
my $product = blessed($invocant) ? $invocant->product : $params->{product};
$name = trim($name);
$name || ThrowUserError('component_blank_name');
if (length($name) > MAX_COMPONENT_SIZE) {
ThrowUserError('component_name_too_long', {'name' => $name});
}
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) = @_;
$product || ThrowCodeError('param_required',
{ function => "$invocant->create", param => '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'}) {
my $flagtypes = Bugzilla::FlagType::match({ product_id => $self->product_id,
component_id => $self->id });
$self->{'flag_types'} = {};
$self->{'flag_types'}->{'bug'} =
[grep { $_->target_type eq 'bug' } @$flagtypes];
$self->{'flag_types'}->{'attachment'} =
[grep { $_->target_type eq 'attachment' } @$flagtypes];
}
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 description { return $_[0]->{'description'}; }
sub product_id { return $_[0]->{'product_id'}; }
##############################################
# Implement Bugzilla::Field::ChoiceInterface #
##############################################
use constant FIELD_NAME => 'component';
use constant is_default => 0;
use constant is_active => 1;
sub is_set_on_bug {
my ($self, $bug) = @_;
# We treat it like a hash always, so that we don't have to check if it's
# a hash or an object.
return 0 if !defined $bug->{component_id};
$bug->{component_id} == $self->id ? 1 : 0;
}
###############################
#### 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,408 +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 Bugzilla::Install::Filesystem qw(fix_file_permissions);
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'};
}
# 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};
}
# Convert the old "ssl" parameter to the new "ssl_redirect" parameter.
# Both "authenticated sessions" and "always" turn on "ssl_redirect"
# when upgrading.
if (exists $param->{'ssl'} and $param->{'ssl'} ne 'never') {
$param->{'ssl_redirect'} = 1;
}
# --- DEFAULTS FOR NEW PARAMS ---
_load_params unless %params;
foreach my $item (@param_list) {
my $name = $item->{'name'};
unless (exists $param->{$name}) {
print "New parameter: $name\n" unless $new_install;
if (exists $answer->{$name}) {
$param->{$name} = $answer->{$name};
}
else {
$param->{$name} = $item->{'default'};
}
}
}
$param->{'utf8'} = 1 if $new_install;
# --- REMOVE OLD PARAMS ---
my %oldparams;
# Remove any old params
foreach my $item (keys %$param) {
if (!grep($_ eq $item, map ($_->{'name'}, @param_list))) {
$oldparams{$item} = $param->{$item};
delete $param->{$item};
}
}
# Write any old parameters to old-params.txt
my $datadir = bz_locations()->{'datadir'};
my $old_param_file = "$datadir/old-params.txt";
if (scalar(keys %oldparams)) {
my $op_file = new IO::File($old_param_file, '>>', 0600)
|| die "Couldn't create $old_param_file: $!";
print "The following parameters are no longer used in Bugzilla,",
" and so have been\nmoved from your parameters file into",
" $old_param_file:\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: $!";
fix_file_permissions($param_file);
# And now we have to reset the params cache so that Bugzilla will re-read
# them.
delete Bugzilla->request_cache->{params};
}
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;
our $sortkey = 200;
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,68 +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>
# Max Kanat-Alexander <mkanat@bugzilla.org>
package Bugzilla::Config::Advanced;
use strict;
use Bugzilla::Config::Common;
our $sortkey = 1700;
use constant get_param_list => (
{
name => 'cookiedomain',
type => 't',
default => ''
},
{
name => 'inbound_proxies',
type => 't',
default => '',
checker => \&check_ip
},
{
name => 'proxy_url',
type => 't',
default => ''
},
{
name => 'strict_transport_security',
type => 's',
choices => ['off', 'this_domain_only', 'include_subdomains'],
default => 'off',
checker => \&check_multi
},
);
1;

View File

@@ -1,91 +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;
our $sortkey = 400;
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
} );
return @param_list;
}
1;

View File

@@ -1,128 +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;
our $sortkey = 300;
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 => '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;
our $sortkey = 500;
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,120 +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;
our $sortkey = 600;
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 => '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,470 +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 Email::Address;
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_user_verify_class check_ip
check_mail_delivery_method check_notification check_utf8
check_bug_status check_smtp_auth check_theschwartz_available
check_maxattachmentsize check_email
);
# 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_email {
my ($value) = @_;
if ($value !~ $Email::Address::mailbox) {
return "must be a valid email address.";
}
return "";
}
sub check_sslbase {
my $url = shift;
if ($url ne '') {
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_ip {
my $inbound_proxies = shift;
my @proxies = split(/[\s,]+/, $inbound_proxies);
foreach my $proxy (@proxies) {
validate_ip($proxy) || return "$proxy is not a valid IPv4 or IPv6 address";
}
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 (!grep($_ eq $value, @$legal_priorities)) {
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 (!grep($_ eq $value, @$legal_severities)) {
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 (!grep($_ eq $value, '', @$legal_platforms)) {
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 (!grep($_ eq $value, '', @$legal_OS)) {
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 (!grep($_ eq $bug_status, @closed_bug_statuses)) {
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_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 $params = Bugzilla->params;
my ($list, $entry) = @_;
$list || return 'You need to specify at least one authentication mechanism';
for my $class (split /,\s*/, $list) {
my $res = check_multi($class, $entry);
return $res if $res;
if ($class eq 'RADIUS') {
if (!Bugzilla->feature('auth_radius')) {
return "RADIUS support is not available. Run checksetup.pl"
. " for more details";
}
return "RADIUS servername (RADIUS_server) is missing"
if !$params->{"RADIUS_server"};
return "RADIUS_secret is empty" if !$params->{"RADIUS_secret"};
}
elsif ($class eq 'LDAP') {
if (!Bugzilla->feature('auth_ldap')) {
return "LDAP support is not available. Run checksetup.pl"
. " for more details";
}
return "LDAP servername (LDAPserver) is missing"
if !$params->{"LDAPserver"};
return "LDAPBaseDN is empty" if !$params->{"LDAPBaseDN"};
}
}
return "";
}
sub check_mail_delivery_method {
my $check = check_multi(@_);
return $check if $check;
my $mailer = shift;
if ($mailer eq 'sendmail' and ON_WINDOWS) {
# 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";
}
if ($option ne 'disabled' && !Bugzilla->feature('updates')) {
return "Some Perl modules are missing to get notifications about " .
"new releases. See the output of checksetup.pl for more information";
}
return "";
}
sub check_smtp_auth {
my $username = shift;
if ($username and !Bugzilla->feature('smtp_auth')) {
return "SMTP Authentication is not available. Run checksetup.pl for"
. " more details";
}
return "";
}
sub check_theschwartz_available {
my $use_queue = shift;
if ($use_queue && !Bugzilla->feature('jobqueue')) {
return "Using the job queue requires that you have certain Perl"
. " modules installed. See the output of checksetup.pl"
. " for more information";
}
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: the value to
check, and a hash with the details of the param (type, default etc.) as defined
in the relevant F<Bugzilla::Config::*> package.
=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,68 +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;
our $sortkey = 100;
use constant get_param_list => (
{
name => 'urlbase',
type => 't',
default => '',
checker => \&check_urlbase
},
{
name => 'ssl_redirect',
type => 'b',
default => 0
},
{
name => 'sslbase',
type => 't',
default => '',
checker => \&check_sslbase
},
{
name => 'cookiepath',
type => 't',
default => '/'
},
);
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;
our $sortkey = 800;
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,83 +0,0 @@
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Netscape Communications
# Corporation. Portions created by Netscape are
# Copyright (C) 1998 Netscape Communications Corporation. All
# Rights Reserved.
#
# Contributor(s): Terry Weissman <terry@mozilla.org>
# Dawn Endico <endico@mozilla.org>
# Dan Mosedale <dmose@mozilla.org>
# Joe Robins <jmrobins@tgix.com>
# Jacob Steenhagen <jake@bugzilla.org>
# J. Paul Reed <preed@sigkill.com>
# Bradley Baetz <bbaetz@student.usyd.edu.au>
# Joseph Heenan <joseph@heenan.me.uk>
# Erik Stambaugh <erik@dasbistro.com>
# Frédéric Buclin <LpSolit@gmail.com>
# Max Kanat-Alexander <mkanat@bugzilla.org>
package Bugzilla::Config::General;
use strict;
use Bugzilla::Config::Common;
our $sortkey = 150;
use constant get_param_list => (
{
name => 'maintainer',
type => 't',
no_reset => '1',
default => '',
checker => \&check_email
},
{
name => 'docs_urlbase',
type => 't',
default => 'docs/%lang%/html/',
checker => \&check_url
},
{
name => 'utf8',
type => 'b',
default => '0',
checker => \&check_utf8
},
{
name => 'shutdownhtml',
type => 'l',
default => ''
},
{
name => 'announcehtml',
type => 'l',
default => ''
},
{
name => 'upgrade_notification',
type => 's',
choices => ['development_snapshot', 'latest_stable_release',
'stable_branch_release', 'disabled'],
default => 'latest_stable_release',
checker => \&check_notification
},
);
1;

View File

@@ -1,110 +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;
our $sortkey = 900;
sub get_param_list {
my $class = shift;
my @param_list = (
{
name => 'makeproductgroups',
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 => 'debug_group',
type => 's',
choices => \&_get_all_group_names,
default => 'admin',
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;
our $sortkey = 1000;
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;
# Return::Value 1.666002 pollutes the error log with warnings about this
# deprecated module. We have to set NO_CLUCK = 1 before loading Email::Send
# to disable these warnings.
BEGIN {
$Return::Value::NO_CLUCK = 1;
}
use Email::Send;
our $sortkey = 1200;
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 => '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;
our $sortkey = 1300;
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,80 +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;
our $sortkey = 1400;
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?resolution=---&amp;emailassigned_to1=1&amp;emailreporter1=1&amp;emailtype1=exact&amp;email1=%userid%'
},
{
name => 'defaultquery',
type => 't',
default => 'resolution=---&emailassigned_to1=1&emailassigned_to2=1&emailreporter2=1&emailcc2=1&emailqa_contact2=1&emaillongdesc3=1&order=Importance&long_desc_type=substring'
},
{
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;
our $sortkey = 1100;
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;
our $sortkey = 1500;
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,70 +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;
our $sortkey = 1600;
sub get_param_list {
my $class = shift;
my @param_list = (
{
name => 'usemenuforusers',
type => 'b',
default => '0'
},
{
name => 'ajax_user_autocompletion',
type => 'b',
default => '1',
},
{
name => 'maxusermatches',
type => 't',
default => '1000',
checker => \&check_numeric
},
{
name => 'confirmuniqueusermatch',
type => 'b',
default => 1,
} );
return @param_list;
}
1;

View File

@@ -1,624 +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
AUTH_LOCKOUT
USER_PASSWORD_MIN_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
DEFAULT_MILESTONE
QUERY_LIST
LIST_OF_BUGS
SAVE_NUM_SEARCHES
COMMENT_COLS
MAX_COMMENT_LENGTH
CMT_NORMAL
CMT_DUPE_OF
CMT_HAS_DUPE
CMT_ATTACHMENT_CREATED
CMT_ATTACHMENT_UPDATED
THROW_ERROR
RELATIONSHIPS
REL_ASSIGNEE REL_QA REL_REPORTER REL_CC 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
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
FIELD_TYPE_KEYWORDS
EMPTY_DATETIME_REGEX
ABNORMAL_SELECTS
TIMETRACKING_FIELDS
USAGE_MODE_BROWSER
USAGE_MODE_CMDLINE
USAGE_MODE_XMLRPC
USAGE_MODE_EMAIL
USAGE_MODE_JSON
USAGE_MODE_TEST
ERROR_MODE_WEBPAGE
ERROR_MODE_DIE
ERROR_MODE_DIE_SOAP_FAULT
ERROR_MODE_JSON_RPC
ERROR_MODE_TEST
COLOR_ERROR
INSTALLATION_MODE_INTERACTIVE
INSTALLATION_MODE_NON_INTERACTIVE
DB_MODULE
ROOT_USER
ON_WINDOWS
ON_ACTIVESTATE
MAX_TOKEN_AGE
MAX_LOGINCOOKIE_AGE
MAX_SUDO_TOKEN_AGE
MAX_LOGIN_ATTEMPTS
LOGIN_LOCKOUT_INTERVAL
MAX_STS_AGE
SAFE_PROTOCOLS
LEGAL_CONTENT_TYPES
MIN_SMALLINT
MAX_SMALLINT
MAX_INT_32
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
MAX_POSSIBLE_DUPLICATES
PASSWORD_DIGEST_ALGORITHM
PASSWORD_SALT_LENGTH
CGI_URI_LIMIT
PRIVILEGES_REQUIRED_NONE
PRIVILEGES_REQUIRED_REPORTER
PRIVILEGES_REQUIRED_ASSIGNEE
PRIVILEGES_REQUIRED_EMPOWERED
);
@Bugzilla::Constants::EXPORT_OK = qw(contenttypes);
# CONSTANTS
#
# Bugzilla version
use constant BUGZILLA_VERSION => "4.0.18";
# 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;
use constant AUTH_LOCKOUT => 6;
# The minimum length a password must have.
use constant USER_PASSWORD_MIN_LENGTH => 6;
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 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 default "defaultmilestone" created for products.
use constant DEFAULT_MILESTONE => '---';
# The possible types for saved searches.
use constant QUERY_LIST => 0;
use constant LIST_OF_BUGS => 1;
# How many of the user's most recent searches to save.
use constant SAVE_NUM_SEARCHES => 10;
# 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;
# Type 3 was CMT_POPULAR_VOTES, which moved to the Voting extension.
# Type 4 was CMT_MOVED_TO, which moved to the OldBugMove extension.
use constant CMT_ATTACHMENT_CREATED => 5;
use constant CMT_ATTACHMENT_UPDATED => 6;
# Determine whether a validation routine should return 0 or throw
# an error when the validation fails.
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;
# REL 4 was REL_VOTER, before it was moved ino an extension.
use constant REL_GLOBAL_WATCHER => 5;
# We need these strings for the X-Bugzilla-Reasons header
# Note: this hash uses "," rather than "=>" to avoid auto-quoting of the LHS.
# This should be accessed through Bugzilla::BugMail::relationships() instead
# of being accessed directly.
use constant RELATIONSHIPS => {
REL_ASSIGNEE , "AssignedTo",
REL_REPORTER , "Reporter",
REL_QA , "QAcontact",
REL_CC , "CC",
REL_GLOBAL_WATCHER, "GlobalWatcher"
};
# 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;
# 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;
use constant FIELD_TYPE_KEYWORDS => 8;
use constant EMPTY_DATETIME_REGEX => qr/^[0\-:\sA-Za-z]+$/;
# See the POD for Bugzilla::Field/is_abnormal to see why these are listed
# here.
use constant ABNORMAL_SELECTS => qw(
classification
product
component
);
# The fields from fielddefs that are blocked from non-timetracking users.
# work_time is sometimes called actual_time.
use constant TIMETRACKING_FIELDS =>
qw(estimated_time remaining_time work_time actual_time
percentage_complete deadline);
# The maximum number of days a token will remain valid.
use constant MAX_TOKEN_AGE => 3;
# How many days a logincookie will remain valid if not used.
use constant MAX_LOGINCOOKIE_AGE => 30;
# How many seconds (default is 6 hours) a sudo cookie remains valid.
use constant MAX_SUDO_TOKEN_AGE => 21600;
# Maximum failed logins to lock account for this IP
use constant MAX_LOGIN_ATTEMPTS => 5;
# If the maximum login attempts occur during this many minutes, the
# account is locked.
use constant LOGIN_LOCKOUT_INTERVAL => 30;
# The maximum number of seconds the Strict-Transport-Security header
# will remain valid. Default is one week.
use constant MAX_STS_AGE => 604800;
# Protocols which are considered as safe.
use constant SAFE_PROTOCOLS => ('afs', 'cid', 'ftp', 'gopher', 'http', 'https',
'irc', 'ircs', 'mid', 'news', 'nntp', 'prospero',
'telnet', 'view-source', 'wais');
# Valid MIME types for attachments.
use constant LEGAL_CONTENT_TYPES => ('application', 'audio', 'image', 'message',
'model', 'multipart', 'text', 'video');
use constant contenttypes =>
{
"html"=> "text/html" ,
"rdf" => "application/rdf+xml" ,
"atom"=> "application/atom+xml" ,
"xml" => "application/xml" ,
"js" => "application/x-javascript" ,
"json"=> "application/json" ,
"csv" => "text/csv" ,
"png" => "image/png" ,
"ics" => "text/calendar" ,
};
# Usage modes. Default USAGE_MODE_BROWSER. Use with Bugzilla->usage_mode.
use constant USAGE_MODE_BROWSER => 0;
use constant USAGE_MODE_CMDLINE => 1;
use constant USAGE_MODE_XMLRPC => 2;
use constant USAGE_MODE_EMAIL => 3;
use constant USAGE_MODE_JSON => 4;
use constant USAGE_MODE_TEST => 5;
# Error modes. Default set by Bugzilla->usage_mode (so ERROR_MODE_WEBPAGE
# usually). Use with Bugzilla->error_mode.
use constant ERROR_MODE_WEBPAGE => 0;
use constant ERROR_MODE_DIE => 1;
use constant ERROR_MODE_DIE_SOAP_FAULT => 2;
use constant ERROR_MODE_JSON_RPC => 3;
use constant ERROR_MODE_TEST => 4;
# The ANSI colors of messages that command-line scripts use
use constant COLOR_ERROR => 'red';
# 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'},
# Also see Bugzilla::DB::Pg::bz_check_server_version, which has special
# code to require DBD::Pg 2.17.2 for PostgreSQL 9 and above.
'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'},
};
# True if we're on Win32.
use constant ON_WINDOWS => ($^O =~ /MSWin32/i);
# True if we're using ActiveState Perl (as opposed to Strawberry) on Windows.
use constant ON_ACTIVESTATE => eval { &Win32::BuildNumber };
# The user who should be considered "root" when we're giving
# instructions to Bugzilla administrators.
use constant ROOT_USER => ON_WINDOWS ? 'Administrator' : 'root';
use constant MIN_SMALLINT => -32768;
use constant MAX_SMALLINT => 32767;
use constant MAX_INT_32 => 2147483647;
# 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;
# The largest number of possible duplicates that Bug::possible_duplicates
# will return.
use constant MAX_POSSIBLE_DUPLICATES => 25;
# 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;
# Certain scripts redirect to GET even if the form was submitted originally
# via POST such as buglist.cgi. This value determines whether the redirect
# can be safely done or not based on the web server's URI length setting.
use constant CGI_URI_LIMIT => 8000;
# If the user isn't allowed to change a field, we must tell him who can.
# We store the required permission set into the $PrivilegesRequired
# variable which gets passed to the error template.
use constant PRIVILEGES_REQUIRED_NONE => 0;
use constant PRIVILEGES_REQUIRED_REPORTER => 1;
use constant PRIVILEGES_REQUIRED_ASSIGNEE => 2;
use constant PRIVILEGES_REQUIRED_EMPOWERED => 3;
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",
'graphsdir' => "$libpath/graphs",
# $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,750 +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 base qw(Bugzilla::DB);
use DBD::Oracle;
use DBD::Oracle qw(:ora_types);
use List::Util qw(max);
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Util;
#####################################################################
# Constants
#####################################################################
use constant EMPTY_STRING => '__BZ_EMPTY_STR__';
use constant ISOLATION_LEVEL => 'READ COMMITTED';
use constant BLOB_TYPE => { ora_type => ORA_BLOB };
# The max size allowed for LOB fields, in kilobytes.
use constant MIN_LONG_READ_LEN => 32 * 1024;
use constant FULLTEXT_OR => ' OR ';
sub new {
my ($class, $params) = @_;
my ($user, $pass, $host, $dbname, $port) =
@$params{qw(db_user db_pass db_host db_name db_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 => max(Bugzilla->params->{'maxattachmentsize'},
MIN_LONG_READ_LEN) * 1024,
};
my $self = $class->db_new({ dsn => $dsn, user => $user,
pass => $pass, attrs => $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_group_concat {
my ($self, $text, $separator) = @_;
$separator = $self->quote(', ') if !defined $separator;
return "group_concat(T_CLOB_DELIM($text, $separator))";
}
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_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) > 0", "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) = @_;
if ($sql =~ /^CREATE OR REPLACE.*/i){
return $sql;
}
# We can't just assume any occurrence of "''" in $sql is an empty
# string, since "''" can occur inside a string literal as a way of
# 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;
# Take the first 4000 chars for comparison
$nonstring =~ s/\(\s*(longdescs_\d+\.thetext|attachdata_\d+\.thedata)/
\(DBMS_LOB.SUBSTR\($1, 4000, 1\)/ig;
# Look for a LIMIT clause
($limit) = ($nonstring =~ m(/\* LIMIT (\d*) \*/)o);
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, 2);
if (defined($offset)) {
my ($before_from, $after_from) = split(/\bFROM\b/i, $new_sql, 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 types for group_concat
my $t_clob_delim = $self->selectcol_arrayref("
SELECT TYPE_NAME FROM USER_TYPES WHERE TYPE_NAME=?",
undef, 'T_CLOB_DELIM');
if ( !@$t_clob_delim ) {
$self->do("CREATE OR REPLACE TYPE T_CLOB_DELIM AS OBJECT "
. "( p_CONTENT CLOB, p_DELIMITER VARCHAR2(256));");
}
$self->do("CREATE OR REPLACE TYPE T_GROUP_CONCAT AS OBJECT
( CLOB_CONTENT CLOB,
DELIMITER VARCHAR2(256),
STATIC FUNCTION ODCIAGGREGATEINITIALIZE(
SCTX IN OUT NOCOPY T_GROUP_CONCAT)
RETURN NUMBER,
MEMBER FUNCTION ODCIAGGREGATEITERATE(
SELF IN OUT NOCOPY T_GROUP_CONCAT,
VALUE IN T_CLOB_DELIM)
RETURN NUMBER,
MEMBER FUNCTION ODCIAGGREGATETERMINATE(
SELF IN T_GROUP_CONCAT,
RETURNVALUE OUT NOCOPY CLOB,
FLAGS IN NUMBER)
RETURN NUMBER,
MEMBER FUNCTION ODCIAGGREGATEMERGE(
SELF IN OUT NOCOPY T_GROUP_CONCAT,
CTX2 IN T_GROUP_CONCAT)
RETURN NUMBER);");
$self->do("CREATE OR REPLACE TYPE BODY T_GROUP_CONCAT IS
STATIC FUNCTION ODCIAGGREGATEINITIALIZE(
SCTX IN OUT NOCOPY T_GROUP_CONCAT)
RETURN NUMBER IS
BEGIN
SCTX := T_GROUP_CONCAT(EMPTY_CLOB(), NULL);
DBMS_LOB.CREATETEMPORARY(SCTX.CLOB_CONTENT, TRUE);
RETURN ODCICONST.SUCCESS;
END;
MEMBER FUNCTION ODCIAGGREGATEITERATE(
SELF IN OUT NOCOPY T_GROUP_CONCAT,
VALUE IN T_CLOB_DELIM)
RETURN NUMBER IS
BEGIN
SELF.DELIMITER := VALUE.P_DELIMITER;
DBMS_LOB.WRITEAPPEND(SELF.CLOB_CONTENT,
LENGTH(SELF.DELIMITER),
SELF.DELIMITER);
DBMS_LOB.APPEND(SELF.CLOB_CONTENT, VALUE.P_CONTENT);
RETURN ODCICONST.SUCCESS;
END;
MEMBER FUNCTION ODCIAGGREGATETERMINATE(
SELF IN T_GROUP_CONCAT,
RETURNVALUE OUT NOCOPY CLOB,
FLAGS IN NUMBER)
RETURN NUMBER IS
BEGIN
RETURNVALUE := RTRIM(LTRIM(SELF.CLOB_CONTENT,
SELF.DELIMITER),
SELF.DELIMITER);
RETURN ODCICONST.SUCCESS;
END;
MEMBER FUNCTION ODCIAGGREGATEMERGE(
SELF IN OUT NOCOPY T_GROUP_CONCAT,
CTX2 IN T_GROUP_CONCAT)
RETURN NUMBER IS
BEGIN
DBMS_LOB.WRITEAPPEND(SELF.CLOB_CONTENT,
LENGTH(SELF.DELIMITER),
SELF.DELIMITER);
DBMS_LOB.APPEND(SELF.CLOB_CONTENT, CTX2.CLOB_CONTENT);
RETURN ODCICONST.SUCCESS;
END;
END;");
# Create user-defined aggregate function group_concat
$self->do("CREATE OR REPLACE FUNCTION GROUP_CONCAT(P_INPUT T_CLOB_DELIM)
RETURN CLOB
DETERMINISTIC PARALLEL_ENABLE AGGREGATE USING T_GROUP_CONCAT;");
# Create a WORLD_LEXER named BZ_LEX for multilingual fulltext search
my $lexer = $self->selectcol_arrayref(
"SELECT pre_name FROM CTXSYS.CTX_PREFERENCES WHERE pre_name = ? AND
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);
}
}
}
}
# Drop the trigger which causes bug 541553
my $trigger_name = "PRODUCTS_MILESTONEURL";
my $exist_trigger = $self->selectcol_arrayref(
"SELECT OBJECT_NAME FROM USER_OBJECTS
WHERE OBJECT_NAME = ?", undef, $trigger_name);
if(@$exist_trigger) {
$self->do("DROP TRIGGER $trigger_name");
}
}
package Bugzilla::DB::Oracle::st;
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,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 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, $params) = @_;
my ($user, $pass, $host, $dbname, $port) =
@$params{qw(db_user db_pass db_host db_name db_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 => $dsn, user => $user,
pass => $pass, attrs => $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_group_concat {
my ($self, $text, $separator, $sort) = @_;
$sort = 1 if !defined $sort;
$separator = $self->quote(', ') if !defined $separator;
my $sql = "array_accum($text)";
if ($sort) {
$sql = "array_sort($sql)";
}
return "array_to_string($sql, $separator)";
}
sub sql_istring {
my ($self, $string) = @_;
return "LOWER(${string}::text)";
}
sub sql_position {
my ($self, $fragment, $text) = @_;
return "POSITION($fragment IN ${text}::text)";
}
sub sql_regexp {
my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_;
$real_pattern ||= $pattern;
$self->bz_check_regexp($real_pattern) if !$nocheck;
return "${expr}::text ~* $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}::text !~* $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', '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_check_server_version {
my $self = shift;
my ($db) = @_;
my $server_version = $self->SUPER::bz_check_server_version(@_);
my ($major_version) = $server_version =~ /^(\d+)/;
# Pg 9 requires DBD::Pg 2.17.2 in order to properly read bytea values.
if ($major_version >= 9) {
local $db->{dbd}->{version} = '2.17.2';
local $db->{name} = $db->{name} . ' 9+';
Bugzilla::DB::_bz_check_dbd(@_);
}
}
sub bz_setup_database {
my $self = shift;
$self->SUPER::bz_setup_database(@_);
# Custom Functions
my $function = 'array_accum';
my $array_accum = $self->selectrow_array(
'SELECT 1 FROM pg_proc WHERE proname = ?', undef, $function);
if (!$array_accum) {
print "Creating function $function...\n";
$self->do("CREATE AGGREGATE array_accum (
SFUNC = array_append,
BASETYPE = anyelement,
STYPE = anyarray,
INITCOND = '{}'
)");
}
$self->do(<<'END');
CREATE OR REPLACE FUNCTION array_sort(ANYARRAY)
RETURNS ANYARRAY LANGUAGE SQL
IMMUTABLE STRICT
AS $$
SELECT ARRAY(
SELECT $1[s.i] AS each_item
FROM
generate_series(array_lower($1,1), array_upper($1,1)) AS s(i)
ORDER BY each_item
);
$$;
END
# 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,399 +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.
#
# THIS CONSTANT IS ONLY USED FOR UPGRADES FROM 2.18 OR EARLIER. DON'T
# UPDATE IT TO MODERN COLUMN NAMES OR DEFINITIONS.
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 @statements;
push(@statements, "UPDATE $table SET $column = $set_nulls_to
WHERE $column IS NULL") if defined $set_nulls_to;
# Calling SET DEFAULT or DROP DEFAULT is *way* faster than calling
# CHANGE COLUMN, so just do that if we're just changing the default.
my %old_defaultless = %$old_def;
my %new_defaultless = %$new_def;
delete $old_defaultless{DEFAULT};
delete $new_defaultless{DEFAULT};
if (!$self->columns_equal($old_def, $new_def)
&& $self->columns_equal(\%new_defaultless, \%old_defaultless))
{
if (!defined $new_def->{DEFAULT}) {
push(@statements,
"ALTER TABLE $table ALTER COLUMN $column DROP DEFAULT");
}
else {
push(@statements, "ALTER TABLE $table ALTER COLUMN $column
SET DEFAULT " . $new_def->{DEFAULT});
}
}
else {
my $new_ddl = $self->get_type_ddl(\%new_def_copy);
push(@statements, "ALTER TABLE $table CHANGE COLUMN
$column $column $new_ddl");
}
if ($old_def->{PRIMARYKEY} && !$new_def->{PRIMARYKEY}) {
# Dropping a PRIMARY KEY needs an explicit DROP PRIMARY KEY
push(@statements, "ALTER TABLE $table DROP PRIMARY KEY");
}
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);
}
sub get_set_serial_sql {
my ($self, $table, $column, $value) = @_;
return ("ALTER TABLE $table AUTO_INCREMENT = $value");
}
# Converts a DBI column_info output to an abstract column definition.
# Expects to only be called by Bugzila::DB::Mysql::_bz_build_schema_from_disk,
# although there's a chance that it will also work properly if called
# 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,439 +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';
use constant MULTIPLE_FKS_IN_ALTER => 0;
# 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 = shift;
my $ddl = $self->SUPER::get_fk_ddl(@_);
# iThe Bugzilla Oracle driver implements UPDATE via a trigger.
$ddl =~ s/ON UPDATE \S+//i;
# RESTRICT is the default for DELETE on Oracle and may not be specified.
$ddl =~ s/ON DELETE RESTRICT//i;
return $ddl;
}
sub get_add_fks_sql {
my $self = shift;
my ($table, $column_fks) = @_;
my @sql = $self->SUPER::get_add_fks_sql(@_);
foreach my $column (keys %$column_fks) {
my $fk = $column_fks->{$column};
next if $fk->{UPDATE} && uc($fk->{UPDATE}) ne 'CASCADE';
my $fk_name = $self->_get_fk_name($table, $column, $fk);
my $to_column = $fk->{COLUMN};
my $to_table = $fk->{TABLE};
my $trigger = <<END;
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;
END
push(@sql, $trigger);
}
return @sql;
}
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};
if (defined $default) {
$default = $specific->{$default} if exists $specific->{$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) )
{
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 = $default if defined $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;
}
sub get_set_serial_sql {
my ($self, $table, $column, $value) = @_;
my @sql;
my $seq_name = "${table}_${column}_SEQ";
push(@sql, "DROP SEQUENCE ${seq_name}");
push(@sql, $self->_get_create_seq_ddl($table, $column, $value));
return @sql;
}
sub get_drop_column_ddl {
my $self = shift;
my ($table, $column) = @_;
my @sql;
push(@sql, $self->SUPER::get_drop_column_ddl(@_));
my $dbh=Bugzilla->dbh;
my $trigger_name = uc($table . "_" . $column);
my $exist_trigger = $dbh->selectcol_arrayref(
"SELECT OBJECT_NAME FROM USER_OBJECTS
WHERE OBJECT_NAME = ?", undef, $trigger_name);
if(@$exist_trigger) {
push(@sql, "DROP TRIGGER $trigger_name");
}
return @sql;
}
1;

View File

@@ -1,173 +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_set_serial_sql {
my ($self, $table, $column, $value) = @_;
return ("SELECT setval('${table}_${column}_seq', $value, false)
FROM $table");
}
sub _get_alter_type_sql {
my ($self, $table, $column, $new_def, $old_def) = @_;
my @statements;
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/\bserial\b/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,270 +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 Carp;
use Data::Dumper;
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;
# Don't show function arguments, in case they contain confidential data.
local $Carp::MaxArgNums = -1;
# Don't show the error as coming from Bugzilla::Error, show it as coming
# from the caller.
local $Carp::CarpInternal{'Bugzilla::Error'} = 1;
$vars->{traceback} = Carp::longmess();
# 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 .= remote_ip();
$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());
}
# There are some tests that throw and catch a lot of errors,
# and calling $template->process over and over for those errors
# is too slow. So instead, we just "die" with a dump of the arguments.
elsif (Bugzilla->error_mode == ERROR_MODE_TEST) {
die Dumper($vars);
}
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
|| Bugzilla->error_mode == ERROR_MODE_JSON_RPC)
{
# Clone the hash so we aren't modifying the constant.
my %error_map = %{ WS_ERROR_CODE() };
require Bugzilla::Hook;
Bugzilla::Hook::process('webservice_error_codes',
{ 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;
}
if (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT) {
die SOAP::Fault->faultcode($code)->faultstring($message);
}
else {
my $server = Bugzilla->_json_server;
# Technically JSON-RPC isn't allowed to have error numbers
# higher than 999, but we do this to avoid conflicts with
# the internal JSON::RPC error codes.
$server->raise_error(code => 100000 + $code,
message => $message,
id => $server->{_bz_request_id},
version => $server->version);
# Most JSON-RPC Throw*Error calls happen within an eval inside
# of JSON::RPC. So, in that circumstance, instead of exiting,
# we die with no message. JSON::RPC checks raise_error before
# it checks $@, so it returns the proper error.
die if _in_eval();
$server->response($server->error_response_header);
}
}
}
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>

View File

@@ -1,813 +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 Developers are Copyright (C) 2009 the
# Initial Developer. All Rights Reserved.
#
# Contributor(s):
# Max Kanat-Alexander <mkanat@bugzilla.org>
package Bugzilla::Extension;
use strict;
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Install::Util qw(
extension_code_files extension_template_directory
extension_package_directory);
use File::Basename;
use File::Spec;
####################
# Subclass Methods #
####################
sub new {
my ($class, $params) = @_;
$params ||= {};
bless $params, $class;
return $params;
}
#######################################
# Class (Bugzilla::Extension) Methods #
#######################################
sub load {
my ($class, $extension_file, $config_file) = @_;
my $package;
# This is needed during checksetup.pl, because Extension packages can
# only be loaded once (they return "1" the second time they're loaded,
# instead of their name). During checksetup.pl, extensions are loaded
# once by Bugzilla::Install::Requirements, and then later again via
# Bugzilla->extensions (because of hooks).
my $map = Bugzilla->request_cache->{extension_requirement_package_map};
if ($config_file) {
if ($map and defined $map->{$config_file}) {
$package = $map->{$config_file};
}
else {
my $name = require $config_file;
if ($name =~ /^\d+$/) {
ThrowCodeError('extension_must_return_name',
{ extension => $config_file,
returned => $name });
}
$package = "${class}::$name";
}
__do_call($package, 'modify_inc', $config_file);
}
if ($map and defined $map->{$extension_file}) {
$package = $map->{$extension_file};
$package->modify_inc($extension_file) if !$config_file;
}
else {
my $name = require $extension_file;
if ($name =~ /^\d+$/) {
ThrowCodeError('extension_must_return_name',
{ extension => $extension_file, returned => $name });
}
$package = "${class}::$name";
$package->modify_inc($extension_file) if !$config_file;
}
$class->_validate_package($package, $extension_file);
return $package;
}
sub _validate_package {
my ($class, $package, $extension_file) = @_;
# For extensions from data/extensions/additional, we don't have a file
# name, so we fake it.
if (!$extension_file) {
$extension_file = $package;
$extension_file =~ s/::/\//g;
$extension_file .= '.pm';
}
if (!eval { $package->NAME }) {
ThrowCodeError('extension_no_name',
{ filename => $extension_file, package => $package });
}
if (!$package->isa($class)) {
ThrowCodeError('extension_must_be_subclass',
{ filename => $extension_file,
package => $package,
class => $class });
}
}
sub load_all {
my $class = shift;
my ($file_sets, $extra_packages) = extension_code_files();
my @packages;
foreach my $file_set (@$file_sets) {
my $package = $class->load(@$file_set);
push(@packages, $package);
}
# Extensions from data/extensions/additional
foreach my $package (@$extra_packages) {
# Don't load an "additional" extension if we already have an extension
# loaded with that name.
next if grep($_ eq $package, @packages);
# Untaint the package name
$package =~ /([\w:]+)/;
$package = $1;
eval("require $package") || die $@;
$package->_validate_package($package);
push(@packages, $package);
}
return \@packages;
}
# Modifies @INC so that extensions can use modules like
# "use Bugzilla::Extension::Foo::Bar", when Bar.pm is in the lib/
# directory of the extension.
sub modify_inc {
my ($class, $file) = @_;
# Note that this package_dir call is necessary to set things up
# for my_inc, even if we didn't take its return value.
my $package_dir = __do_call($class, 'package_dir', $file);
# Don't modify @INC for extensions that are just files in the extensions/
# directory. We don't want Bugzilla's base lib/CGI.pm being loaded as
# Bugzilla::Extension::Foo::CGI or any other confusing thing like that.
return if $package_dir eq bz_locations->{'extensionsdir'};
unshift(@INC, sub { __do_call($class, 'my_inc', @_) });
}
# This is what gets put into @INC by modify_inc.
sub my_inc {
my ($class, undef, $file) = @_;
# This avoids infinite recursion in case anything inside of this function
# does a "require". (I know for sure that File::Spec->case_tolerant does
# a "require" on Windows, for example.)
return if $file !~ /^Bugzilla/;
my $lib_dir = __do_call($class, 'lib_dir');
my @class_parts = split('::', $class);
my ($vol, $dir, $file_name) = File::Spec->splitpath($file);
my @dir_parts = File::Spec->splitdir($dir);
# File::Spec::Win32 (any maybe other OSes) add an empty directory at the
# end of @dir_parts.
@dir_parts = grep { $_ ne '' } @dir_parts;
# Validate that this is a sub-package of Bugzilla::Extension::Foo ($class).
for (my $i = 0; $i < scalar(@class_parts); $i++) {
return if !@dir_parts;
if (File::Spec->case_tolerant) {
return if lc($class_parts[$i]) ne lc($dir_parts[0]);
}
else {
return if $class_parts[$i] ne $dir_parts[0];
}
shift(@dir_parts);
}
# For Bugzilla::Extension::Foo::Bar, this would look something like
# extensions/Example/lib/Bar.pm
my $resolved_path = File::Spec->catfile($lib_dir, @dir_parts, $file_name);
open(my $fh, '<', $resolved_path);
return $fh;
}
####################
# Instance Methods #
####################
use constant enabled => 1;
sub lib_dir {
my $invocant = shift;
my $package_dir = __do_call($invocant, 'package_dir');
# For extensions that are just files in the extensions/ directory,
# use the base lib/ dir as our "lib_dir". Note that Bugzilla never
# uses lib_dir in this case, though, because modify_inc is prevented
# from modifying @INC when we're just a file in the extensions/ directory.
# So this particular code block exists just to make lib_dir return
# something right in case an extension needs it for some odd reason.
if ($package_dir eq bz_locations()->{'extensionsdir'}) {
return bz_locations->{'ext_libpath'};
}
return File::Spec->catdir($package_dir, 'lib');
}
sub template_dir { return extension_template_directory(@_); }
sub package_dir { return extension_package_directory(@_); }
######################
# Helper Subroutines #
######################
# In order to not conflict with extensions' private subroutines, any helpers
# here should start with a double underscore.
# This is for methods that can optionally be overridden in Config.pm.
# It falls back to the local implementation if $class cannot do
# the method. This is necessary because Config.pm is not a subclass of
# Bugzilla::Extension.
sub __do_call {
my ($class, $method, @args) = @_;
if ($class->can($method)) {
return $class->$method(@args);
}
my $function_ref;
{ no strict 'refs'; $function_ref = \&{$method}; }
return $function_ref->($class, @args);
}
1;
__END__
=head1 NAME
Bugzilla::Extension - Base class for Bugzilla Extensions.
=head1 SYNOPSIS
The following would be in F<extensions/Foo/Extension.pm> or
F<extensions/Foo.pm>:
package Bugzilla::Extension::Foo
use strict;
use base qw(Bugzilla::Extension);
our $VERSION = '0.02';
use constant NAME => 'Foo';
sub some_hook_name { ... }
__PACKAGE__->NAME;
Custom templates would go into F<extensions/Foo/template/en/default/>.
L<Template hooks|/Template Hooks> would go into
F<extensions/Foo/template/en/default/hook/>.
=head1 DESCRIPTION
This is the base class for all Bugzilla extensions.
=head1 WRITING EXTENSIONS
The L</SYNOPSIS> above gives a pretty good overview of what's basically
required to write an extension. This section gives more information
on exactly how extensions work and how you write them. There is also a
L<wiki page|https://wiki.mozilla.org/Bugzilla:Extension_Notes> with additional HOWTOs, tips and tricks.
=head2 Using F<extensions/create.pl>
There is a script, L<extensions::create>, that will set up the framework
of a new extension for you. To use it, pick a name for your extension
and, in the base bugzilla directory, do:
C<extensions/create.pl NAME>
But replace C<NAME> with the name you picked for your extension. That
will create a new directory in the F<extensions/> directory with the name
of your extension. The directory will contain a full framework for
a new extension, with helpful comments in each file describing things
about them.
=head2 Example Extension
There is a sample extension in F<extensions/Example/> that demonstrates
most of the things described in this document, so if you find the
documentation confusing, try just reading the code instead.
=head2 Where Extension Code Goes
Extension code lives under the F<extensions/> directory in Bugzilla.
There are two ways to write extensions:
=over
=item 1
If your extension will have only code and no templates or other files,
you can create a simple C<.pm> file in the F<extensions/> directory.
For example, if you wanted to create an extension called "Foo" using this
method, you would put your code into a file called F<extensions/Foo.pm>.
=item 2
If you plan for your extension to have templates and other files, you
can create a whole directory for your extension, and the main extension
code would go into a file called F<Extension.pm> in that directory.
For example, if you wanted to create an extension called "Foo" using this
method, you would put your code into a file called
F<extensions/Foo/Extension.pm>.
=back
=head2 The Extension C<NAME>.
The "name" of an extension shows up in several places:
=over
=item 1
The name of the package:
C<package Bugzilla::Extension::Foo;>
=item 2
In a C<NAME> constant that B<must> be defined for every extension:
C<< use constant NAME => 'Foo'; >>
=item 3
At the very end of the file:
C<< __PACKAGE__->NAME; >>
You'll notice that though most Perl packages end with C<1;>, Bugzilla
Extensions must B<always> end with C<< __PACKAGE__->NAME; >>.
=back
The name must be identical in all of those locations.
=head2 Hooks
In L<Bugzilla::Hook>, there is a L<list of hooks|Bugzilla::Hook/HOOKS>.
These are the various areas of Bugzilla that an extension can "hook" into,
which allow your extension to perform code during that point in Bugzilla's
execution.
If your extension wants to implement a hook, all you have to do is
write a subroutine in your hook package that has the same name as
the hook. The subroutine will be called as a method on your extension,
and it will get the arguments specified in the hook's documentation as
named parameters in a hashref.
For example, here's an implementation of a hook named C<foo_start>
that gets an argument named C<bar>:
sub foo_start {
my ($self, $args) = @_;
my $bar = $args->{bar};
print "I got $bar!\n";
}
And that would go into your extension's code file--the file that was
described in the L</Where Extension Code Goes> section above.
During your subroutine, you may want to know what values were passed
as CGI arguments to the current script, or what arguments were passed to
the current WebService method. You can get that data via
L<Bugzilla/input_params>.
=head3 Adding New Hooks To Bugzilla
If you need a new hook for your extension and you want that hook to be
added to Bugzilla itself, see our development process at
L<http://wiki.mozilla.org/Bugzilla:Developers>.
In order for a new hook to be accepted into Bugzilla, it has to work,
it must have documentation in L<Bugzilla::Hook>, and it must have example
code in F<extensions/Example/Extension.pm>.
One question that is often asked about new hooks is, "Is this the most
flexible way to implement this hook?" That is, the more power extension
authors get from a hook, the more likely it is to be accepted into Bugzilla.
Hooks that only hook a very specific part of Bugzilla will not be accepted
if their functionality can be accomplished equally well with a more generic
hook.
=head2 If Your Extension Requires Certain Perl Modules
If there are certain Perl modules that your extension requires in order
to run, there is a way you can tell Bugzilla this, and then L<checksetup>
will make sure that those modules are installed, when you run L<checksetup>.
To do this, you need to specify a constant called C<REQUIRED_MODULES>
in your extension. This constant has the same format as
L<Bugzilla::Install::Requirements/REQUIRED_MODULES>.
If there are optional modules that add additional functionality to your
application, you can specify them in a constant called OPTIONAL_MODULES,
which has the same format as
L<Bugzilla::Install::Requirements/OPTIONAL_MODULES>.
=head3 If Your Extension Needs Certain Modules In Order To Compile
If your extension needs a particular Perl module in order to
I<compile>, then you have a "chicken and egg" problem--in order to
read C<REQUIRED_MODULES>, we have to compile your extension. In order
to compile your extension, we need to already have the modules in
C<REQUIRED_MODULES>!
To get around this problem, Bugzilla allows you to have an additional
file, besides F<Extension.pm>, called F<Config.pm>, that contains
just C<REQUIRED_MODULES>. If you have a F<Config.pm>, it must also
contain the C<NAME> constant, instead of your main F<Extension.pm>
containing the C<NAME> constant.
The contents of the file would look something like this for an extension
named C<Foo>:
package Bugzilla::Extension::Foo;
use strict;
use constant NAME => 'Foo';
use constant REQUIRED_MODULES => [
{
package => 'Some-Package',
module => 'Some::Module',
version => 0,
}
];
__PACKAGE__->NAME;
Note that it is I<not> a subclass of C<Bugzilla::Extension>, because
at the time that module requirements are being checked in L<checksetup>,
C<Bugzilla::Extension> cannot be loaded. Also, just like F<Extension.pm>,
it ends with C<< __PACKAGE__->NAME; >>. Note also that it has the
B<exact same> C<package> name as F<Extension.pm>.
This file may not use any Perl modules other than L<Bugzilla::Constants>,
L<Bugzilla::Install::Util>, L<Bugzilla::Install::Requirements>, and
modules that ship with Perl itself.
If you want to define both C<REQUIRED_MODULES> and C<OPTIONAL_MODULES>,
they must both be in F<Config.pm> or both in F<Extension.pm>.
Every time your extension is loaded by Bugzilla, F<Config.pm> will be
read and then F<Extension.pm> will be read, so your methods in F<Extension.pm>
will have access to everything in F<Config.pm>. Don't define anything
with an identical name in both files, or Perl may throw a warning that
you are redefining things.
This method of setting C<REQUIRED_MODULES> is of course not available if
your extension is a single file named C<Foo.pm>.
If any of this is confusing, just look at the code of the Example extension.
It uses this method to specify requirements.
=head2 Libraries
Extensions often want to have their own Perl modules. Your extension
can load any Perl module in its F<lib/> directory. (So, if your extension is
F<extensions/Foo/>, then your Perl modules go into F<extensions/Foo/lib/>.)
However, the C<package> name of your libraries will not work quite
like normal Perl modules do. F<extensions/Foo/lib/Bar.pm> is
loaded as C<Bugzilla::Extension::Foo::Bar>. Or, to say it another way,
C<use Bugzilla::Extension::Foo::Bar;> loads F<extensions/Foo/lib/Bar.pm>,
which should have C<package Bugzilla::Extension::Foo::Bar;> as its package
name.
This allows any place in Bugzilla to load your modules, which is important
for some hooks. It even allows other extensions to load your modules, and
allows you to install your modules into the global Perl install
as F<Bugzilla/Extension/Foo/Bar.pm>, if you'd like, which helps allow CPAN
distribution of Bugzilla extensions.
B<Note:> If you want to C<use> or C<require> a module that's in
F<extensions/Foo/lib/> at the top level of your F<Extension.pm>,
you must have a F<Config.pm> (see above) with at least the C<NAME>
constant defined in it.
=head2 Templates
Extensions store templates in a C<template> subdirectory of the extension.
(Obviously, this isn't available for extensions that aren't a directory.)
The format of this directory is exactly like the normal layout of Bugzilla's
C<template> directory--in fact, your extension's C<template> directory
becomes part of Bugzilla's template "search path" as described in
L<Bugzilla::Install::Util/template_include_path>.
You can actually include templates in your extension without having any
C<.pm> files in your extension at all, if you want. (That is, it's entirely
valid to have an extension that's just template files and no code files.)
Bugzilla's templates are written in a language called Template Toolkit.
You can find out more about Template Toolkit at L<http://template-toolkit.org>.
There are two ways to extend or modify Bugzilla's templates: you can use
template hooks (described below) or you can override existing templates
entirely (described further down).
=head2 Template Hooks
Templates can be extended using a system of "hooks" that add new UI elements
to a particular area of Bugzilla without modifying the code of the existing
templates. This is the recommended way for extensions to modify the user
interface of Bugzilla.
=head3 Which Templates Can Be Hooked
There is no list of template hooks like there is for standard code hooks.
To find what places in the user interface can be hooked, search for the
string C<Hook.process> in Bugzilla's templates (in the
F<template/en/default/> directory). That will also give you the name of
the hooks--the first argument to C<Hook.process> is the name of the hook.
(A later section in this document explains how to use that name).
For example, if you see C<Hook.process("additional_header")>, that means
the name of the hook is C<additional_header>.
=head3 Where Template Hooks Go
To extend templates in your extension using template hooks, you put files into
the F<template/en/default/hook> directory of your extension. So, if you had an
extension called "Foo", your template extensions would go into
F<extensions/Foo/template/en/default/hook/>.
(Note that the base F<template/en/default/hook> directory in Bugzilla itself
also works, although you would never use that for an extension that you
intended to distribute.)
The files that go into this directory have a certain name, based on the
name of the template that is being hooked, and the name of the hook.
For example, let's imagine that you have an extension named "Foo",
and you want to use the C<additional_header> hook in
F<template/en/default/global/header.html.tmpl>. Your code would go into
F<extensions/Foo/template/en/default/hook/global/header-additional_header.html.tmpl>. Any code you put into that file will happen at the point that
C<Hook.process("additional_header")> is called in
F<template/en/default/global/header.html.tmpl>.
As you can see, template extension file names follow a pattern. The
pattern looks like:
<templates>/hook/<template path>/<template name>-<hook name>.<template type>.tmpl
=over
=item <templates>
This is the full path to the template directory, like
F<extensions/Foo/template/en/default>. This works much like normal templates
do, in the sense that template extensions in C<custom> override template
extensions in C<default> for your extension, templates for different languages
can be supplied, etc. Template extensions are searched for and run in the
order described in L<Bugzilla::Install::Util/template_include_path>.
The difference between normal templates and template hooks is that hooks
will be run for I<every> extension, whereas for normal templates, Bugzilla
just takes the first one it finds and stops searching. So while a template
extension in the C<custom> directory may override the same-named template
extension in the C<default> directory I<within your Bugzilla extension>,
it will not override the same-named template extension in the C<default>
directory of another Bugzilla extension.
=item <template path>
This is the part of the path (excluding the filename) that comes after
F<template/en/default/> in a template's path. So, for
F<template/en/default/global/header.html.tmpl>, this would simply be
C<global>.
=item <template name>
This is the file name of the template, before the C<.html.tmpl> part.
So, for F<template/en/default/global/header.html.tmpl>, this would be
C<header>.
=item <hook name>
This is the name of the hook--what you saw in C<Hook.process> inside
of the template you want to hook. In our example, this is
C<additional_header>.
=item <template type>
This is what comes after the template name but before C<.tmpl> in the
template's path. In most cases this is C<html>, but sometimes it's
C<none>, C<txt>, C<js>, or various other formats, indicating what
type of output the template has.
=back
=head3 Adding New Template Hooks to Bugzilla
Adding new template hooks is just like adding code hooks (see
L</Adding New Hooks To Bugzilla>) except that you don't have to
document them, and including example code is optional.
=head2 Overriding Existing Templates
Sometimes you don't want to extend a template, you just want to replace
it entirely with your extension's template, or you want to add an entirely
new template to Bugzilla for your extension to use.
To replace the F<template/en/default/global/banner.html.tmpl> template
in an extension named "Foo", create a file called
F<extensions/Foo/template/en/default/global/banner.html.tmpl>. Note that this
is very similar to the path for a template hook, except that it excludes
F<hook/>, and the template is named I<exactly> like the standard Bugzilla
template.
You can also use this method to add entirely new templates. If you have
an extension named "Foo", and you add a file named
F<extensions/Foo/template/en/default/foo/bar.html.tmpl>, you can load
that in your code using C<< $template->process('foo/bar.html.tmpl') >>.
=head3 A Warning About Extensions That You Want To Distribute
You should never override an existing Bugzilla template in an
extension that you plan to distribute to others, because only one extension
can override any given template, and which extension will "win" that war
if there are multiple extensions installed is totally undefined.
However, adding new templates in an extension that you want to distribute
is fine, though you have to be careful about how you name them, because
any templates with an identical path and name (say, both called
F<global/stuff.html.tmpl>) will conflict. The usual way to work around
this is to put all your custom templates into a template path that's
named after your extension (since the name of your extension has to be
unique anyway). So if your extension was named Foo, your custom templates
would go into F<extensions/Foo/template/en/default/foo/>. The only
time that doesn't work is with the C<page_before_template> extension, in which
case your templates should probably be in a directory like
F<extensions/Foo/template/en/default/page/foo/> so as not to conflict with
other pages that other extensions might add.
=head2 CSS, JavaScript, and Images
If you include CSS, JavaScript, and images in your extension that are
served directly to the user (that is, they're not read by a script and
then printed--they're just linked directly in your HTML), they should go
into the F<web/> subdirectory of your extension.
So, for example, if you had a CSS file called F<style.css> and your
extension was called F<Foo>, your file would go into
F<extensions/Foo/web/style.css>.
=head2 Disabling Your Extension
If you want your extension to be totally ignored by Bugzilla (it will
not be compiled or seen to exist at all), then create a file called
C<disabled> in your extension's directory. (If your extension is just
a file, like F<extensions/Foo.pm>, you cannot use this method to disable
your extension, and will just have to remove it from the directory if you
want to totally disable it.) Note that if you are running under mod_perl,
you may have to restart your web server for this to take effect.
If you want your extension to be compiled and have L<checksetup> check
for its module pre-requisites, but you don't want the module to be used
by Bugzilla, then you should make your extension's L</enabled> method
return C<0> or some false value.
=head1 DISTRIBUTING EXTENSIONS
If you've made an extension and you want to publish it, the first
thing you'll want to do is package up your extension's code and
then put a link to it in the appropriate section of
L<http://wiki.mozilla.org/Bugzilla:Addons>.
=head2 Distributing on CPAN
If you want a centralized distribution point that makes it easy
for Bugzilla users to install your extension, it is possible to
distribute your Bugzilla Extension through CPAN.
The details of making a standard CPAN module are too much to
go into here, but a lot of it is covered in L<perlmodlib>
and on L<http://www.cpan.org/> among other places.
When you distribute your extension via CPAN, your F<Extension.pm>
should simply install itself as F<Bugzilla/Extension/Foo.pm>,
where C<Foo> is the name of your module. You do not need a separate
F<Config.pm> file, because CPAN itself will handle installing
the prerequisites of your module, so Bugzilla doesn't have to
worry about it.
=head3 Templates in extensions distributed on CPAN
If your extension is F</usr/lib/perl5/Bugzilla/Extension/Foo.pm>,
then Bugzilla will look for templates in the directory
F</usr/lib/perl5/Bugzilla/Extension/Foo/template/>.
You can change this behavior by overriding the L</template_dir>
or L</package_dir> methods described lower down in this document.
=head3 Using an extension distributed on CPAN
There is a file named F<data/extensions/additional> in Bugzilla.
This is a plain-text file. Each line is the name of a module,
like C<Bugzilla::Extension::Foo>. In addition to the extensions
in the F<extensions/> directory, each module listed in this file
will be loaded as a Bugzilla Extension whenever Bugzilla loads or
uses extensions.
=head1 GETTING HELP WITH WRITING EXTENSIONS
If you are an extension author and you'd like some assistance from other
extension authors or the Bugzilla development team, you can use the
normal support channels described at L<http://www.bugzilla.org/support/>.
=head1 ADDITIONAL CONSTANTS
In addition to C<NAME>, there are some other constants you might
want to define:
=head2 C<$VERSION>
This should be a string that describes what version of your extension
this is. Something like C<1.0>, C<1.3.4> or a similar string.
There are no particular restrictions on the format of version numbers,
but you should probably keep them to just numbers and periods, in the
interest of other software that parses version numbers.
By default, this will be C<undef> if you don't define it.
=head1 SUBCLASS METHODS
In addition to hooks, there are a few methods that your extension can
define to modify its behavior, if you want:
=head2 Class Methods
These methods are called on your extension's class. (Like
C<< Bugzilla::Extension::Foo->some_method >>).
=head3 C<new>
Once every request, this method is called on your extension in order
to create an "instance" of it. (Extensions are treated like objects--they
are instantiated once per request in Bugzilla, and then methods are
called on the object.)
=head2 Instance Methods
These are called on an instantiated Extension object.
=head3 C<enabled>
This should return C<1> if this extension's hook code should be run
by Bugzilla, and C<0> otherwise.
=head3 C<package_dir>
This returns the directory that your extension is located in.
If this is an extension that was installed via CPAN, the directory will
be the path to F<Bugzilla/Extension/Foo/>, if C<Foo.pm> is the name of your
extension.
If you want to override this method, and you have a F<Config.pm>, you must
override this method in F<Config.pm>.
=head3 C<template_dir>
The directory that your package's templates are in.
This defaults to the C<template> subdirectory of the L</package_dir>.
If you want to override this method, and you have a F<Config.pm>, you must
override this method in F<Config.pm>.
=head3 C<lib_dir>
The directory where your extension's libraries are.
This defaults to the C<lib> subdirectory of the L</package_dir>.
If you want to override this method, and you have a F<Config.pm>, you must
override this method in F<Config.pm>.
=head1 BUGZILLA::EXTENSION CLASS METHODS
These are used internally by Bugzilla to load and set up extensions.
If you are an extension author, you don't need to care about these.
=head2 C<load>
Takes two arguments, the path to F<Extension.pm> and the path to F<Config.pm>,
for an extension. Loads the extension's code packages into memory using
C<require>, does some sanity-checking on the extension, and returns the
package name of the loaded extension.
=head2 C<load_all>
Calls L</load> for every enabled extension installed into Bugzilla,
and returns an arrayref of all the package names that were loaded.

File diff suppressed because it is too large Load Diff

View File

@@ -1,347 +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>
# Greg Hendricks <ghendricks@novell.com>
use strict;
package Bugzilla::Field::Choice;
use base qw(Bugzilla::Field::ChoiceInterface 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
isactive
visibility_value_id
);
use constant UPDATE_COLUMNS => qw(
value
sortkey
isactive
visibility_value_id
);
use constant NAME_FIELD => 'value';
use constant LIST_ORDER => 'sortkey, value';
use constant VALIDATORS => {
value => \&_check_value,
sortkey => \&_check_sortkey,
visibility_value_id => \&_check_visibility_value_id,
isactive => \&_check_isactive,
};
use constant CLASS_MAP => {
bug_status => 'Bugzilla::Status',
classification => 'Bugzilla::Classification',
component => 'Bugzilla::Component',
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->_get_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(); # From ChoiceInterface.
$self->SUPER::remove_from_db();
}
############
# Mutators #
############
sub set_is_active { $_[0]->set('isactive', $_[1]); }
sub set_name { $_[0]->set('value', $_[1]); }
sub set_sortkey { $_[0]->set('sortkey', $_[1]); }
sub set_visibility_value {
my ($self, $value) = @_;
$self->set('visibility_value_id', $value);
delete $self->{visibility_value};
}
##############
# Validators #
##############
sub _check_isactive {
my ($invocant, $value) = @_;
$value = Bugzilla::Object::check_boolean($invocant, $value);
if (!$value and ref $invocant) {
if ($invocant->is_default) {
my $field = $invocant->field;
ThrowUserError('fieldvalue_is_default',
{ value => $invocant, field => $field,
param_name => $invocant->DEFAULT_MAP->{$field->name}
});
}
if ($invocant->is_static) {
ThrowUserError('fieldvalue_not_deletable',
{ value => $invocant, field => $invocant->field });
}
}
return $value;
}
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.
This class implements L<Bugzilla::Field::ChoiceInterface>, and so all
methods of that class are also available here.
=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 Mutators
This class implements mutators for all of the settable accessors in
L<Bugzilla::Field::ChoiceInterface>.

View File

@@ -1,273 +0,0 @@
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The 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>
# Greg Hendricks <ghendricks@novell.com>
package Bugzilla::Field::ChoiceInterface;
use strict;
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Field;
use Scalar::Util qw(blessed);
# Helps implement the "field" accessor without subclasses having to
# write code.
sub FIELD_NAME { return $_[0]->DB_TABLE; }
####################
# Subclass Helpers #
####################
sub _check_if_controller {
my $self = shift;
my $vis_fields = $self->controls_visibility_of_fields;
my $values = $self->controlled_values_array;
if (@$vis_fields || @$values) {
ThrowUserError('fieldvalue_is_controller',
{ value => $self, fields => [map($_->name, @$vis_fields)],
vals => $self->controlled_values });
}
}
#############
# Accessors #
#############
sub is_active { return $_[0]->{'isactive'}; }
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 Bugzilla::Field::Choice->type() already have this set.
$cache->{"field_$class"} ||=
new Bugzilla::Field({ name => $class->FIELD_NAME });
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', '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}) {
require Bugzilla::Field::Choice;
$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;
require Bugzilla::Field::Choice;
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};
}
sub controlled_values_array {
my ($self) = @_;
my $values = $self->controlled_values;
return [map { @{ $values->{$_} } } keys %$values];
}
sub is_visible_on_bug {
my ($self, $bug) = @_;
# Values currently set on the bug are always shown.
return 1 if $self->is_set_on_bug($bug);
# Inactive values are, otherwise, never shown.
return 0 if !$self->is_active;
# Values without a visibility value are, otherwise, always shown.
my $visibility_value = $self->visibility_value;
return 1 if !$visibility_value;
# Values with a visibility value are only shown if the visibility
# value is set on the bug.
return $visibility_value->is_set_on_bug($bug);
}
sub is_set_on_bug {
my ($self, $bug) = @_;
my $field_name = $self->FIELD_NAME;
# This allows bug/create/create.html.tmpl to pass in a hashref that
# looks like a bug object.
my $value = blessed($bug) ? $bug->$field_name : $bug->{$field_name};
return 0 if !defined $value;
if ($self->field->type == FIELD_TYPE_BUG_URLS
or $self->field->type == FIELD_TYPE_MULTI_SELECT)
{
return grep($_ eq $self->name, @$value) ? 1 : 0;
}
return $value eq $self->name ? 1 : 0;
}
1;
__END__
=head1 NAME
Bugzilla::Field::ChoiceInterface - Makes an object act like a
Bugzilla::Field::Choice.
=head1 DESCRIPTION
This is an "interface", in the Java sense (sometimes called a "Role"
or a "Mixin" in other languages). L<Bugzilla::Field::Choice> is the
primary implementor of this interface, but other classes also implement
it if they want to "act like" L<Bugzilla::Field::Choice>.
=head1 METHODS
=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<is_active>
Whether or not this value should appear as an option on bugs that do
not already have it set as the current value.
=item C<is_static>
C<0> if this field value can be renamed or deleted, C<1> otherwise.
=item C<is_default>
C<1> if this is the default value for this field, C<0> otherwise.
=item C<bug_count>
An integer count of the number of bugs that have this value set.
=item C<controls_visibility_of_fields>
Returns an arrayref of L<Bugzilla::Field> objects, representing any
fields whose visibility are controlled by this field value.
=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 objects that implement
C<Bugzilla::Field::ChoiceInterface>, representing values that this value
controls the visibility of, for that field.
=item C<visibility_value>
Returns an object that implements C<Bugzilla::Field::ChoiceInterface>,
which represents the value that needs to be set in order for this
value to appear in the UI.
=item C<is_visible_on_bug>
Returns C<1> if, according to the settings of C<is_active> and
C<visibility_value>, this value should be displayed as an option
when viewing a bug. Returns C<0> otherwise.
Takes a single argument, a L<Bugzilla::Bug> object or a hash with
similar fields to a L<Bugzilla::Bug> object.
=item C<is_set_on_bug>
Returns C<1> if this value is the current value set for its field on
the passed-in L<Bugzilla::Bug> object (or a hash that looks like a
L<Bugzilla::Bug>). For multi-valued fields, we return C<1> if
I<any> of the currently selected values are this value.
Returns C<0> otherwise.
=back

File diff suppressed because it is too large Load Diff

View File

@@ -1,509 +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::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;
require Bugzilla::User;
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;
if (!defined $self->{inclusions}) {
($self->{inclusions}, $self->{inclusions_as_hash}) = get_clusions($self->id, 'in');
}
return $self->{inclusions};
}
sub inclusions_as_hash {
my $self = shift;
$self->inclusions unless defined $self->{inclusions_as_hash};
return $self->{inclusions_as_hash};
}
sub exclusions {
my $self = shift;
if (!defined $self->{exclusions}) {
($self->{exclusions}, $self->{exclusions_as_hash}) = get_clusions($self->id, 'ex');
}
return $self->{exclusions};
}
sub exclusions_as_hash {
my $self = shift;
$self->exclusions unless defined $self->{exclusions_as_hash};
return $self->{exclusions_as_hash};
}
######################################################################
# 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, %clusions_as_hash);
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";
$clusions_as_hash{$product_id}->{$component_id} = 1;
}
return (\%clusions, \%clusions_as_hash);
}
=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}) {
my $product_id = $criteria->{product_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)");
# 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 ";
my $addl_join_clause = "";
if ($criteria->{component_id}) {
my $component_id = $criteria->{component_id};
push(@criteria, "(i.component_id = $component_id OR i.component_id IS NULL)");
$join_clause .= "AND (e.component_id = $component_id OR e.component_id IS NULL) ";
}
else {
$addl_join_clause = "AND e.component_id IS NULL OR (i.component_id != e.component_id) ";
}
$join_clause .= "AND ((e.product_id = $product_id $addl_join_clause) OR e.product_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,645 +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 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 bugs {
my $self = shift;
return $self->{bugs} if exists $self->{bugs};
my $bug_ids = Bugzilla->dbh->selectcol_arrayref(
'SELECT bug_id FROM bug_group_map WHERE group_id = ?',
undef, $self->id);
require Bugzilla::Bug;
$self->{bugs} = Bugzilla::Bug->new_from_list($bug_ids);
return $self->{bugs};
}
sub members_direct {
my ($self) = @_;
$self->{members_direct} ||= $self->_get_members(GRANT_DIRECT);
return $self->{members_direct};
}
sub members_non_inherited {
my ($self) = @_;
$self->{members_non_inherited} ||= $self->_get_members();
return $self->{members_non_inherited};
}
# A helper for members_direct and members_non_inherited
sub _get_members {
my ($self, $grant_type) = @_;
my $dbh = Bugzilla->dbh;
my $grant_clause = $grant_type ? "AND grant_type = $grant_type" : "";
my $user_ids = $dbh->selectcol_arrayref(
"SELECT DISTINCT user_id
FROM user_group_map
WHERE isbless = 0 $grant_clause AND group_id = ?", undef, $self->id);
require Bugzilla::User;
return Bugzilla::User->new_from_list($user_ids);
}
sub flag_types {
my $self = shift;
require Bugzilla::FlagType;
$self->{flag_types} ||= Bugzilla::FlagType::match({ group => $self->id });
return $self->{flag_types};
}
sub grant_direct {
my ($self, $type) = @_;
$self->{grant_direct} ||= {};
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};
}
sub products {
my $self = shift;
return $self->{products} if exists $self->{products};
my $product_data = Bugzilla->dbh->selectall_arrayref(
'SELECT product_id, entry, membercontrol, othercontrol,
canedit, editcomponents, editbugs, canconfirm
FROM group_control_map WHERE group_id = ?', {Slice=>{}},
$self->id);
my @ids = map { $_->{product_id} } @$product_data;
require Bugzilla::Product;
my $products = Bugzilla::Product->new_from_list(\@ids);
my %data_map = map { $_->{product_id} => $_ } @$product_data;
my @retval;
foreach my $product (@$products) {
# Data doesn't need to contain product_id--we already have
# the product object.
delete $data_map{$product->id}->{product_id};
push(@retval, { controls => $data_map{$product->id},
product => $product });
}
$self->{products} = \@retval;
return $self->{products};
}
###############################
#### Methods ####
###############################
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 $dbh = Bugzilla->dbh;
$dbh->bz_start_transaction();
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};
Bugzilla::Hook::process('group_end_of_update',
{ group => $self, changes => $changes });
$dbh->bz_commit_transaction();
return $changes;
}
sub check_remove {
my ($self, $params) = @_;
# System groups cannot be deleted!
if (!$self->is_bug_group) {
ThrowUserError("system_group_not_deletable", { name => $self->name });
}
# Groups having a special role cannot be deleted.
my @special_groups;
foreach my $special_group (GROUP_PARAMS) {
if ($self->name eq Bugzilla->params->{$special_group}) {
push(@special_groups, $special_group);
}
}
if (scalar(@special_groups)) {
ThrowUserError('group_has_special_role',
{ name => $self->name,
groups => \@special_groups });
}
return if $params->{'test_only'};
my $cantdelete = 0;
my $users = $self->members_non_inherited;
if (scalar(@$users) && !$params->{'remove_from_users'}) {
$cantdelete = 1;
}
my $bugs = $self->bugs;
if (scalar(@$bugs) && !$params->{'remove_from_bugs'}) {
$cantdelete = 1;
}
my $products = $self->products;
if (scalar(@$products) && !$params->{'remove_from_products'}) {
$cantdelete = 1;
}
my $flag_types = $self->flag_types;
if (scalar(@$flag_types) && !$params->{'remove_from_flags'}) {
$cantdelete = 1;
}
ThrowUserError('group_cannot_delete', { group => $self }) if $cantdelete;
}
sub remove_from_db {
my $self = shift;
my $dbh = Bugzilla->dbh;
$self->check_remove(@_);
$dbh->bz_start_transaction();
Bugzilla::Hook::process('group_before_delete', { group => $self });
$dbh->do('DELETE FROM whine_schedules
WHERE mailto_type = ? AND mailto = ?',
undef, MAILTO_GROUP, $self->id);
# All the other tables will be handled by foreign keys when we
# drop the main "groups" row.
$self->SUPER::remove_from_db(@_);
$dbh->bz_commit_transaction();
}
# Add missing entries in bug_group_map for bugs created while
# a mandatory group was disabled and which is now enabled again.
sub _enforce_mandatory {
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 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;
$dbh->bz_start_transaction();
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;
Bugzilla::Hook::process('group_end_of_create', { group => $group });
$dbh->bz_commit_transaction();
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;
}
sub check_no_disclose {
my ($class, $params) = @_;
my $action = delete $params->{action};
$action =~ /^(?:add|remove)$/
or ThrowCodeError('bad_arg', { argument => $action,
function => "${class}::check_no_disclose" });
$params->{_error} = ($action eq 'add') ? 'group_restriction_not_allowed'
: 'group_invalid_removal';
my $group = $class->check($params);
return $group;
}
###############################
### 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) || lc($invocant->name) ne lc($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<check_no_disclose>
=over
=item B<Description>
Throws an error if the user cannot add or remove this group to/from a given
bug, but doesn't specify if this is because the group doesn't exist, or the
user is not allowed to edit this group restriction.
=item B<Params>
This method takes a single hashref as argument, with the following keys:
=over
=item C<name>
C<string> The name of the group to add or remove.
=item C<bug_id>
C<integer> The ID of the bug to which the group change applies.
=item C<product>
C<string> The name of the product the bug belongs to.
=item C<action>
C<string> Must be either C<add> or C<remove>, depending on whether the group
must be added or removed from the bug. Any other value will generate an error.
=back
=item C<Returns>
A C<Bugzilla::Group> object on success, else an error is thrown.
=back
=item C<check_remove>
=over
=item B<Description>
Determines whether it's OK to remove this group from the database, and
throws an error if it's not OK.
=item B<Params>
=over
=item C<test_only>
C<boolean> If you want to only check if the group can be deleted I<at all>,
under any circumstances, specify C<test_only> to just do the most basic tests
(the other parameters will be ignored in this situation, as those tests won't
be run).
=item C<remove_from_users>
C<boolean> True if it would be OK to remove all users who are in this group
from this group.
=item C<remove_from_bugs>
C<boolean> True if it would be OK to remove all bugs that are in this group
from this group.
=item C<remove_from_flags>
C<boolean> True if it would be OK to stop all flagtypes that reference
this group from referencing this group (e.g., as their grantgroup or
requestgroup).
=item C<remove_from_products>
C<boolean> True if it would be OK to remove this group from all group controls
on products.
=back
=item B<Returns> (nothing)
=back
=item C<members_non_inherited>
Returns an arrayref of L<Bugzilla::User> objects representing people who are
"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

File diff suppressed because it is too large Load Diff

View File

@@ -1,468 +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::Component;
use Bugzilla::Config qw(:admin);
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;
use constant STATUS_WORKFLOW => (
[undef, 'UNCONFIRMED'],
[undef, 'CONFIRMED'],
[undef, 'IN_PROGRESS'],
['UNCONFIRMED', 'CONFIRMED'],
['UNCONFIRMED', 'IN_PROGRESS'],
['UNCONFIRMED', 'RESOLVED'],
['CONFIRMED', 'IN_PROGRESS'],
['CONFIRMED', 'RESOLVED'],
['IN_PROGRESS', 'CONFIRMED'],
['IN_PROGRESS', 'RESOLVED'],
['RESOLVED', 'UNCONFIRMED'],
['RESOLVED', 'CONFIRMED'],
['RESOLVED', 'VERIFIED'],
['VERIFIED', 'UNCONFIRMED'],
['VERIFIED', 'CONFIRMED'],
);
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" },
# 2009-02-01 mozilla@matt.mchenryfamily.org -- Bug 398473
comment_box_position => { options => ['before_comments', 'after_comments'],
default => 'before_comments' },
# 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_canusewhineatothers',
description => 'Can configure whine reports for other users',
},
{
name => 'bz_canusewhines',
description => 'User can configure whine reports for self',
# inherited_by means that users in the groups listed below are
# automatically members of bz_canusewhines.
inherited_by => ['editbugs', 'bz_canusewhineatothers'],
},
{
name => 'bz_sudoers',
description => 'Can perform actions as other users',
},
{
name => 'bz_sudo_protect',
description => 'Can not be impersonated by other users',
inherited_by => ['bz_sudoers'],
},
);
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.',
version => Bugzilla::Version::DEFAULT_VERSION,
classification => 'Unclassified',
defaultmilestone => DEFAULT_MILESTONE,
};
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;
$dbh->bz_start_transaction();
# Create most of the system groups
foreach my $definition (SYSTEM_GROUPS) {
my $exists = new Bugzilla::Group({ name => $definition->{name} });
if (!$exists) {
$definition->{isbuggroup} = 0;
my $inherited_by = delete $definition->{inherited_by};
my $created = Bugzilla::Group->create($definition);
# Each group in inherited_by is automatically a member of this
# group.
if ($inherited_by) {
foreach my $name (@$inherited_by) {
my $member = Bugzilla::Group->check($name);
$dbh->do('INSERT INTO group_group_map (grantor_id,
member_id) VALUES (?,?)',
undef, $created->id, $member->id);
}
}
}
}
$dbh->bz_commit_transaction();
}
sub create_default_classification {
my $dbh = Bugzilla->dbh;
# Make the default Classification if it doesn't already exist.
if (!$dbh->selectrow_array('SELECT 1 FROM classifications')) {
print get_text('install_default_classification',
{ name => DEFAULT_CLASSIFICATION->{name} }) . "\n";
Bugzilla::Classification->create(DEFAULT_CLASSIFICATION);
}
}
# This function should be called only after creating the admin user.
sub create_default_product {
my $dbh = Bugzilla->dbh;
# And same for the default product/component.
if (!$dbh->selectrow_array('SELECT 1 FROM products')) {
print get_text('install_default_product',
{ name => DEFAULT_PRODUCT->{name} }) . "\n";
my $product = Bugzilla::Product->create(DEFAULT_PRODUCT);
# Get the user who will be the owner of the Component.
# We pick the admin with the lowest id, which is probably the
# admin checksetup.pl just created.
my $admin_group = new Bugzilla::Group({name => 'admin'});
my ($admin_id) = $dbh->selectrow_array(
'SELECT user_id FROM user_group_map WHERE group_id = ?
ORDER BY user_id ' . $dbh->sql_limit(1),
undef, $admin_group->id);
my $admin = Bugzilla::User->new($admin_id);
Bugzilla::Component->create({
%{ DEFAULT_COMPONENT() }, product => $product,
initialowner => $admin->login });
}
}
sub init_workflow {
my $dbh = Bugzilla->dbh;
my $has_workflow = $dbh->selectrow_array('SELECT 1 FROM status_workflow');
return if $has_workflow;
print get_text('install_workflow_init'), "\n";
my %status_ids = @{ $dbh->selectcol_arrayref(
'SELECT value, id FROM bug_status', {Columns=>[1,2]}) };
foreach my $pair (STATUS_WORKFLOW) {
my $old_id = $pair->[0] ? $status_ids{$pair->[0]} : undef;
my $new_id = $status_ids{$pair->[1]};
$dbh->do('INSERT INTO status_workflow (old_status, new_status)
VALUES (?,?)', undef, $old_id, $new_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);
};
# If there is no maintainer set, make this user the maintainer.
if (!Bugzilla->params->{'maintainer'}) {
SetParam('maintainer', $user->email);
write_params();
}
if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
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_classification>
Creates the default "Unclassified" L<Classification|Bugzilla::Classification>
if it doesn't already exist
=item C<create_default_product()>
Description: Creates the default product and component if
they don't exist.
Params: none
Returns: nothing
=back

View File

@@ -1,349 +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(
BZ_LIB
check_cpan_requirements
set_cpan_config
install_module
);
use Bugzilla::Constants;
use Bugzilla::Install::Requirements qw(have_vers);
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);
# These are required for install-module.pl to be able to install
# all modules properly.
use constant REQUIREMENTS => (
{
module => 'CPAN',
package => 'CPAN',
version => '1.81',
},
{
# When Module::Build isn't installed, the YAML module allows
# CPAN to read META.yml to determine that Module::Build first
# needs to be installed to compile a module.
module => 'YAML',
package => 'YAML',
version => 0,
},
{
# Many modules on CPAN are now built with Dist::Zilla, which
# unfortunately means they require this version of EU::MM to install.
module => 'ExtUtils::MakeMaker',
package => 'ExtUtils-MakeMaker',
version => '6.31',
},
);
# 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});
# These modules are problematic to install with "notest" (sometimes they
# get installed when they shouldn't). So we always test their installation
# and never ignore test failures.
use constant ALWAYS_TEST => qw(
Math::Random::Secure
);
# 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,
build_requires_install_policy => 'yes',
cache_metadata => 1,
colorize_output => 1,
colorize_print => 'bold',
index_expire => 1,
scan_cache => 'atstart',
inhibit_startup_message => 1,
bzip2 => bin_loc('bzip2'),
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://mirrors.kernel.org/cpan/
http://mirrors2.kernel.org/cpan/)],
};
sub check_cpan_requirements {
my ($original_dir, $original_args) = @_;
my @install;
foreach my $module (REQUIREMENTS) {
my $installed = have_vers($module, 1);
push(@install, $module) if !$installed;
}
return if !@install;
my $restart_required;
foreach my $module (@install) {
$restart_required = 1 if $module->{module} eq 'CPAN';
install_module($module->{module}, 1);
}
if ($restart_required) {
chdir $original_dir;
exec($^X, $0, @$original_args);
}
}
sub install_module {
my ($name, $test) = @_;
my $bzlib = BZ_LIB;
# Make Module::AutoInstall install all dependencies and never prompt.
local $ENV{PERL_AUTOINSTALL} = '--alldeps';
# This makes Net::SSLeay not prompt the user, if it gets installed.
# It also makes any other MakeMaker prompts accept their defaults.
local $ENV{PERL_MM_USE_DEFAULT} = 1;
# 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 'SOAP::Lite') {
$CPAN::Config->{makepl_arg} .= " --noprompt";
}
my $module = CPAN::Shell->expand('Module', $name);
if (!$module) {
die install_string('no_such_module', { module => $name }) . "\n";
}
my $version = $module->cpan_version;
my $module_name = $name;
if ($name eq 'LWP::UserAgent' && $^V lt v5.8.8) {
# LWP 6.x requires Perl 5.8.8 or newer.
# As PAUSE only indexes the very last version of each module,
# we have to specify the path to the tarball ourselves.
$name = 'GAAS/libwww-perl-5.837.tar.gz';
# This tarball contains LWP::UserAgent 5.835.
$version = '5.835';
}
print install_string('install_module',
{ module => $module_name, version => $version }) . "\n";
if (_always_test($name)) {
CPAN::Shell->install($name);
}
elsif ($test) {
CPAN::Shell->force('install', $name);
}
else {
CPAN::Shell->notest('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 _always_test {
my ($name) = @_;
return grep(lc($_) eq lc($name), ALWAYS_TEST) ? 1 : 0;
}
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) {
require Config;
$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\""
. " --install_path lib=\"$bzlib\""
. " --install_path arch=\"$bzlib/$Config::Config{archname}\"";
$CPAN::Config->{mbuild_install_arg} = $CPAN::Config->{mbuild_arg};
# 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');
=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<$test> - If true, we run tests on this module before installing,
but we still force the install if the tests fail. This is only used
when we internally install a newer CPAN module.
=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,848 +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::Install::Util qw(install_string);
use Bugzilla::Util;
use Bugzilla::Hook;
use File::Find;
use File::Path;
use File::Basename;
use File::Copy qw(move);
use IO::File;
use POSIX ();
use base qw(Exporter);
our @EXPORT = qw(
update_filesystem
create_htaccess
fix_all_file_permissions
fix_file_permissions
);
use constant HT_DEFAULT_DENY => <<EOT;
# nothing in this directory is retrievable unless overridden by an .htaccess
# in a subdirectory
deny from all
EOT
###############
# Permissions #
###############
# Used by the permissions "constants" below.
sub _suexec { Bugzilla->localconfig->{'use_suexec'} };
sub _group { Bugzilla->localconfig->{'webservergroup'} };
# Writeable by the owner only.
use constant OWNER_WRITE => 0600;
# Executable by the owner only.
use constant OWNER_EXECUTE => 0700;
# A directory which is only writeable by the owner.
use constant DIR_OWNER_WRITE => 0700;
# A cgi script that the webserver can execute.
sub WS_EXECUTE { _group() ? 0750 : 0755 };
# A file that is read by cgi scripts, but is not ever read
# directly by the webserver.
sub CGI_READ { _group() ? 0640 : 0644 };
# A file that is written to by cgi scripts, but is not ever
# read or written directly by the webserver.
sub CGI_WRITE { _group() ? 0660 : 0666 };
# A file that is served directly by the web server.
sub WS_SERVE { (_group() and !_suexec()) ? 0640 : 0644 };
# A directory whose contents can be read or served by the
# webserver (so even directories containing cgi scripts
# would have this permission).
sub DIR_WS_SERVE { (_group() and !_suexec()) ? 0750 : 0755 };
# A directory that is read by cgi scripts, but is never accessed
# directly by the webserver
sub DIR_CGI_READ { _group() ? 0750 : 0755 };
# A directory that is written to by cgi scripts, but where the
# scripts never needs to overwrite files created by other
# users.
sub DIR_CGI_WRITE { _group() ? 0770 : 01777 };
# A directory that is written to by cgi scripts, where the
# scripts need to overwrite files created by other users.
sub DIR_CGI_OVERWRITE { _group() ? 0770 : 0777 };
# This can be combined (using "|") with other permissions for
# directories that, in addition to their normal permissions (such
# as DIR_CGI_WRITE) also have content served directly from them
# (or their subdirectories) to the user, via the webserver.
sub DIR_ALSO_WS_SERVE { _suexec() ? 0001 : 0 };
# 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 there is a group the
# web server executes the scripts as, so the cgi scripts should not be writable
# by this group. Otherwise someone may find it possible to change the cgis
# when exploiting some security flaw somewhere (not necessarily in Bugzilla!)
sub FILESYSTEM {
my $datadir = bz_locations()->{'datadir'};
my $attachdir = bz_locations()->{'attachdir'};
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 $graphsdir = bz_locations()->{'graphsdir'};
# We want to set the permissions the same for all localconfig files
# across all PROJECTs, so we do something special with $localconfig,
# lower down in the permissions section.
if ($ENV{PROJECT}) {
$localconfig =~ s/\.\Q$ENV{PROJECT}\E$//;
}
# 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 => OWNER_WRITE },
# Some .pl files are WS_EXECUTE because we want
# users to be able to cron them or otherwise run
# them as a secure user, like the webserver owner.
'*.cgi' => { perms => WS_EXECUTE },
'whineatnews.pl' => { perms => WS_EXECUTE },
'collectstats.pl' => { perms => WS_EXECUTE },
'importxml.pl' => { perms => WS_EXECUTE },
'testserver.pl' => { perms => WS_EXECUTE },
'whine.pl' => { perms => WS_EXECUTE },
'email_in.pl' => { perms => WS_EXECUTE },
'sanitycheck.pl' => { perms => WS_EXECUTE },
'checksetup.pl' => { perms => OWNER_EXECUTE },
'runtests.pl' => { perms => OWNER_EXECUTE },
'jobqueue.pl' => { perms => OWNER_EXECUTE },
'migrate.pl' => { perms => OWNER_EXECUTE },
'install-module.pl' => { perms => OWNER_EXECUTE },
'Bugzilla.pm' => { perms => CGI_READ },
"$localconfig*" => { perms => CGI_READ },
'bugzilla.dtd' => { perms => WS_SERVE },
'mod_perl.pl' => { perms => WS_SERVE },
'robots.txt' => { perms => WS_SERVE },
'.htaccess' => { perms => WS_SERVE },
'contrib/README' => { perms => OWNER_WRITE },
'contrib/*/README' => { perms => OWNER_WRITE },
'docs/bugzilla.ent' => { perms => OWNER_WRITE },
'docs/makedocs.pl' => { perms => OWNER_EXECUTE },
'docs/style.css' => { perms => WS_SERVE },
'docs/*/rel_notes.txt' => { perms => WS_SERVE },
'docs/*/README.docs' => { perms => OWNER_WRITE },
"$datadir/params" => { perms => CGI_WRITE },
"$datadir/old-params.txt" => { perms => OWNER_WRITE },
"$extensionsdir/create.pl" => { perms => OWNER_EXECUTE },
"$extensionsdir/*/*.pl" => { perms => WS_EXECUTE },
);
# 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 = (
'.' => DIR_WS_SERVE,
docs => DIR_WS_SERVE,
);
# 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 => CGI_READ,
dirs => DIR_CGI_OVERWRITE },
$attachdir => { files => CGI_WRITE,
dirs => DIR_CGI_WRITE },
$webdotdir => { files => WS_SERVE,
dirs => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE },
$graphsdir => { files => WS_SERVE,
dirs => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE },
# Readable directories
"$datadir/mining" => { files => CGI_READ,
dirs => DIR_CGI_READ },
"$libdir/Bugzilla" => { files => CGI_READ,
dirs => DIR_CGI_READ },
$extlib => { files => CGI_READ,
dirs => DIR_CGI_READ },
$templatedir => { files => CGI_READ,
dirs => DIR_CGI_READ },
# Directories in the extensions/ dir are WS_SERVE so that
# the web/ directories can be served by the web server.
# But, for extra security, we deny direct webserver access to
# the lib/ and template/ directories of extensions.
$extensionsdir => { files => CGI_READ,
dirs => DIR_WS_SERVE },
"$extensionsdir/*/lib" => { files => CGI_READ,
dirs => DIR_CGI_READ },
"$extensionsdir/*/template" => { files => CGI_READ,
dirs => DIR_CGI_READ },
# Content served directly by the webserver
images => { files => WS_SERVE,
dirs => DIR_WS_SERVE },
js => { files => WS_SERVE,
dirs => DIR_WS_SERVE },
$skinsdir => { files => WS_SERVE,
dirs => DIR_WS_SERVE },
'docs/*/html' => { files => WS_SERVE,
dirs => DIR_WS_SERVE },
'docs/*/pdf' => { files => WS_SERVE,
dirs => DIR_WS_SERVE },
'docs/*/txt' => { files => WS_SERVE,
dirs => DIR_WS_SERVE },
'docs/*/images' => { files => WS_SERVE,
dirs => DIR_WS_SERVE },
"$extensionsdir/*/web" => { files => WS_SERVE,
dirs => DIR_WS_SERVE },
# Directories only for the owner, not for the webserver.
'.bzr' => { files => OWNER_WRITE,
dirs => DIR_OWNER_WRITE },
t => { files => OWNER_WRITE,
dirs => DIR_OWNER_WRITE },
xt => { files => OWNER_WRITE,
dirs => DIR_OWNER_WRITE },
'docs/lib' => { files => OWNER_WRITE,
dirs => DIR_OWNER_WRITE },
'docs/*/xml' => { files => OWNER_WRITE,
dirs => DIR_OWNER_WRITE },
'contrib' => { files => OWNER_EXECUTE,
dirs => DIR_OWNER_WRITE, },
);
# --- FILES TO CREATE --- #
# The name of each directory that we should actually *create*,
# pointing at its default permissions.
my %create_dirs = (
# This is DIR_ALSO_WS_SERVE because it contains $webdotdir.
$datadir => DIR_CGI_OVERWRITE | DIR_ALSO_WS_SERVE,
# Directories that are read-only for cgi scripts
"$datadir/mining" => DIR_CGI_READ,
"$datadir/extensions" => DIR_CGI_READ,
$extensionsdir => DIR_CGI_READ,
# Directories that cgi scripts can write to.
$attachdir => DIR_CGI_WRITE,
$graphsdir => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE,
$webdotdir => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE,
# Directories that contain content served directly by the web server.
"$skinsdir/custom" => DIR_WS_SERVE,
"$skinsdir/contrib" => DIR_WS_SERVE,
);
# The name of each file, pointing at its default permissions and
# default contents.
my %create_files = (
"$datadir/extensions/additional" => { perms => CGI_READ,
contents => '' },
# We create this file so that it always has the right owner
# and permissions. Otherwise, the webserver creates it as
# owned by itself, which can cause problems if jobqueue.pl
# or something else is not running as the webserver or root.
"$datadir/mailer.testfile" => { perms => CGI_WRITE,
contents => '' },
);
# 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_SERVE, 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.
#
# Note that these get WS_SERVE as their permission
# because they're *read* by the webserver, even though they're not
# actually, themselves, served.
my %htaccess = (
"$attachdir/.htaccess" => { perms => WS_SERVE,
contents => HT_DEFAULT_DENY },
"$libdir/Bugzilla/.htaccess" => { perms => WS_SERVE,
contents => HT_DEFAULT_DENY },
"$extlib/.htaccess" => { perms => WS_SERVE,
contents => HT_DEFAULT_DENY },
"$templatedir/.htaccess" => { perms => WS_SERVE,
contents => HT_DEFAULT_DENY },
'contrib/.htaccess' => { perms => WS_SERVE,
contents => HT_DEFAULT_DENY },
't/.htaccess' => { perms => WS_SERVE,
contents => HT_DEFAULT_DENY },
'xt/.htaccess' => { perms => WS_SERVE,
contents => HT_DEFAULT_DENY },
"$datadir/.htaccess" => { perms => WS_SERVE,
contents => HT_DEFAULT_DENY },
"$graphsdir/.htaccess" => { perms => WS_SERVE, contents => <<EOT
# Allow access to .png and .gif files.
<FilesMatch (\\.gif|\\.png)\$>
Allow from all
</FilesMatch>
# And no directory listings, either.
Deny from all
EOT
},
"$webdotdir/.htaccess" => { perms => WS_SERVE, 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
},
);
Bugzilla::Hook::process('install_filesystem', {
files => \%files,
create_dirs => \%create_dirs,
non_recurse_dirs => \%non_recurse_dirs,
recurse_dirs => \%recurse_dirs,
create_files => \%create_files,
htaccess => \%htaccess,
});
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'};
my $graphsdir = bz_locations->{'graphsdir'};
# 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 $graphsdir) {
_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 or die "mkdir $dir failed: $!";
# For some reason, passing in the permissions to "mkdir"
# doesn't work right, but doing a "chmod" does.
chmod $dirs{$dir}, $dir or warn "Cannot chmod $dir: $!";
}
}
# Move the testfile if we can't write to it, so that we can re-create
# it with the correct permissions below.
my $testfile = "$datadir/mailer.testfile";
if (-e $testfile and !-w $testfile) {
_rename_file($testfile, "$testfile.old");
}
# If old-params.txt exists in the root directory, move it to datadir.
my $oldparamsfile = "old_params.txt";
if (-e $oldparamsfile) {
_rename_file($oldparamsfile, "$datadir/$oldparamsfile");
}
_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";
}
if (-e "$datadir/duplicates") {
print "Removing duplicates directory...\n";
rmtree("$datadir/duplicates");
}
_remove_empty_css_files();
_convert_single_file_skins();
}
sub _remove_empty_css_files {
my $skinsdir = bz_locations()->{'skinsdir'};
foreach my $css_file (glob("$skinsdir/custom/*.css"),
glob("$skinsdir/contrib/*/*.css"))
{
_remove_empty_css($css_file);
}
}
# A simple helper for the update code that removes "empty" CSS files.
sub _remove_empty_css {
my ($file) = @_;
my $basename = basename($file);
my $empty_contents = <<EOT;
/*
* Custom rules for $basename.
* The rules you put here override rules in that stylesheet.
*/
EOT
if (length($empty_contents) == -s $file) {
open(my $fh, '<', $file) or warn "$file: $!";
my $file_contents;
{ local $/; $file_contents = <$fh>; }
if ($file_contents eq $empty_contents) {
print install_string('file_remove', { name => $file }), "\n";
unlink $file or warn "$file: $!";
}
};
}
# We used to allow a single css file in the skins/contrib/ directory
# to be a whole skin.
sub _convert_single_file_skins {
my $skinsdir = bz_locations()->{'skinsdir'};
foreach my $skin_file (glob "$skinsdir/contrib/*.css") {
my $dir_name = $skin_file;
$dir_name =~ s/\.css$//;
mkdir $dir_name or warn "$dir_name: $!";
_rename_file($skin_file, "$dir_name/global.css");
}
}
sub create_htaccess {
_create_files(%{FILESYSTEM()->{htaccess}});
# Repair old .htaccess files
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;
}
}
sub _rename_file {
my ($from, $to) = @_;
print install_string('file_rename', { from => $from, to => $to }), "\n";
if (-e $to) {
warn "$to already exists, not moving\n";
}
else {
move($from, $to) or warn $!;
}
}
# 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_file_permissions {
my ($file) = @_;
return if ON_WINDOWS;
my $perms = FILESYSTEM()->{all_files}->{$file}->{perms};
# Note that _get_owner_and_group is always silent here.
my ($owner_id, $group_id) = _get_owner_and_group();
_fix_perms($file, $owner_id, $group_id, $perms);
}
sub fix_all_file_permissions {
my ($output) = @_;
# _get_owner_and_group also checks that the webservergroup is valid.
my ($owner_id, $group_id) = _get_owner_and_group($output);
return if ON_WINDOWS;
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;
foreach my $dir (sort keys %dirs) {
next unless -d $dir;
_fix_perms($dir, $owner_id, $group_id, $dirs{$dir});
}
foreach my $pattern (sort keys %recurse_dirs) {
my $perms = $recurse_dirs{$pattern};
# %recurse_dirs supports globs
foreach my $dir (glob $pattern) {
next unless -d $dir;
_fix_perms_recursively($dir, $owner_id, $group_id, $perms);
}
}
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, '.');
}
sub _get_owner_and_group {
my ($output) = @_;
my $group_id = _check_web_server_group($output);
return () if ON_WINDOWS;
my $owner_id = POSIX::getuid();
$group_id = POSIX::getgid() unless defined $group_id;
return ($owner_id, $group_id);
}
# A helper for fix_all_file_permissions
sub _fix_cvs_dirs {
my ($owner_id, $dir) = @_;
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 && $_ =~ /CVS$/))
{
my $perms = 0600;
if (-d $name) {
$perms = 0700;
}
_fix_perms($name, $owner_id, $owner_gid, $perms);
}
}}, $dir);
}
sub _fix_perms {
my ($name, $owner, $group, $perms) = @_;
#printf ("Changing $name to %o\n", $perms);
# The webserver should never try to chown files.
if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
chown $owner, $group, $name
or warn install_string('chown_failed', { path => $name,
error => $! }) . "\n";
}
chmod $perms, $name
or warn install_string('chmod_failed', { path => $name,
error => $! }) . "\n";
}
sub _fix_perms_recursively {
my ($dir, $owner_id, $group_id, $perms) = @_;
# Set permissions on the directory itself.
_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);
}
sub _check_web_server_group {
my ($output) = @_;
my $group = Bugzilla->localconfig->{'webservergroup'};
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
=item C<fix_file_permissions>
Given the name of a file, its permissions will be fixed according to
how they are supposed to be set in Bugzilla's current configuration.
If it fails to set the permissions, a warning will be printed to STDERR.
=back

View File

@@ -1,515 +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{# Usually, this is the group your web server runs as.
# If you have a Windows box, ignore this setting.
# If you have use_suexec switched on below, this is the group Apache switches
# to in order to run Bugzilla scripts.
# If you do not have access to the group your scripts will run under,
# set this to "". If you do set this to "", then your Bugzilla installation
# will be _VERY_ insecure, because some files will be world readable/writable,
# and so anyone who can get local access to your machine can do whatever they
# 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 => 'use_suexec',
default => 0,
desc => <<EOT
# Set this if Bugzilla runs in an Apache SuexecUserGroup environment.
# (If your web server runs control panel software (cPanel, Plesk or similar),
# or if your Bugzilla is to run in a shared hosting environment, then you are
# almost certainly in an Apache SuexecUserGroup environment.)
# If you have a Windows box, ignore this setting.
# If set to 0, Bugzilla will set file permissions as tightly as possible.
# If set to 1, Bugzilla will set file permissions so that it may work in an
# SuexecUserGroup environment. The difference is that static files (CSS,
# JavaScript and so on) will receive world read permissions.
EOT
},
{
name => 'db_driver',
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',
# 64 characters is roughly the equivalent of a 384-bit key, which
# is larger than anybody would ever be able to brute-force.
default => sub { generate_random_password(64) },
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 (@$glob) {
$localconfig{$var} = \@$glob;
}
elsif (%$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};
my $value = $localconfig->{$name};
# Regenerate site_wide_secret if it was made by our old, weak
# generate_random_password. Previously we used to generate
# a 256-character string for site_wide_secret.
$value = undef if ($name eq 'site_wide_secret' and defined $value
and length($value) == 256);
if (!defined $value) {
push(@new_vars, $name);
$var->{default} = &{$var->{default}} if ref($var->{default}) eq 'CODE';
if (exists $answer->{$name}) {
$localconfig->{$name} = $answer->{$name};
}
else {
$localconfig->{$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,887 +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 bin_loc
extension_requirement_packages);
use List::Util qw(max);
use Safe;
use Term::ANSIColor;
# Return::Value 1.666002 pollutes the error log with warnings about this
# deprecated module. We have to set NO_CLUCK = 1 before loading Email::Send
# in have_vers() to disable these warnings.
BEGIN {
$Return::Value::NO_CLUCK = 1;
}
use base qw(Exporter);
our @EXPORT = qw(
REQUIRED_MODULES
OPTIONAL_MODULES
FEATURE_FILES
check_requirements
check_graphviz
have_vers
install_command
map_files_to_features
);
# This is how many *'s are in the top of each "box" message printed
# by checksetup.pl.
use constant TABLE_WIDTH => 71;
# Optional Apache modules that have no Perl component to them.
# If these are installed, Bugzilla has additional functionality.
#
# The keys are the names of the modules, the values are what the module
# is called in the output of "apachectl -t -D DUMP_MODULES".
use constant APACHE_MODULES => {
mod_headers => 'headers_module',
mod_env => 'env_module',
mod_expires => 'expires_module',
};
# These are all of the binaries that we could possibly use that can
# give us info about which Apache modules are installed.
# If we can't use "apachectl", the "httpd" binary itself takes the same
# parameters. Note that on Debian and Gentoo, there is an "apache2ctl",
# but it takes different parameters on each of those two distros, so we
# don't use apache2ctl.
use constant APACHE => qw(apachectl httpd apache2 apache);
# If we don't find any of the above binaries in the normal PATH,
# these are extra places we look.
use constant APACHE_PATH => [qw(
/usr/sbin
/usr/local/sbin
/usr/libexec
/usr/local/libexec
)];
# 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',
# 3.51 fixes a security problem that affects Bugzilla.
# (bug 591165)
version => '3.51',
},
{
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 => 'DBI',
module => 'DBI',
version => (vers_cmp($perl_ver, '5.13.3') > -1) ? '1.614' : '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',
# This fixes a memory leak in walk_parts that affected jobqueue.pl.
version => '1.904'
},
{
package => 'URI',
module => 'URI',
version => 0
},
{
package => 'List-MoreUtils',
module => 'List::MoreUtils',
version => 0.22,
},
);
my $extra_modules = _get_extension_requirements('REQUIRED_MODULES');
push(@modules, @$extra_modules);
return \@modules;
};
sub OPTIONAL_MODULES {
my $perl_ver = sprintf('%vd', $^V);
my @modules = (
{
package => 'GD',
module => 'GD',
version => '1.20',
feature => [qw(graphical_reports new_charts old_charts)],
},
{
package => 'Chart',
module => 'Chart::Lines',
# Versions below 2.1 cannot be detected accurately.
version => '2.1',
feature => [qw(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 => 'MIME-tools',
# MIME::Parser is packaged as MIME::Tools on ActiveState Perl
module => ON_WINDOWS ? 'MIME::Tools' : 'MIME::Parser',
version => '5.406',
feature => ['moving'],
},
{
package => 'libwww-perl',
module => 'LWP::UserAgent',
version => 0,
feature => ['updates'],
},
{
package => 'XML-Twig',
module => 'XML::Twig',
version => 0,
feature => ['moving', 'updates'],
},
{
package => 'PatchReader',
module => 'PatchReader',
version => '0.9.4',
feature => ['patch_viewer'],
},
{
package => 'perl-ldap',
module => 'Net::LDAP',
version => 0,
feature => ['auth_ldap'],
},
{
package => 'Authen-SASL',
module => 'Authen::SASL',
version => 0,
feature => ['smtp_auth'],
},
{
package => 'RadiusPerl',
module => 'Authen::Radius',
version => 0,
feature => ['auth_radius'],
},
{
package => 'SOAP-Lite',
module => 'SOAP::Lite',
# Fixes various bugs, including 542931 and 552353 + stops
# throwing warnings with Perl 5.12.
version => '0.712',
feature => ['xmlrpc'],
},
{
package => 'JSON-RPC',
module => 'JSON::RPC',
version => 0,
feature => ['jsonrpc'],
},
{
package => 'JSON-XS',
module => 'JSON::XS',
# 2.0 is the first version that will work with JSON::RPC.
version => '2.0',
feature => ['jsonrpc_faster'],
},
{
package => 'Test-Taint',
module => 'Test::Taint',
version => 0,
feature => ['jsonrpc', 'xmlrpc'],
},
{
# We need the 'utf8_mode' method of HTML::Parser, for HTML::Scrubber.
package => 'HTML-Parser',
module => 'HTML::Parser',
version => (vers_cmp($perl_ver, '5.13.3') > -1) ? '3.67' : '3.40',
feature => ['html_desc'],
},
{
package => 'HTML-Scrubber',
module => 'HTML::Scrubber',
version => 0,
feature => ['html_desc'],
},
# 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 => ['jobqueue'],
},
{
package => 'Daemon-Generic',
module => 'Daemon::Generic',
version => 0,
feature => ['jobqueue'],
},
# mod_perl
{
package => 'mod_perl',
module => 'mod_perl2',
version => '1.999022',
feature => ['mod_perl'],
},
{
package => 'Apache-SizeLimit',
module => 'Apache2::SizeLimit',
# 0.93 fixes problems on Linux and Windows, and changes the
# syntax used by SizeLimit.
version => '0.93',
feature => ['mod_perl'],
},
{
package => 'Math-Random-Secure',
module => 'Math::Random::Secure',
# This is the first version that installs properly on Windows.
version => '0.05',
feature => ['rand_security'],
},
);
if (ON_WINDOWS) {
# SizeLimit needs Win32::API to work on Windows.
push(@modules, {
package => 'Win32-API',
module => 'Win32::API',
version => 0,
feature => ['mod_perl'],
});
}
my $extra_modules = _get_extension_requirements('OPTIONAL_MODULES');
push(@modules, @$extra_modules);
return \@modules;
};
# This maps features to the files that require that feature in order
# to compile. It is used by t/001compile.t and mod_perl.pl.
use constant FEATURE_FILES => (
jsonrpc => ['Bugzilla/WebService/Server/JSONRPC.pm', 'jsonrpc.cgi'],
xmlrpc => ['Bugzilla/WebService/Server/XMLRPC.pm', 'xmlrpc.cgi',
'Bugzilla/WebService.pm', 'Bugzilla/WebService/*.pm'],
moving => ['importxml.pl'],
auth_ldap => ['Bugzilla/Auth/Verify/LDAP.pm'],
auth_radius => ['Bugzilla/Auth/Verify/RADIUS.pm'],
inbound_email => ['email_in.pl'],
jobqueue => ['Bugzilla/Job/*', 'Bugzilla/JobQueue.pm',
'Bugzilla/JobQueue/*', 'jobqueue.pl'],
patch_viewer => ['Bugzilla/Attachment/PatchReader.pm'],
updates => ['Bugzilla/Update.pm'],
);
# This implements the REQUIRED_MODULES and OPTIONAL_MODULES stuff
# described in in Bugzilla::Extension.
sub _get_extension_requirements {
my ($function) = @_;
my $packages = extension_requirement_packages();
my @modules;
foreach my $package (@$packages) {
if ($package->can($function)) {
my $extra_modules = $package->$function;
push(@modules, @$extra_modules);
}
}
return \@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);
my $missing_apache = _missing_apache_modules(APACHE_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,
apache => $missing_apache,
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;
}
sub _missing_apache_modules {
my ($modules, $output) = @_;
my $apachectl = _get_apachectl();
return [] if !$apachectl;
my $command = "$apachectl -t -D DUMP_MODULES";
my $cmd_info = `$command 2>&1`;
# If apachectl returned a value greater than 0, then there was an
# error parsing Apache's configuration, and we can't check modules.
my $retval = $?;
if ($retval > 0) {
print STDERR install_string('apachectl_failed',
{ command => $command, root => ROOT_USER }), "\n";
return [];
}
my @missing;
foreach my $module (keys %$modules) {
my $ok = _check_apache_module($module, $modules->{$module},
$cmd_info, $output);
push(@missing, $module) if !$ok;
}
return \@missing;
}
sub _get_apachectl {
foreach my $bin_name (APACHE) {
my $bin = bin_loc($bin_name);
return $bin if $bin;
}
# Try again with a possibly different path.
foreach my $bin_name (APACHE) {
my $bin = bin_loc($bin_name, APACHE_PATH);
return $bin if $bin;
}
return undef;
}
sub _check_apache_module {
my ($module, $config_name, $mod_info, $output) = @_;
my $ok;
if ($mod_info =~ /^\s+\Q$config_name\E\b/m) {
$ok = 1;
}
if ($output) {
_checking_for({ package => $module, ok => $ok });
}
return $ok;
}
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},
_translate_feature($package->{feature});
}
}
if (my @missing = @{ $check_results->{apache} }) {
print install_string('modules_message_apache');
my $missing_string = join(', ', @missing);
my $size = TABLE_WIDTH - 7;
printf "* \%-${size}s *\n", $missing_string;
my $spaces = TABLE_WIDTH - 2;
print "*", (' ' x $spaces), "*\n";
}
my $need_module_instructions =
( (!$output and @{$check_results->{missing}})
or ($output and $check_results->{any_missing}) ) ? 1 : 0;
if ($need_module_instructions or @{ $check_results->{apache} }) {
# 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 colored(install_string('commands_required'), COLOR_ERROR), "\n";
foreach my $package (@missing) {
my $command = install_command($package);
print " $command\n";
}
}
if ($output && $check_results->{any_missing} && !ON_ACTIVESTATE
&& !$check_results->{hide_all})
{
print install_string('install_all', { perl => $^X });
}
if (!$check_results->{pass}) {
print colored(install_string('installation_failed'), COLOR_ERROR),
"\n\n";
}
}
sub _translate_feature {
my $features = shift;
my @strings;
foreach my $feature (@$features) {
push(@strings, install_string("feature_$feature"));
}
return join(', ', @strings);
}
sub check_graphviz {
my ($output) = @_;
return 1 if (Bugzilla->params->{'webdotbase'} =~ /^https?:/);
my $return;
$return = 1 if -x Bugzilla->params->{'webdotbase'};
if ($output) {
_checking_for({ package => 'GraphViz', ok => $return });
}
if (!$return) {
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;";
# Don't let loading a module change the output-encoding of STDOUT
# or STDERR. (CGI.pm tries to set "binmode" on these file handles when
# it's loaded, and other modules may do the same in the future.)
Bugzilla::Install::Util::set_output_encoding();
# VERSION is provided by UNIVERSAL::, and can be called even if
# the module isn't loaded.
my $vnum = $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;
}
# CPAN did a similar thing, where it has versions like 1.9304.
if ($module eq 'CPAN' and $vnum =~ /^(\d\.\d{2})\d{2}$/) {
$vnum = $1;
}
my $vok = (vers_cmp($vnum,$wanted) > -1);
my $blacklisted;
if ($vok && $params->{blacklist}) {
$blacklisted = grep($vnum =~ /$_/, @{$params->{blacklist}});
$vok = 0 if $blacklisted;
}
if ($output) {
_checking_for({
package => $package, ok => $vok, wanted => $wanted,
found => $vnum, blacklisted => $blacklisted
});
}
return $vok ? 1 : 0;
}
sub _checking_for {
my ($params) = @_;
my ($package, $ok, $wanted, $blacklisted, $found) =
@$params{qw(package ok wanted blacklisted found)};
my $ok_string = $ok ? install_string('module_ok') : '';
# If we're actually checking versions (like for Perl modules), then
# we have some rather complex logic to determine what we want to
# show. If we're not checking versions (like for GraphViz) we just
# show "ok" or "not found".
if (exists $params->{found}) {
my $found_string;
# We do a string compare in case it's non-numeric. We make sure
# it's not a version object as negative versions are forbidden.
if ($found && !ref($found) && $found eq '-1') {
$found_string = install_string('module_not_found');
}
elsif ($found) {
$found_string = install_string('module_found', { ver => $found });
}
else {
$found_string = install_string('module_unknown_version');
}
$ok_string = $ok ? "$ok_string: $found_string" : $found_string;
}
elsif (!$ok) {
$ok_string = install_string('module_not_found');
}
my $black_string = $blacklisted ? install_string('blacklisted') : '';
my $want_string = $wanted ? "v$wanted" : install_string('any');
my $str = sprintf "%s %20s %-11s $ok_string $black_string\n",
install_string('checking_for'), $package, "($want_string)";
print $ok ? $str : colored($str, COLOR_ERROR);
}
sub install_command {
my $module = shift;
my ($command, $package);
if (ON_ACTIVESTATE) {
$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;
}
# This does a reverse mapping for FEATURE_FILES.
sub map_files_to_features {
my %features = FEATURE_FILES;
my %files;
foreach my $feature (keys %features) {
my @my_files = @{ $features{$feature} };
foreach my $pattern (@my_files) {
foreach my $file (glob $pattern) {
$files{$file} = $feature;
}
}
}
return \%files;
}
1;
__END__
=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
=item C<REQUIRED_MODULES>
An arrayref of hashrefs that describes the perl modules required by
Bugzilla. The hashes have three keys:
=over
=item C<package> - The name of the Perl package that you'd find on
CPAN for this requirement.
=item C<module> - The name of a module that can be passed to the
C<install> command in C<CPAN.pm> to install this module.
=item C<version> - The version of this module that we require, or C<0>
if any version is acceptable.
=back
=item C<OPTIONAL_MODULES>
An arrayref of hashrefs that describes the perl modules that add
additional features to Bugzilla if installed. Its hashes have all
the fields of L</REQUIRED_MODULES>, plus a C<feature> item--an arrayref
of strings that describe what features require this module.
=item C<FEATURE_FILES>
A hashref that describes what files should only be compiled if a certain
feature is enabled. The feature is the key, and the values are arrayrefs
of file names (which are passed to C<glob>, so shell patterns work).
=back
=head1 SUBROUTINES
=over 4
=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<apache> - The name of each optional Apache module that is missing.
=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 Perl 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
=item C<map_files_to_features>
Returns a hashref where file names are the keys and the value is the feature
that must be enabled in order to compile that file.
=back

View File

@@ -1,892 +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 Encode;
use ExtUtils::MM ();
use File::Basename;
use File::Spec;
use POSIX qw(setlocale LC_CTYPE);
use Safe;
use Scalar::Util qw(tainted);
use Term::ANSIColor qw(colored);
use PerlIO;
use base qw(Exporter);
our @EXPORT_OK = qw(
bin_loc
get_version_and_os
extension_code_files
extension_package_directory
extension_requirement_packages
extension_template_directory
indicate_progress
install_string
include_languages
template_include_path
vers_cmp
init_console
);
sub bin_loc {
my ($bin, $path) = @_;
my @path = $path ? @$path : File::Spec->path;
foreach my $dir (@path) {
next if !-d $dir;
my $full_path = File::Spec->catfile($dir, $bin);
# MM is an alias for ExtUtils::MM. maybe_command is nice
# because it checks .com, .bat, .exe (etc.) on Windows.
my $command = MM->maybe_command($full_path);
return $command if $command;
}
return '';
}
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 _extension_paths {
my $dir = bz_locations()->{'extensionsdir'};
my @extension_items = glob("$dir/*");
my @paths;
foreach my $item (@extension_items) {
my $basename = basename($item);
# Skip CVS directories and any hidden files/dirs.
next if ($basename eq 'CVS' or $basename =~ /^\./);
if (-d $item) {
if (!-e "$item/disabled") {
push(@paths, $item);
}
}
elsif ($item =~ /\.pm$/i) {
push(@paths, $item);
}
}
return @paths;
}
sub extension_code_files {
my ($requirements_only) = @_;
my @files;
foreach my $path (_extension_paths()) {
my @load_files;
if (-d $path) {
my $extension_file = "$path/Extension.pm";
my $config_file = "$path/Config.pm";
if (-e $extension_file) {
push(@load_files, $extension_file);
}
if (-e $config_file) {
push(@load_files, $config_file);
}
# Don't load Extension.pm if we just want Config.pm and
# we found both.
if ($requirements_only and scalar(@load_files) == 2) {
shift(@load_files);
}
}
else {
push(@load_files, $path);
}
next if !scalar(@load_files);
# We know that these paths are safe, because they came from
# extensionsdir and we checked them specifically for their format.
# Also, the only thing we ever do with them is pass them to "require".
trick_taint($_) foreach @load_files;
push(@files, \@load_files);
}
my @additional;
my $datadir = bz_locations()->{'datadir'};
my $addl_file = "$datadir/extensions/additional";
if (-e $addl_file) {
open(my $fh, '<', $addl_file) || die "$addl_file: $!";
@additional = map { trim($_) } <$fh>;
close($fh);
}
return (\@files, \@additional);
}
# Used by _get_extension_requirements in Bugzilla::Install::Requirements.
sub extension_requirement_packages {
# If we're in a .cgi script or some time that's not the requirements phase,
# just use Bugzilla->extensions. This avoids running the below code during
# a normal Bugzilla page, which is important because the below code
# doesn't actually function right if it runs after
# Bugzilla::Extension->load_all (because stuff has already been loaded).
# (This matters because almost every page calls Bugzilla->feature, which
# calls OPTIONAL_MODULES, which calls this method.)
#
# We check if Bugzilla.pm is already loaded, instead of doing a "require",
# because we *do* want the code lower down to run during the Requirements
# phase of checksetup.pl, instead of Bugzilla->extensions, and Bugzilla.pm
# actually *can* be loaded during the Requirements phase if all the
# requirements have already been installed.
if ($INC{'Bugzilla.pm'}) {
return Bugzilla->extensions;
}
my $packages = _cache()->{extension_requirement_packages};
return $packages if $packages;
$packages = [];
my %package_map;
my ($file_sets, $extra_packages) = extension_code_files('requirements only');
foreach my $file_set (@$file_sets) {
my $file = shift @$file_set;
my $name = require $file;
if ($name =~ /^\d+$/) {
die install_string('extension_must_return_name',
{ file => $file, returned => $name });
}
my $package = "Bugzilla::Extension::$name";
if ($package->can('package_dir')) {
$package->package_dir($file);
}
else {
extension_package_directory($package, $file);
}
$package_map{$file} = $package;
push(@$packages, $package);
}
foreach my $package (@$extra_packages) {
eval("require $package") || die $@;
push(@$packages, $package);
}
_cache()->{extension_requirement_packages} = $packages;
# Used by Bugzilla::Extension->load if it's called after this method
# (which only happens during checksetup.pl, currently).
_cache()->{extension_requirement_package_map} = \%package_map;
return $packages;
}
# Used in this file and in Bugzilla::Extension.
sub extension_template_directory {
my $extension = shift;
my $class = ref($extension) || $extension;
my $base_dir = extension_package_directory($class);
if ($base_dir eq bz_locations->{'extensionsdir'}) {
return bz_locations->{'templatedir'};
}
return "$base_dir/template";
}
# For extensions that are in the extensions/ dir, this both sets and fetches
# the name of the directory that stores an extension's "stuff". We need this
# when determining the template directory for extensions (or other things
# that are relative to the extension's base directory).
sub extension_package_directory {
my ($invocant, $file) = @_;
my $class = ref($invocant) || $invocant;
# $file is set on the first invocation, store the value in the extension's
# package for retrieval on subsequent calls
my $var;
{
no warnings 'once';
no strict 'refs';
$var = \${"${class}::EXTENSION_PACKAGE_DIR"};
}
if ($file) {
$$var = dirname($file);
}
my $value = $$var;
# This is for extensions loaded from data/extensions/additional.
if (!$value) {
my $short_path = $class;
$short_path =~ s/::/\//g;
$short_path .= ".pm";
my $long_path = $INC{$short_path};
die "$short_path is not in \%INC" if !$long_path;
$value = $long_path;
$value =~ s/\.pm//;
}
return $value;
}
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()->{install_string_path} ||= template_include_path();
my $path = _cache()->{install_string_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;
utf8::decode($string_template) if !utf8::is_utf8($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 _wanted_languages {
my ($requested, @wanted);
# Checking SERVER_SOFTWARE is the same as i_am_cgi() in Bugzilla::Util.
if (exists $ENV{'SERVER_SOFTWARE'}) {
my $cgi = Bugzilla->cgi;
$requested = $cgi->http('Accept-Language') || '';
my $lang = $cgi->cookie('LANG');
push(@wanted, $lang) if $lang;
}
else {
$requested = get_console_locale();
}
push(@wanted, _sort_accept_language($requested));
return \@wanted;
}
sub _wanted_to_actual_languages {
my ($wanted, $supported) = @_;
my @actual;
foreach my $lang (@$wanted) {
# If we support the language we want, or *any version* of
# the language we want, it gets pushed into @actual.
#
# 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)
my @found = grep(/^\Q$lang\E(-.+)?$/i, @$supported);
push(@actual, @found) if @found;
}
# We always include English at the bottom if it's not there, even if
# it wasn't selected by the user.
if (!grep($_ eq 'en', @actual)) {
push(@actual, 'en');
}
return \@actual;
}
sub supported_languages {
my $cache = _cache();
return $cache->{supported_languages} if $cache->{supported_languages};
my @dirs = glob(bz_locations()->{'templatedir'} . "/*");
my @languages;
foreach my $dir (@dirs) {
# It's a language directory only if it contains "default" or
# "custom". This auto-excludes CVS directories as well.
next if (!-d "$dir/default" and !-d "$dir/custom");
my $lang = basename($dir);
# Check for language tag format conforming to RFC 1766.
next unless $lang =~ /^[a-zA-Z]{1,8}(-[a-zA-Z]{1,8})?$/;
push(@languages, $lang);
}
$cache->{supported_languages} = \@languages;
return \@languages;
}
sub include_languages {
my ($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.
my $wanted;
if ($params->{language}) {
# We can pass several languages at once as an arrayref
# or a single language.
$wanted = $params->{language};
$wanted = [$wanted] unless ref $wanted;
}
else {
$wanted = _wanted_languages();
}
my $supported = supported_languages();
my $actual = _wanted_to_actual_languages($wanted, $supported);
return @$actual;
}
# Used by template_include_path
sub _template_lang_directories {
my ($languages, $templatedir) = @_;
my @add = qw(custom default);
my $project = bz_locations->{'project'};
unshift(@add, $project) if $project;
my @result;
foreach my $lang (@$languages) {
foreach my $dir (@add) {
my $full_dir = "$templatedir/$lang/$dir";
if (-d $full_dir) {
trick_taint($full_dir);
push(@result, $full_dir);
}
}
}
return @result;
}
# Used by template_include_path.
sub _template_base_directories {
# 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.
#
# We use extension_requirement_packages instead of Bugzilla->extensions
# because this fucntion is called during the requirements phase of
# installation (so Bugzilla->extensions isn't available).
my $extensions = extension_requirement_packages();
my @template_dirs;
foreach my $extension (@$extensions) {
my $dir;
# If there's a template_dir method available in the extension
# package, then call it. Note that this has to be defined in
# Config.pm for extensions that have a Config.pm, to be effective
# during the Requirements phase of checksetup.pl.
if ($extension->can('template_dir')) {
$dir = $extension->template_dir;
}
else {
$dir = extension_template_directory($extension);
}
if (-d $dir) {
push(@template_dirs, $dir);
}
}
# Extensions may also contain *only* templates, in which case they
# won't show up in extension_requirement_packages.
foreach my $path (_extension_paths()) {
next if !-d $path;
if (!-e "$path/Extension.pm" and !-e "$path/Config.pm"
and -d "$path/template")
{
push(@template_dirs, "$path/template");
}
}
push(@template_dirs, bz_locations()->{'templatedir'});
return \@template_dirs;
}
sub template_include_path {
my ($params) = @_;
my @used_languages = include_languages($params);
# Now, we add template directories in the order they will be searched:
my $template_dirs = _template_base_directories();
my @include_path;
foreach my $template_dir (@$template_dirs) {
my @lang_dirs = _template_lang_directories(\@used_languages,
$template_dir);
# Hooks get each set of extension directories separately.
if ($params->{hook}) {
push(@include_path, \@lang_dirs);
}
# Whereas everything else just gets a whole INCLUDE_PATH.
else {
push(@include_path, @lang_dirs);
}
}
return \@include_path;
}
# 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};
}
# 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;
}
sub set_output_encoding {
# If we've already set an encoding layer on STDOUT, don't
# add another one.
my @stdout_layers = PerlIO::get_layers(STDOUT);
return if grep(/^encoding/, @stdout_layers);
my $encoding;
if (ON_WINDOWS and eval { require Win32::Console }) {
# Although setlocale() works on Windows, it doesn't always return
# the current *console's* encoding. So we use OutputCP here instead,
# when we can.
$encoding = Win32::Console::OutputCP();
}
else {
my $locale = setlocale(LC_CTYPE);
if ($locale =~ /\.([^\.]+)$/) {
$encoding = $1;
}
}
$encoding = "cp$encoding" if ON_WINDOWS;
$encoding = Encode::resolve_alias($encoding) if $encoding;
if ($encoding and $encoding !~ /utf-8/i) {
binmode STDOUT, ":encoding($encoding)";
binmode STDERR, ":encoding($encoding)";
}
else {
binmode STDOUT, ':utf8';
binmode STDERR, ':utf8';
}
}
sub init_console {
eval { ON_WINDOWS && require Win32::Console::ANSI; };
$ENV{'ANSI_COLORS_DISABLED'} = 1 if ($@ || !-t *STDOUT);
$SIG{__DIE__} = \&_console_die;
prevent_windows_dialog_boxes();
set_output_encoding();
}
sub _console_die {
my ($message) = @_;
# $^S means "we are in an eval"
if ($^S) {
die $message;
}
# Remove newlines from the message before we color it, and then
# add them back in on display. Otherwise the ANSI escape code
# for resetting the color comes after the newline, and Perl thinks
# that it should put "at Bugzilla/Install.pm line 1234" after the
# message.
$message =~ s/\n+$//;
# We put quotes around the message to stringify any object exceptions,
# like Template::Exception.
die colored("$message", COLOR_ERROR) . "\n";
}
sub prevent_windows_dialog_boxes {
# This code comes from http://bugs.activestate.com/show_bug.cgi?id=82183
# and prevents Perl modules from popping up dialog boxes, particularly
# during checksetup (since loading DBD::Oracle during checksetup when
# Oracle isn't installed causes a scary popup and pauses checksetup).
#
# Win32::API ships with ActiveState by default, though there could
# theoretically be a Windows installation without it, I suppose.
if (ON_WINDOWS and eval { require Win32::API }) {
# Call kernel32.SetErrorMode with arguments that mean:
# "The system does not display the critical-error-handler message box.
# Instead, the system sends the error to the calling process." and
# "A child process inherits the error mode of its parent process."
my $SetErrorMode = Win32::API->new('kernel32', 'SetErrorMode',
'I', 'I');
my $SEM_FAILCRITICALERRORS = 0x0001;
my $SEM_NOGPFAULTERRORBOX = 0x0002;
$SetErrorMode->Call($SEM_FAILCRITICALERRORS | $SEM_NOGPFAULTERRORBOX);
}
}
# This is like request_cache, but it's used only by installation code
# for checksetup.pl and things like that.
our $_cache = {};
sub _cache {
# If the normal request_cache is available (which happens any time
# after the requirements phase) then we should use that.
if (eval { Bugzilla->request_cache; }) {
return Bugzilla->request_cache;
}
return $_cache;
}
###############################
# 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]));
}
sub trim {
my ($str) = @_;
if ($str) {
$str =~ s/^\s+//g;
$str =~ s/\s+$//g;
}
return $str;
}
__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<init_console>
Sets the C<ANSI_COLORS_DISABLED> and C<HTTP_ACCEPT_LANGUAGE> environment variables.
=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 ($class, $num_retries) = @_;
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,110 +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);
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 (!Bugzilla->feature('jobqueue')) {
ThrowCodeError('feature_disabled', { feature => 'jobqueue' });
}
my $lc = Bugzilla->localconfig;
# We need to use the main DB as TheSchwartz module is going
# to write to it.
my $self = $class->SUPER::new(
databases => [{
dsn => Bugzilla->dbh_main->{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,228 +0,0 @@
# -*- Mode: perl; indent-tabs-mode: nil -*-
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is 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 Cwd qw(abs_path);
use File::Basename;
use File::Copy;
use Pod::Usage;
use Bugzilla::Constants;
use Bugzilla::JobQueue;
use Bugzilla::Util qw(get_text);
BEGIN { eval "use base qw(Daemon::Generic)"; }
our $VERSION = BUGZILLA_VERSION;
# Info we need to install/uninstall the daemon.
our $chkconfig = "/sbin/chkconfig";
our $initd = "/etc/init.d";
our $initscript = "bugzilla-queue";
# The Daemon::Generic docs say that it uses all sorts of
# things from gd_preconfig, but in fact it does not. The
# only thing it uses from gd_preconfig is the "pidfile"
# config parameter.
sub gd_preconfig {
my $self = shift;
my $pidfile = $self->{gd_args}{pidfile};
if (!$pidfile) {
$pidfile = bz_locations()->{datadir} . '/' . $self->{gd_progname}
. ".pid";
}
return (pidfile => $pidfile);
}
# All config other than the pidfile has to be done in gd_getopt
# in order for it to be set up early enough.
sub gd_getopt {
my $self = shift;
$self->SUPER::gd_getopt();
if ($self->{gd_args}{progname}) {
$self->{gd_progname} = $self->{gd_args}{progname};
}
else {
$self->{gd_progname} = basename($0);
}
# There are places that Daemon Generic's new() uses $0 instead of
# gd_progname, which it really shouldn't, but this hack fixes it.
$self->{_original_zero} = $0;
$0 = $self->{gd_progname};
}
sub gd_postconfig {
my $self = shift;
# See the hack above in gd_getopt. This just reverses it
# in case anything else needs the accurate $0.
$0 = delete $self->{_original_zero};
}
sub gd_more_opt {
my $self = shift;
return (
'pidfile=s' => \$self->{gd_args}{pidfile},
'n=s' => \$self->{gd_args}{progname},
);
}
sub gd_usage {
pod2usage({ -verbose => 0, -exitval => 'NOEXIT' });
return 0
}
sub gd_can_install {
my $self = shift;
my $source_file;
if ( -e "/etc/SuSE-release" ) {
$source_file = "contrib/$initscript.suse";
} else {
$source_file = "contrib/$initscript.rhel";
}
my $dest_file = "$initd/$initscript";
my $sysconfig = '/etc/sysconfig';
my $config_file = "$sysconfig/$initscript";
if (!-x $chkconfig or !-d $initd) {
return $self->SUPER::gd_can_install(@_);
}
return sub {
if (!-w $initd) {
print "You must run the 'install' command as root.\n";
return;
}
if (-e $dest_file) {
print "$initscript already in $initd.\n";
}
else {
copy($source_file, $dest_file)
or die "Could not copy $source_file to $dest_file: $!";
chmod(0755, $dest_file)
or die "Could not change permissions on $dest_file: $!";
}
system($chkconfig, '--add', $initscript);
print "$initscript installed.",
" To start the daemon, do \"$dest_file start\" as root.\n";
if (-d $sysconfig and -w $sysconfig) {
if (-e $config_file) {
print "$config_file already exists.\n";
return;
}
open(my $config_fh, ">", $config_file)
or die "Could not write to $config_file: $!";
my $directory = abs_path(dirname($self->{_original_zero}));
my $owner_id = (stat $self->{_original_zero})[4];
my $owner = getpwuid($owner_id);
print $config_fh <<END;
#!/bin/sh
BUGZILLA="$directory"
USER=$owner
END
close($config_fh);
}
else {
print "Please edit $dest_file to configure the daemon.\n";
}
}
}
sub gd_can_uninstall {
my $self = shift;
if (-x $chkconfig and -d $initd) {
return sub {
if (!-e "$initd/$initscript") {
print "$initscript not installed.\n";
return;
}
system($chkconfig, '--del', $initscript);
print "$initscript disabled.",
" To stop it, run: $initd/$initscript stop\n";
}
}
return $self->SUPER::gd_can_install(@_);
}
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_setup_signals {
my $self = shift;
$self->SUPER::gd_setup_signals();
$SIG{TERM} = sub { $self->gd_quit_event(); }
}
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,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.
#
# 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 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 get_all_with_bug_count {
my $class = shift;
my $dbh = Bugzilla->dbh;
my $keywords =
$dbh->selectall_arrayref('SELECT '
. join(', ', $class->_get_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);
if (!defined $name or $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);
if (!defined $desc or $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 $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<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,226 +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;
# Return::Value 1.666002 pollutes the error log with warnings about this
# deprecated module. We have to set NO_CLUCK = 1 before loading Email::Send
# to disable these warnings.
BEGIN {
$Return::Value::NO_CLUCK = 1;
}
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,
# even if the admin changes the "ssl_redirect" parameter some day.
$email->header_set('X-Bugzilla-URL', Bugzilla->params->{'urlbase'});
# We add this header to mark the mail as "auto-generated" and
# 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;
}
}
}
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, %d %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, mailer_args => \@args });
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 {
my $rand_bits = generate_random_password(10);
$threadingmarker = "Message-ID: <bug-$bug_id-$user_id-$rand_bits$sitespec>" .
"\nIn-Reply-To: <bug-$bug_id-$user_id$sitespec>" .
"\nReferences: <bug-$bug_id-$user_id$sitespec>";
}
return $threadingmarker;
}
1;

File diff suppressed because it is too large Load Diff

View File

@@ -1,712 +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 Migration Tool.
#
# The Initial Developer of the Original Code is Lambda Research
# Corporation. Portions created by the Initial Developer are Copyright
# (C) 2009 the Initial Developer. All Rights Reserved.
#
# Contributor(s):
# Max Kanat-Alexander <mkanat@bugzilla.org>
package Bugzilla::Migrate::Gnats;
use strict;
use base qw(Bugzilla::Migrate);
use Bugzilla::Constants;
use Bugzilla::Install::Util qw(indicate_progress);
use Bugzilla::Util qw(format_time trim generate_random_password);
use Email::Address;
use Email::MIME;
use File::Basename;
use IO::File;
use List::MoreUtils qw(firstidx);
use List::Util qw(first);
use constant REQUIRED_MODULES => [
{
package => 'Email-Simple-FromHandle',
module => 'Email::Simple::FromHandle',
# This version added seekable handles.
version => 0.050,
},
];
use constant FIELD_MAP => {
'Number' => 'bug_id',
'Category' => 'product',
'Synopsis' => 'short_desc',
'Responsible' => 'assigned_to',
'State' => 'bug_status',
'Class' => 'cf_type',
'Classification' => '',
'Originator' => 'reporter',
'Arrival-Date' => 'creation_ts',
'Last-Modified' => 'delta_ts',
'Release' => 'version',
'Severity' => 'bug_severity',
'Description' => 'comment',
};
use constant VALUE_MAP => {
bug_severity => {
'serious' => 'major',
'cosmetic' => 'trivial',
'new-feature' => 'enhancement',
'non-critical' => 'normal',
},
bug_status => {
'open' => 'CONFIRMED',
'analyzed' => 'IN_PROGRESS',
'suspended' => 'RESOLVED',
'feedback' => 'RESOLVED',
'released' => 'VERIFIED',
},
bug_status_resolution => {
'feedback' => 'FIXED',
'released' => 'FIXED',
'closed' => 'FIXED',
'suspended' => 'LATER',
},
priority => {
'medium' => 'Normal',
},
};
use constant GNATS_CONFIG_VARS => (
{
name => 'gnats_path',
default => '/var/lib/gnats',
desc => <<END,
# The path to the directory that contains the GNATS database.
END
},
{
name => 'default_email_domain',
default => 'example.com',
desc => <<'END',
# Some GNATS users do not have full email addresses, but Bugzilla requires
# every user to have an email address. What domain should be appended to
# usernames that don't have emails, to make them into email addresses?
# (For example, if you leave this at the default, "unknown" would become
# "unknown@example.com".)
END
},
{
name => 'component_name',
default => 'General',
desc => <<'END',
# GNATS has only "Category" to classify bugs. However, Bugzilla has a
# multi-level system of Products that contain Components. When importing
# GNATS categories, they become a Product with one Component. What should
# the name of that Component be?
END
},
{
name => 'version_regex',
default => '',
desc => <<'END',
# In GNATS, the "version" field can contain almost anything. However, in
# Bugzilla, it's a drop-down, so you don't want too many choices in there.
# If you specify a regular expression here, versions will be tested against
# this regular expression, and if they match, the first match (the first set
# of parentheses in the regular expression, also called "$1") will be used
# as the version value for the bug instead of the full version value specified
# in GNATS.
END
},
{
name => 'default_originator',
default => 'gnats-admin',
desc => <<'END',
# Sometimes, a PR has no valid Originator, so we fall back to the From
# header of the email. If the From header also isn't a valid username
# (is just a name with spaces in it--we can't convert that to an email
# address) then this username (which can either be a GNATS username or an
# email address) will be considered to be the Originator of the PR.
END
}
);
sub CONFIG_VARS {
my $self = shift;
my @vars = (GNATS_CONFIG_VARS, $self->SUPER::CONFIG_VARS);
my $field_map = first { $_->{name} eq 'translate_fields' } @vars;
$field_map->{default} = FIELD_MAP;
my $value_map = first { $_->{name} eq 'translate_values' } @vars;
$value_map->{default} = VALUE_MAP;
return @vars;
}
# Directories that aren't projects, or that we shouldn't be parsing
use constant SKIP_DIRECTORIES => qw(
gnats-adm
gnats-queue
pending
);
use constant NON_COMMENT_FIELDS => qw(
Audit-Trail
Closed-Date
Confidential
Unformatted
attachments
);
# Certain fields can contain things that look like fields in them,
# because they might contain quoted emails. To avoid mis-parsing,
# we list out here the exact order of fields at the end of a PR
# and wait for the next field to consider that we actually have
# a field to parse.
use constant END_FIELD_ORDER => qw(
Description
How-To-Repeat
Fix
Release-Note
Audit-Trail
Unformatted
);
use constant CUSTOM_FIELDS => {
cf_type => {
type => FIELD_TYPE_SINGLE_SELECT,
description => 'Type',
},
};
use constant FIELD_REGEX => qr/^>(\S+):\s*(.*)$/;
# Used for bugs that have no Synopsis.
use constant NO_SUBJECT => "(no subject)";
# This is the divider that GNATS uses between attachments in its database
# files. It's missign two hyphens at the beginning because MIME Emails use
# -- to start boundaries.
use constant GNATS_BOUNDARY => '----gnatsweb-attachment----';
use constant LONG_VERSION_LENGTH => 32;
#########
# Hooks #
#########
sub before_insert {
my $self = shift;
# gnats_id isn't a valid User::create field, and we don't need it
# anymore now.
delete $_->{gnats_id} foreach @{ $self->users };
# Grab a version out of a bug for each product, so that there is a
# valid "version" argument for Bugzilla::Product->create.
foreach my $product (@{ $self->products }) {
my $bug = first { $_->{product} eq $product->{name} and $_->{version} }
@{ $self->bugs };
if (defined $bug) {
$product->{version} = $bug->{version};
}
else {
$product->{version} = 'unspecified';
}
}
}
#########
# Users #
#########
sub _read_users {
my $self = shift;
my $path = $self->config('gnats_path');
my $file = "$path/gnats-adm/responsible";
$self->debug("Reading users from $file");
my $default_domain = $self->config('default_email_domain');
open(my $users_fh, '<', $file) || die "$file: $!";
my @users;
foreach my $line (<$users_fh>) {
$line = trim($line);
next if $line =~ /^#/;
my ($id, $name, $email) = split(':', $line, 3);
$email ||= "$id\@$default_domain";
# We can't call our own translate_value, because that depends on
# the existence of user_map, which doesn't exist until after
# this method. However, we still want to translate any users found.
$email = $self->SUPER::translate_value('user', $email);
push(@users, { realname => $name, login_name => $email,
gnats_id => $id });
}
close($users_fh);
return \@users;
}
sub user_map {
my $self = shift;
$self->{user_map} ||= { map { $_->{gnats_id} => $_->{login_name} }
@{ $self->users } };
return $self->{user_map};
}
sub add_user {
my ($self, $id, $email) = @_;
return if defined $self->user_map->{$id};
$self->user_map->{$id} = $email;
push(@{ $self->users }, { login_name => $email, gnats_id => $id });
}
sub user_to_email {
my ($self, $value) = @_;
if (defined $self->user_map->{$value}) {
$value = $self->user_map->{$value};
}
elsif ($value !~ /@/) {
my $domain = $self->config('default_email_domain');
$value = "$value\@$domain";
}
return $value;
}
############
# Products #
############
sub _read_products {
my $self = shift;
my $path = $self->config('gnats_path');
my $file = "$path/gnats-adm/categories";
$self->debug("Reading categories from $file");
open(my $categories_fh, '<', $file) || die "$file: $!";
my @products;
foreach my $line (<$categories_fh>) {
$line = trim($line);
next if $line =~ /^#/;
my ($name, $description, $assigned_to, $cc) = split(':', $line, 4);
my %product = ( name => $name, description => $description );
my @initial_cc = split(',', $cc);
@initial_cc = @{ $self->translate_value('user', \@initial_cc) };
$assigned_to = $self->translate_value('user', $assigned_to);
my %component = ( name => $self->config('component_name'),
description => $description,
initialowner => $assigned_to,
initial_cc => \@initial_cc );
$product{components} = [\%component];
push(@products, \%product);
}
close($categories_fh);
return \@products;
}
################
# Reading Bugs #
################
sub _read_bugs {
my $self = shift;
my $path = $self->config('gnats_path');
my @directories = glob("$path/*");
my @bugs;
foreach my $directory (@directories) {
next if !-d $directory;
my $name = basename($directory);
next if grep($_ eq $name, SKIP_DIRECTORIES);
push(@bugs, @{ $self->_parse_project($directory) });
}
@bugs = sort { $a->{Number} <=> $b->{Number} } @bugs;
return \@bugs;
}
sub _parse_project {
my ($self, $directory) = @_;
my @files = glob("$directory/*");
$self->debug("Reading Project: $directory");
# Sometimes other files get into gnats directories.
@files = grep { basename($_) =~ /^\d+$/ } @files;
my @bugs;
my $count = 1;
my $total = scalar @files;
print basename($directory) . ":\n";
foreach my $file (@files) {
push(@bugs, $self->_parse_bug_file($file));
if (!$self->verbose) {
indicate_progress({ current => $count++, every => 5,
total => $total });
}
}
return \@bugs;
}
sub _parse_bug_file {
my ($self, $file) = @_;
$self->debug("Reading $file");
open(my $fh, "<", $file) || die "$file: $!";
my $email = Email::Simple::FromHandle->new($fh);
my $fields = $self->_get_gnats_field_data($email);
# We parse attachments here instead of during translate_bug,
# because otherwise we'd be taking up huge amounts of memory storing
# all the raw attachment data in memory.
$fields->{attachments} = $self->_parse_attachments($fields);
close($fh);
return $fields;
}
sub _get_gnats_field_data {
my ($self, $email) = @_;
my ($current_field, @value_lines, %fields);
$email->reset_handle();
my $handle = $email->handle;
foreach my $line (<$handle>) {
# If this line starts a field name
if ($line =~ FIELD_REGEX) {
my ($new_field, $rest_of_line) = ($1, $2);
# If this is one of the last few PR fields, then make sure
# that we're getting our fields in the right order.
my $new_field_valid = 1;
my $search_for = $current_field || '';
my $current_field_pos = firstidx { $_ eq $search_for }
END_FIELD_ORDER;
if ($current_field_pos > -1) {
my $new_field_pos = firstidx { $_ eq $new_field }
END_FIELD_ORDER;
# We accept any field, as long as it's later than this one.
$new_field_valid = $new_field_pos > $current_field_pos ? 1 : 0;
}
if ($new_field_valid) {
if ($current_field) {
$fields{$current_field} = _handle_lines(\@value_lines);
@value_lines = ();
}
$current_field = $new_field;
$line = $rest_of_line;
}
}
push(@value_lines, $line) if defined $line;
}
$fields{$current_field} = _handle_lines(\@value_lines);
$fields{cc} = [$email->header('Cc')] if $email->header('Cc');
# If the Originator is invalid and we don't have a translation for it,
# use the From header instead.
my $originator = $self->translate_value('reporter', $fields{Originator},
{ check_only => 1 });
if ($originator !~ Bugzilla->params->{emailregexp}) {
# We use the raw header sometimes, because it looks like "From: user"
# which Email::Address won't parse but we can still use.
my $address = $email->header('From');
my ($parsed) = Email::Address->parse($address);
if ($parsed) {
$address = $parsed->address;
}
if ($address) {
$self->debug(
"PR $fields{Number} had an Originator that was not a valid"
. " user ($fields{Originator}). Using From ($address)"
. " instead.\n");
my $address_email = $self->translate_value('reporter', $address,
{ check_only => 1 });
if ($address_email !~ Bugzilla->params->{emailregexp}) {
$self->debug(" From was also invalid, using default_originator.\n");
$address = $self->config('default_originator');
}
$fields{Originator} = $address;
}
}
$self->debug(\%fields, 3);
return \%fields;
}
sub _handle_lines {
my ($lines) = @_;
my $value = join('', @$lines);
$value =~ s/\s+$//;
return $value;
}
####################
# Translating Bugs #
####################
sub translate_bug {
my ($self, $fields) = @_;
my ($bug, $other_fields) = $self->SUPER::translate_bug($fields);
$bug->{attachments} = delete $other_fields->{attachments};
if (defined $other_fields->{_add_to_comment}) {
$bug->{comment} .= delete $other_fields->{_add_to_comment};
}
my ($changes, $extra_comment) =
$self->_parse_audit_trail($bug, $other_fields->{'Audit-Trail'});
my @comments;
foreach my $change (@$changes) {
if (exists $change->{comment}) {
push(@comments, {
thetext => $change->{comment},
who => $change->{who},
bug_when => $change->{bug_when} });
delete $change->{comment};
}
}
$bug->{history} = $changes;
if (trim($extra_comment)) {
push(@comments, { thetext => $extra_comment, who => $bug->{reporter},
bug_when => $bug->{delta_ts} || $bug->{creation_ts} });
}
$bug->{comments} = \@comments;
$bug->{component} = $self->config('component_name');
if (!$bug->{short_desc}) {
$bug->{short_desc} = NO_SUBJECT;
}
foreach my $attachment (@{ $bug->{attachments} || [] }) {
$attachment->{submitter} = $bug->{reporter};
$attachment->{creation_ts} = $bug->{creation_ts};
}
$self->debug($bug, 3);
return $bug;
}
sub _parse_audit_trail {
my ($self, $bug, $audit_trail) = @_;
return [] if !trim($audit_trail);
$self->debug(" Parsing audit trail...", 2);
if ($audit_trail !~ /^\S+-Changed-\S+:/ms) {
# This is just a comment from the bug's creator.
$self->debug(" Audit trail is just a comment.", 2);
return ([], $audit_trail);
}
my (@changes, %current_data, $current_column, $on_why);
my $extra_comment = '';
my $current_field;
my @all_lines = split("\n", $audit_trail);
foreach my $line (@all_lines) {
# GNATS history looks like:
# Status-Changed-From-To: open->closed
# Status-Changed-By: jack
# Status-Changed-When: Mon May 12 14:46:59 2003
# Status-Changed-Why:
# This is some comment here about the change.
if ($line =~ /^(\S+)-Changed-(\S+):(.*)/) {
my ($field, $column, $value) = ($1, $2, $3);
my $bz_field = $self->translate_field($field);
# If it's not a field we're importing, we don't care about
# its history.
next if !$bz_field;
# GNATS doesn't track values for description changes,
# unfortunately, and that's the only information we'd be able to
# use in Bugzilla for the audit trail on that field.
next if $bz_field eq 'comment';
$current_field = $bz_field if !$current_field;
if ($bz_field ne $current_field) {
$self->_store_audit_change(
\@changes, $current_field, \%current_data);
%current_data = ();
$current_field = $bz_field;
}
$value = trim($value);
$self->debug(" $bz_field $column: $value", 3);
if ($column eq 'From-To') {
my ($from, $to) = split('->', $value, 2);
# Sometimes there's just a - instead of a -> between the values.
if (!defined($to)) {
($from, $to) = split('-', $value, 2);
}
$current_data{added} = $to;
$current_data{removed} = $from;
}
elsif ($column eq 'By') {
my $email = $self->translate_value('user', $value);
# Sometimes we hit users in the audit trail that we haven't
# seen anywhere else.
$current_data{who} = $email;
}
elsif ($column eq 'When') {
$current_data{bug_when} = $self->parse_date($value);
}
if ($column eq 'Why') {
$value = '' if !defined $value;
$current_data{comment} = $value;
$on_why = 1;
}
else {
$on_why = 0;
}
}
elsif ($on_why) {
# "Why" lines are indented four characters.
$line =~ s/^\s{4}//;
$current_data{comment} .= "$line\n";
}
else {
$self->debug(
"Extra Audit-Trail line on $bug->{product} $bug->{bug_id}:"
. " $line\n", 2);
$extra_comment .= "$line\n";
}
}
$self->_store_audit_change(\@changes, $current_field, \%current_data);
return (\@changes, $extra_comment);
}
sub _store_audit_change {
my ($self, $changes, $old_field, $current_data) = @_;
$current_data->{field} = $old_field;
$current_data->{removed} =
$self->translate_value($old_field, $current_data->{removed});
$current_data->{added} =
$self->translate_value($old_field, $current_data->{added});
push(@$changes, { %$current_data });
}
sub _parse_attachments {
my ($self, $fields) = @_;
my $unformatted = delete $fields->{'Unformatted'};
my $gnats_boundary = GNATS_BOUNDARY;
# A sanity checker to make sure that we're parsing attachments right.
my $num_attachments = 0;
$num_attachments++ while ($unformatted =~ /\Q$gnats_boundary\E/g);
# Sometimes there's a GNATS_BOUNDARY that is on the same line as other data.
$unformatted =~ s/(\S\s*)\Q$gnats_boundary\E$/$1\n$gnats_boundary/mg;
# Often the "Unformatted" section starts with stuff before
# ----gnatsweb-attachment---- that isn't necessary.
$unformatted =~ s/^\s*From:.+?Reply-to:[^\n]+//s;
$unformatted = trim($unformatted);
return [] if !$unformatted;
$self->debug('Reading attachments...', 2);
my $boundary = generate_random_password(48);
$unformatted =~ s/\Q$gnats_boundary\E/--$boundary/g;
# Sometimes the whole Unformatted section is indented by exactly
# one space, and needs to be fixed.
if ($unformatted =~ /--\Q$boundary\E\n /) {
$unformatted =~ s/^ //mg;
}
$unformatted = <<END;
From: nobody
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="$boundary"
This is a multi-part message in MIME format.
--$boundary
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 7bit
$unformatted
--$boundary--
END
my $email = new Email::MIME(\$unformatted);
my @parts = $email->parts;
# Remove the fake body.
my $part1 = shift @parts;
if ($part1->body) {
$self->debug(" Additional Unformatted data found on "
. $fields->{Category} . " bug " . $fields->{Number});
$self->debug($part1->body, 3);
$fields->{_add_comment} .= "\n\nUnformatted:\n" . $part1->body;
}
my @attachments;
foreach my $part (@parts) {
$self->debug(' Parsing attachment: ' . $part->filename);
my $temp_fh = IO::File->new_tmpfile or die ("Can't create tempfile: $!");
$temp_fh->binmode;
print $temp_fh $part->body;
my $content_type = $part->content_type;
$content_type =~ s/; name=.+$//;
my $attachment = { filename => $part->filename,
description => $part->filename,
mimetype => $content_type,
data => $temp_fh };
$self->debug($attachment, 3);
push(@attachments, $attachment);
}
if (scalar(@attachments) ne $num_attachments) {
warn "WARNING: Expected $num_attachments attachments but got "
. scalar(@attachments) . "\n" ;
$self->debug($unformatted, 3);
}
return \@attachments;
}
sub translate_value {
my $self = shift;
my ($field, $value, $options) = @_;
my $original_value = $value;
$options ||= {};
if (!ref($value) and grep($_ eq $field, $self->USER_FIELDS)) {
if ($value =~ /(\S+\@\S+)/) {
$value = $1;
$value =~ s/^<//;
$value =~ s/>$//;
}
else {
# Sometimes names have extra stuff on the end like "(Somebody's Name)"
$value =~ s/\s+\(.+\)$//;
# Sometimes user fields look like "(user)" instead of just "user".
$value =~ s/^\((.+)\)$/$1/;
$value = trim($value);
}
}
if ($field eq 'version' and $value ne '') {
my $version_re = $self->config('version_regex');
if ($version_re and $value =~ $version_re) {
$value = $1;
}
# In the GNATS that I tested this with, there were many extremely long
# values for "version" that caused some import problems (they were
# longer than the max allowed version value). So if the version value
# is longer than 32 characters, pull out the first thing that looks
# like a version number.
elsif (length($value) > LONG_VERSION_LENGTH) {
$value =~ s/^.+?\b(\d[\w\.]+)\b.+$/$1/;
}
}
my @args = @_;
$args[1] = $value;
$value = $self->SUPER::translate_value(@args);
return $value if ref $value;
if (grep($_ eq $field, $self->USER_FIELDS)) {
my $from_value = $value;
$value = $self->user_to_email($value);
$args[1] = $value;
# If we got something new from user_to_email, do any necessary
# translation of it.
$value = $self->SUPER::translate_value(@args);
if (!$options->{check_only}) {
$self->add_user($from_value, $value);
}
}
return $value;
}
1;

View File

@@ -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;
use Scalar::Util qw(blessed);
################################
##### 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_FIELD_MAP => {
product_id => 'product',
};
use constant UPDATE_COLUMNS => qw(
value
sortkey
);
use constant VALIDATORS => {
product => \&_check_product,
sortkey => \&_check_sortkey,
value => \&_check_value,
};
use constant VALIDATOR_DEPENDENCIES => {
value => ['product'],
};
################################
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;
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, undef, $params) = @_;
my $product = blessed($invocant) ? $invocant->product : $params->{product};
$name = trim($name);
$name || ThrowUserError('milestone_blank_name');
if (length($name) > MAX_MILESTONE_SIZE) {
ThrowUserError('milestone_name_too_long', {name => $name});
}
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) = @_;
$product || ThrowCodeError('param_required',
{ function => "$invocant->create", param => "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(
{ value => $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({value => $value, product => $product, sortkey => $sortkey})>
Description: Create a new milestone for the given product.
Params: $value - 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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,617 +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 List::Util qw(min max);
use List::MoreUtils qw(firstidx);
use base qw(Exporter);
@Bugzilla::Search::Quicksearch::EXPORT = qw(quicksearch);
# Custom mappings for some fields.
use constant MAPPINGS => {
# Status, Resolution, Platform, OS, Priority, Severity
"status" => "bug_status",
"platform" => "rep_platform",
"os" => "op_sys",
"severity" => "bug_severity",
# People: AssignedTo, Reporter, QA Contact, CC, etc.
"assignee" => "assigned_to",
"owner" => "assigned_to",
# Product, Version, Component, Target Milestone
"milestone" => "target_milestone",
# Summary, Description, URL, Status whiteboard, Keywords
"summary" => "short_desc",
"description" => "longdesc",
"comment" => "longdesc",
"url" => "bug_file_loc",
"whiteboard" => "status_whiteboard",
"sw" => "status_whiteboard",
"kw" => "keywords",
"group" => "bug_group",
# Flags
"flag" => "flagtypes.name",
"requestee" => "requestees.login_name",
"setter" => "setters.login_name",
# Attachments
"attachment" => "attachments.description",
"attachmentdesc" => "attachments.description",
"attachdesc" => "attachments.description",
"attachmentdata" => "attach_data.thedata",
"attachdata" => "attach_data.thedata",
"attachmentmimetype" => "attachments.mimetype",
"attachmimetype" => "attachments.mimetype"
};
sub FIELD_MAP {
my $cache = Bugzilla->request_cache;
return $cache->{quicksearch_fields} if $cache->{quicksearch_fields};
# Get all the fields whose names don't contain periods. (Fields that
# contain periods are always handled in MAPPINGS.)
my @db_fields = grep { $_->name !~ /\./ }
Bugzilla->get_fields({ obsolete => 0 });
my %full_map = (%{ MAPPINGS() }, map { $_->name => $_->name } @db_fields);
# Eliminate the fields that start with bug_ or rep_, because those are
# handled by the MAPPINGS instead, and we don't want too many names
# for them. (Also, otherwise "rep" doesn't match "reporter".)
#
# Remove "status_whiteboard" because we have "whiteboard" for it in
# the mappings, and otherwise "stat" can't match "status".
#
# Also, don't allow searching the _accessible stuff via quicksearch
# (both because it's unnecessary and because otherwise
# "reporter_accessible" and "reporter" both match "rep".
delete @full_map{qw(rep_platform bug_status bug_file_loc bug_group
bug_severity bug_status
status_whiteboard
cclist_accessible reporter_accessible)};
Bugzilla::Hook::process('quicksearch_map', {'map' => \%full_map} );
$cache->{quicksearch_fields} = \%full_map;
return $cache->{quicksearch_fields};
}
# Certain fields, when specified like "field:value" get an operator other
# than "substring"
use constant FIELD_OPERATOR => {
content => 'matches',
owner_idle_time => 'greaterthan',
};
# We might want to put this into localconfig or somewhere
use constant 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;
$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_numbers_only($searchstring);
}
else {
_handle_alias($searchstring);
# Globally translate " AND ", " OR ", " NOT " to space, pipe, dash.
$searchstring =~ s/\s+AND\s+/ /g;
$searchstring =~ s/\s+OR\s+/|/g;
$searchstring =~ s/\s+NOT\s+/ -/g;
my @words = splitString($searchstring);
_handle_status_and_resolution(\@words);
my (@unknownFields, %ambiguous_fields);
# Loop over all main-level QuickSearch words.
foreach my $qsword (@words) {
my $negate = substr($qsword, 0, 1) eq '-';
if ($negate) {
$qsword = substr($qsword, 1);
}
# No special first char
if (!_handle_special_first_chars($qsword, $negate)) {
# Split by '|' to get all operands for a boolean OR.
foreach my $or_operand (split(/\|/, $qsword)) {
if (!_handle_field_names($or_operand, $negate,
\@unknownFields,
\%ambiguous_fields))
{
# 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)) {
if (!_special_field_syntax($word, $negate)) {
_default_quicksearch_word($word, $negate);
}
_handle_urls($word, $negate);
}
}
}
}
$chart++;
$and = 0;
$or = 0;
} # foreach (@words)
# Inform user about any unknown fields
if (scalar(@unknownFields) || scalar(keys %ambiguous_fields)) {
ThrowUserError("quicksearch_unknown_field",
{ unknown => \@unknownFields,
ambiguous => \%ambiguous_fields });
}
# 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')) {
my $urlbase = correct_urlbase();
# Param 'load' asks us to display the query in the advanced search form.
print $cgi->redirect(-uri => "${urlbase}query.cgi?format=advanced&amp;"
. $modified_query_string);
}
# 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;
}
##########################
# Parts of quicksearch() #
##########################
sub _bug_numbers_only {
my $searchstring = shift;
my $cgi = Bugzilla->cgi;
# Allow separation by comma or whitespace.
$searchstring =~ s/[,\s]+/,/g;
if ($searchstring !~ /,/) {
# Single bug number; shortcut to show_bug.cgi.
print $cgi->redirect(
-uri => correct_urlbase() . "show_bug.cgi?id=$searchstring");
exit;
}
else {
# List of bug numbers.
$cgi->param('bug_id', $searchstring);
$cgi->param('order', 'bugs.bug_id');
$cgi->param('bug_id_type', 'anyexact');
}
}
sub _handle_alias {
my $searchstring = shift;
if ($searchstring =~ /^([^,\s]+)$/) {
my $alias = $1;
# We use this direct SQL because we want quicksearch to be VERY fast.
my $is_alias = Bugzilla->dbh->selectrow_array(
q{SELECT 1 FROM bugs WHERE alias = ?}, undef, $alias);
if ($is_alias) {
$alias = url_quote($alias);
print Bugzilla->cgi->redirect(
-uri => correct_urlbase() . "show_bug.cgi?id=$alias");
exit;
}
}
}
sub _handle_status_and_resolution {
my ($words) = @_;
my $legal_statuses = get_legal_field_values('bug_status');
my $legal_resolutions = get_legal_field_values('resolution');
my @openStates = BUG_STATE_OPEN;
my @closedStates;
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. CON,IN_PR,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 }
}
Bugzilla->cgi->param('bug_status', keys(%states));
Bugzilla->cgi->param('resolution', keys(%resolutions));
}
sub _handle_special_first_chars {
my ($qsword, $negate) = @_;
my $firstChar = substr($qsword, 0, 1);
my $baseWord = substr($qsword, 1);
my @subWords = split(/[\|,]/, $baseWord);
if ($firstChar eq '#') {
addChart('short_desc', 'substring', $baseWord, $negate);
addChart('content', 'matches', _matches_phrase($baseWord), $negate);
return 1;
}
if ($firstChar eq ':') {
foreach (@subWords) {
addChart('product', 'substring', $_, $negate);
addChart('component', 'substring', $_, $negate);
}
return 1;
}
if ($firstChar eq '@') {
addChart('assigned_to', 'substring', $_, $negate) foreach (@subWords);
return 1;
}
if ($firstChar eq '[') {
addChart('short_desc', 'substring', $baseWord, $negate);
addChart('status_whiteboard', 'substring', $baseWord, $negate);
return 1;
}
if ($firstChar eq '!') {
addChart('keywords', 'anywords', $baseWord, $negate);
return 1;
}
return 0;
}
sub _handle_field_names {
my ($or_operand, $negate, $unknownFields, $ambiguous_fields) = @_;
# Flag and requestee shortcut
if ($or_operand =~ /^(?:flag:)?([^\?]+\?)([^\?]*)$/) {
addChart('flagtypes.name', 'substring', $1, $negate);
$chart++; $and = $or = 0; # Next chart for boolean AND
addChart('requestees.login_name', 'substring', $2, $negate);
return 1;
}
# generic field1,field2,field3:value1,value2 notation
if ($or_operand =~ /^([^:]+):([^:]+)$/) {
my @fields = split(/,/, $1);
my @values = split(/,/, $2);
foreach my $field (@fields) {
my $translated = _translate_field_name($field);
# Skip and record any unknown fields
if (!defined $translated) {
push(@$unknownFields, $field);
next;
}
# If we got back an array, that means the substring is
# ambiguous and could match more than field name
elsif (ref $translated) {
$ambiguous_fields->{$field} = $translated;
next;
}
foreach my $value (@values) {
my $operator = FIELD_OPERATOR->{$translated} || 'substring';
addChart($translated, $operator, $value, $negate);
}
}
return 1;
}
return 0;
}
sub _translate_field_name {
my $field = shift;
$field = lc($field);
my $field_map = FIELD_MAP;
# If the field exactly matches a mapping, just return right now.
return $field_map->{$field} if exists $field_map->{$field};
# Check if we match, as a starting substring, exactly one field.
my @field_names = keys %$field_map;
my @matches = grep { $_ =~ /^\Q$field\E/ } @field_names;
# Eliminate duplicates that are actually the same field
# (otherwise "assi" matches both "assignee" and "assigned_to", and
# the lines below fail when they shouldn't.)
my %match_unique = map { $field_map->{$_} => $_ } @matches;
@matches = values %match_unique;
if (scalar(@matches) == 1) {
return $field_map->{$matches[0]};
}
elsif (scalar(@matches) > 1) {
return \@matches;
}
# Check if we match exactly one custom field, ignoring the cf_ on the
# custom fields (to allow people to type things like "build" for
# "cf_build").
my %cfless;
foreach my $name (@field_names) {
my $no_cf = $name;
if ($no_cf =~ s/^cf_//) {
if ($field eq $no_cf) {
return $field_map->{$name};
}
$cfless{$no_cf} = $name;
}
}
# See if we match exactly one substring of any of the cf_-less fields.
my @cfless_matches = grep { $_ =~ /^\Q$field\E/ } (keys %cfless);
if (scalar(@cfless_matches) == 1) {
my $match = $cfless_matches[0];
my $actual_field = $cfless{$match};
return $field_map->{$actual_field};
}
elsif (scalar(@matches) > 1) {
return \@matches;
}
return undef;
}
sub _special_field_syntax {
my ($word, $negate) = @_;
# P1-5 Syntax
if ($word =~ m/^P(\d+)(?:-(\d+))?$/i) {
my ($p_start, $p_end) = ($1, $2);
my $legal_priorities = get_legal_field_values('priority');
# If Pn exists explicitly, use it.
my $start = firstidx { $_ eq "P$p_start" } @$legal_priorities;
my $end;
$end = firstidx { $_ eq "P$p_end" } @$legal_priorities if defined $p_end;
# If Pn doesn't exist explicitly, then we mean the nth priority.
if ($start == -1) {
$start = max(0, $p_start - 1);
}
my $prios = $legal_priorities->[$start];
if (defined $end) {
# If Pn doesn't exist explicitly, then we mean the nth priority.
if ($end == -1) {
$end = min(scalar(@$legal_priorities), $p_end) - 1;
$end = max(0, $end); # Just in case the user typed P0.
}
($start, $end) = ($end, $start) if $end < $start;
$prios = join(',', @$legal_priorities[$start..$end])
}
addChart('priority', 'anyexact', $prios, $negate);
return 1;
}
return 0;
}
sub _default_quicksearch_word {
my ($word, $negate) = @_;
if (!grep { lc($word) eq $_ } PRODUCT_EXCEPTIONS and length($word) > 2) {
addChart('product', 'substring', $word, $negate);
}
if (!grep { lc($word) eq $_ } COMPONENT_EXCEPTIONS and length($word) > 2) {
addChart('component', 'substring', $word, $negate);
}
my @legal_keywords = map($_->name, Bugzilla::Keyword->get_all);
if (grep { lc($word) eq lc($_) } @legal_keywords) {
addChart('keywords', 'substring', $word, $negate);
}
addChart('alias', 'substring', $word, $negate);
addChart('short_desc', 'substring', $word, $negate);
addChart('status_whiteboard', 'substring', $word, $negate);
addChart('content', 'matches', _matches_phrase($word), $negate);
}
sub _handle_urls {
my ($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);
}
}
###########################################################################
# 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
if ($i++ %2) {
$part = url_quote($part);
# Protect the minus sign from being considered
# as negation, in quotes.
$part =~ s/(?<=^)\-/%2D/;
}
}
# 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;
}
# Quote and escape a phrase appropriately for a "content matches" search.
sub _matches_phrase {
my ($phrase) = @_;
$phrase =~ s/"/\\"/g;
return "\"$phrase\"";
}
# 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 'anywords') {
return 'nowords';
}
return "not$comparisonType";
}
# 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,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 Everything Solved, Inc.
# Portions created by the Initial Developer are Copyright (C) 2010 the
# Initial Developer. All Rights Reserved.
#
# Contributor(s):
# Max Kanat-Alexander <mkanat@bugzilla.org>
package Bugzilla::Search::Recent;
use strict;
use base qw(Bugzilla::Object);
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Util;
#############
# Constants #
#############
use constant DB_TABLE => 'profile_search';
use constant LIST_ORDER => 'id DESC';
use constant DB_COLUMNS => qw(
id
user_id
bug_list
list_order
);
use constant VALIDATORS => {
user_id => \&_check_user_id,
bug_list => \&_check_bug_list,
list_order => \&_check_list_order,
};
use constant UPDATE_COLUMNS => qw(bug_list list_order);
###################
# DB Manipulation #
###################
sub create {
my $class = shift;
my $dbh = Bugzilla->dbh;
$dbh->bz_start_transaction();
my $search = $class->SUPER::create(@_);
# Enforce there only being SAVE_NUM_SEARCHES per user.
my ($num_searches, $min_id) = $dbh->selectrow_array(
'SELECT COUNT(*), MIN(id) FROM profile_search WHERE user_id = ?',
undef, $search->user_id);
if ($num_searches > SAVE_NUM_SEARCHES) {
$dbh->do('DELETE FROM profile_search WHERE id = ?', undef, $min_id);
}
$dbh->bz_commit_transaction();
return $search;
}
sub create_placeholder {
my $class = shift;
return $class->create({ user_id => Bugzilla->user->id,
bug_list => '' });
}
###############
# Constructor #
###############
sub check {
my $class = shift;
my $search = $class->SUPER::check(@_);
my $user = Bugzilla->user;
if ($search->user_id != $user->id) {
ThrowUserError('object_does_not_exist', { id => $search->id });
}
return $search;
}
sub check_quietly {
my $class = shift;
my $error_mode = Bugzilla->error_mode;
Bugzilla->error_mode(ERROR_MODE_DIE);
my $search = eval { $class->check(@_) };
Bugzilla->error_mode($error_mode);
return $search;
}
sub new_from_cookie {
my ($invocant, $bug_ids) = @_;
my $class = ref($invocant) || $invocant;
my $search = { id => 'cookie',
user_id => Bugzilla->user->id,
bug_list => join(',', @$bug_ids) };
bless $search, $class;
return $search;
}
####################
# Simple Accessors #
####################
sub bug_list { return [split(',', $_[0]->{'bug_list'})]; }
sub list_order { return $_[0]->{'list_order'}; }
sub user_id { return $_[0]->{'user_id'}; }
############
# Mutators #
############
sub set_bug_list { $_[0]->set('bug_list', $_[1]); }
sub set_list_order { $_[0]->set('list_order', $_[1]); }
##############
# Validators #
##############
sub _check_user_id {
my ($invocant, $id) = @_;
require Bugzilla::User;
return Bugzilla::User->check({ id => $id })->id;
}
sub _check_bug_list {
my ($invocant, $list) = @_;
my @bug_ids = ref($list) ? @$list : split(',', $list || '');
detaint_natural($_) foreach @bug_ids;
return join(',', @bug_ids);
}
sub _check_list_order { defined $_[1] ? trim($_[1]) : '' }
1;
__END__
=head1 NAME
Bugzilla::Search::Recent - A search recently run by a logged-in user.
=head1 SYNOPSIS
use Bugzilla::Search::Recent;
=head1 DESCRIPTION
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.

View File

@@ -1,406 +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;
use Scalar::Util qw(blessed);
#############
# Constants #
#############
use constant DB_TABLE => 'namedqueries';
use constant DB_COLUMNS => qw(
id
userid
name
query
query_type
);
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);
###############
# Constructor #
###############
sub new {
my $class = shift;
my $param = shift;
my $dbh = Bugzilla->dbh;
my $user;
if (ref $param) {
$user = $param->{user} || Bugzilla->user;
my $name = $param->{name};
if (!defined $name) {
ThrowCodeError('bad_arg',
{argument => 'name',
function => "${class}::new"});
}
my $condition = 'userid = ? AND name = ?';
my $user_id = blessed $user ? $user->id : $user;
detaint_natural($user_id)
|| ThrowCodeError('param_must_be_numeric',
{function => $class . '::_init', param => 'user'});
my @values = ($user_id, $name);
$param = { condition => $condition, values => \@values };
}
unshift @_, $param;
my $self = $class->SUPER::new(@_);
if ($self) {
$self->{user} = $user if blessed $user;
# Some DBs (read: Oracle) incorrectly mark the query string as UTF-8
# when it's coming out of the database, even though it has no UTF-8
# characters in it, which prevents Bugzilla::CGI from later reading
# it correctly.
utf8::downgrade($self->{query}) if utf8::is_utf8($self->{query});
}
return $self;
}
sub check {
my $class = shift;
my $search = $class->SUPER::check(@_);
my $user = Bugzilla->user;
return $search if $search->user->id == $user->id;
if (!$search->shared_with_group
or !$user->in_group($search->shared_with_group))
{
ThrowUserError('missing_query', { queryname => $search->name,
sharer_id => $search->user->id });
}
return $search;
}
##############
# Validators #
##############
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 rename_field_value {
my ($class, $field, $old_value, $new_value) = @_;
my $old = url_quote($old_value);
my $new = url_quote($new_value);
my $old_sql = $old;
$old_sql =~ s/([_\%])/\\$1/g;
my $table = $class->DB_TABLE;
my $id_field = $class->ID_FIELD;
my $dbh = Bugzilla->dbh;
$dbh->bz_start_transaction();
my %queries = @{ $dbh->selectcol_arrayref(
"SELECT $id_field, query FROM $table WHERE query LIKE ?",
{Columns=>[1,2]}, "\%$old_sql\%") };
foreach my $id (keys %queries) {
my $query = $queries{$id};
$query =~ s/\b$field=\Q$old\E\b/$field=$new/gi;
# Fix boolean charts.
while ($query =~ /\bfield(\d+-\d+-\d+)=\Q$field\E\b/gi) {
my $chart_id = $1;
# Note that this won't handle lists or substrings inside of
# boolean charts. Users will have to fix those themselves.
$query =~ s/\bvalue\Q$chart_id\E=\Q$old\E\b/value$chart_id=$new/i;
}
$dbh->do("UPDATE $table SET query = ? WHERE $id_field = ?",
undef, $query, $id);
}
$dbh->bz_commit_transaction();
}
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 type { return $_[0]->{'query_type'}; }
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>
Takes either an id, or the named parameters C<user> and C<name>.
C<user> can be either a L<Bugzilla::User> object or a numeric user id.
See also: L<Bugzilla::Object/new>.
=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<type>
The numeric id of the type of search this is (from L<Bugzilla::Constants>).
=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,286 +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;
# This is a hack so that we can re-use the rename_field_value
# code from Bugzilla::Search::Saved.
use constant DB_TABLE => 'series';
use constant ID_FIELD => 'series_id';
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.
# XXX - You cannot set category_id and subcategory_id from here.
$self->initFromParameters(@_);
}
else {
die("Bad parameters passed in - invalid number of args: $arg_count");
}
return $retval;
}
sub initFromDatabase {
my ($self, $series_id) = @_;
my $dbh = Bugzilla->dbh;
my $user = Bugzilla->user;
detaint_natural($series_id)
|| ThrowCodeError("invalid_series_id", { 'series_id' => $series_id });
my $grouplist = $user->groups_as_string;
my @series = $dbh->selectrow_array("SELECT series.series_id, cc1.name, " .
"cc2.name, series.name, series.creator, series.frequency, " .
"series.query, series.is_public, series.category, series.subcategory " .
"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 series.series_id = ? " .
" AND (creator = ? OR (is_public = 1 AND cgm.category_id IS NULL))",
undef, ($series_id, $user->id));
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_id'}, $self->{'frequency'},
$self->{'query'}, $self->{'public'}, $self->{'category_id'},
$self->{'subcategory_id'}) = @_;
# If the first parameter is undefined, check if this series already
# exists and update it series_id accordingly
$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_id'} = 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_id'}, $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;
}
##########
# Methods
##########
sub id { return $_[0]->{'series_id'}; }
sub name { return $_[0]->{'name'}; }
sub creator {
my $self = shift;
if (!$self->{creator} && $self->{creator_id}) {
require Bugzilla::User;
$self->{creator} = new Bugzilla::User($self->{creator_id});
}
return $self->{creator};
}
sub remove_from_db {
my $self = shift;
my $dbh = Bugzilla->dbh;
$dbh->do('DELETE FROM series WHERE series_id = ?', undef, $self->id);
}
1;

View File

@@ -1,317 +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;
# This subclasses Bugzilla::Field::Choice instead of implementing
# ChoiceInterface, because a bug status literally is a special type
# of Field::Choice, not just an object that happens to have the same
# methods.
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(@_);
delete Bugzilla->request_cache->{status_bug_state_open};
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();
delete Bugzilla->request_cache->{status_bug_state_open};
}
###############################
##### 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 {
my $dbh = Bugzilla->dbh;
my $cache = Bugzilla->request_cache;
$cache->{status_bug_state_open} ||=
$dbh->selectcol_arrayref('SELECT value FROM bug_status
WHERE is_open = 1');
return @{ $cache->{status_bug_state_open} };
}
# Tells you whether or not the argument is a valid "open" state.
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 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 => 'IN_PROGRESS' });
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<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

File diff suppressed because it is too large Load Diff

View File

@@ -1,104 +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 ITA Software.
# Portions created by the Initial Developer are Copyright (C) 2009
# the Initial Developer. All Rights Reserved.
#
# Contributor(s):
# Max Kanat-Alexander <mkanat@bugzilla.org>
# This exists to implement the template-before_process hook.
package Bugzilla::Template::Context;
use strict;
use base qw(Template::Context);
use Bugzilla::Hook;
use Scalar::Util qw(blessed);
sub process {
my $self = shift;
# We don't want to run the template_before_process hook for
# template hooks (but we do want it to run if a hook calls
# PROCESS inside itself). The problem is that the {component}->{name} of
# hooks is unreliable--sometimes it starts with ./ and it's the
# full path to the hook template, and sometimes it's just the relative
# name (like hook/global/field-descs-end.none.tmpl). Also, calling
# template_before_process for hook templates doesn't seem too useful,
# because that's already part of the extension and they should be able
# to modify their hook if they want (or just modify the variables in the
# calling template).
if (not delete $self->{bz_in_hook}) {
$self->{bz_in_process} = 1;
}
my $result = $self->SUPER::process(@_);
delete $self->{bz_in_process};
return $result;
}
# This method is called by Template-Toolkit exactly once per template or
# block (look at a compiled template) so this is an ideal place for us to
# modify the variables before a template or block runs.
#
# We don't do it during Context::process because at that time
# our stash hasn't been set correctly--the parameters we were passed
# in the PROCESS or INCLUDE directive haven't been set, and if we're
# in an INCLUDE, the stash is not yet localized during process().
sub stash {
my $self = shift;
my $stash = $self->SUPER::stash(@_);
my $name = $stash->{component}->{name};
my $pre_process = $self->config->{PRE_PROCESS};
# Checking bz_in_process tells us that we were indeed called as part of a
# Context::process, and not at some other point.
#
# Checking $name makes sure that we're processing a file, and not just a
# block, by checking that the name has a period in it. We don't allow
# blocks because their names are too unreliable--an extension could have
# a block with the same name, or multiple files could have a same-named
# block, and then your extension would malfunction.
#
# We also make sure that we don't run, ever, during the PRE_PROCESS
# templates, because if somebody calls Throw*Error globally inside of
# template_before_process, that causes an infinite recursion into
# the PRE_PROCESS templates (because Bugzilla, while inside
# global/intialize.none.tmpl, loads the template again to create the
# template object for Throw*Error).
#
# Checking Bugzilla::Hook::in prevents infinite recursion on this hook.
if ($self->{bz_in_process} and $name =~ /\./
and !grep($_ eq $name, @$pre_process)
and !Bugzilla::Hook::in('template_before_process'))
{
Bugzilla::Hook::process("template_before_process",
{ vars => $stash, context => $self,
file => $name });
}
# This prevents other calls to stash() that might somehow happen
# later in the file from also triggering the hook.
delete $self->{bz_in_process};
return $stash;
}
# We need a DESTROY sub for the same reason that Bugzilla::CGI does.
sub DESTROY {
my $self = shift;
$self->SUPER::DESTROY(@_);
};
1;

View File

@@ -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,165 +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>
# Max Kanat-Alexander <mkanat@bugzilla.org>
package Bugzilla::Template::Plugin::Hook;
use strict;
use base qw(Template::Plugin);
use Bugzilla::Constants;
use Bugzilla::Install::Util qw(template_include_path);
use Bugzilla::Util;
use Bugzilla::Error;
use File::Spec;
sub new {
my ($class, $context) = @_;
return bless { _CONTEXT => $context }, $class;
}
sub _context { return $_[0]->{_CONTEXT} }
sub process {
my ($self, $hook_name, $template) = @_;
my $context = $self->_context();
$template ||= $context->stash->{component}->{name};
# sanity check:
if (!$template =~ /[\w\.\/\-_\\]+/) {
ThrowCodeError('template_invalid', { name => $template });
}
my (undef, $path, $filename) = File::Spec->splitpath($template);
$path ||= '';
$filename =~ m/(.+)\.(.+)\.tmpl$/;
my $template_name = $1;
my $type = $2;
# Hooks are named like this:
my $extension_template = "$path$template_name-$hook_name.$type.tmpl";
# Get the hooks out of the cache if they exist. Otherwise, read them
# from the disk.
my $cache = Bugzilla->request_cache->{template_plugin_hook_cache} ||= {};
my $lang = $context->{bz_language} || '';
$cache->{"${lang}__$extension_template"}
||= $self->_get_hooks($extension_template);
# process() accepts an arrayref of templates, so we just pass the whole
# arrayref.
$context->{bz_in_hook} = 1; # See Bugzilla::Template::Context
return $context->process($cache->{"${lang}__$extension_template"});
}
sub _get_hooks {
my ($self, $extension_template) = @_;
my $template_sets = $self->_template_hook_include_path();
my @hooks;
foreach my $dir_set (@$template_sets) {
foreach my $template_dir (@$dir_set) {
my $file = "$template_dir/hook/$extension_template";
if (-e $file) {
my $template = $self->_context->template($file);
push(@hooks, $template);
# Don't run the hook for more than one language.
last;
}
}
}
return \@hooks;
}
sub _template_hook_include_path {
my $self = shift;
my $cache = Bugzilla->request_cache;
my $language = $self->_context->{bz_language} || '';
my $cache_key = "template_plugin_hook_include_path_$language";
$cache->{$cache_key} ||= template_include_path({
language => $language,
hook => 1,
});
return $cache->{$cache_key};
}
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://wiki.mozilla.org/Bugzilla:Writing_Extensions>

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,630 +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());
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', remote_ip());
# 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());
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 ($db_token, $issuedate, $tokentype, $eventdata, $userid) =
$dbh->selectrow_array('SELECT token, ' . $dbh->sql_date_format('issuedate') . ',
tokentype, eventdata, userid
FROM tokens
WHERE token = ?',
undef, $token);
# Some DBs such as MySQL are case-insensitive by default so we do
# a quick comparison to make sure the tokens are indeed the same.
(defined $db_token && $db_token eq $token)
|| ThrowCodeError("cancel_token_does_not_exist");
# 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'} = remote_ip();
$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());
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);
my @token_data = $dbh->selectrow_array(
"SELECT token, userid, " . $dbh->sql_date_format('issuedate') . ", eventdata
FROM tokens
WHERE token = ?", undef, $token);
# Some DBs such as MySQL are case-insensitive by default so we do
# a quick comparison to make sure the tokens are indeed the same.
my $db_token = shift @token_data;
return undef if (!defined $db_token || $db_token ne $token);
return @token_data;
}
# 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,209 +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->feature('updates');
return if (Bugzilla->params->{'upgrade_notification'} eq 'disabled');
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)) {
unlink $local_file; # Make sure the old copy is away.
if (-e $local_file) {
return { 'error' => 'no_update', xml_file => $local_file };
}
my $error = _synchronize_data();
# If an error is returned, leave now.
return $error if $error;
}
# If we cannot access the local XML file, ignore it.
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 {
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.
=end private
=back
=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

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