Commit 6421f7f6 authored by Per Cederqvist's avatar Per Cederqvist

Imported Bugzilla 4.0.3.

parent 94a14f18
No preview for this file type
......@@ -536,9 +536,13 @@ sub _check_content_type {
}
$content_type = 'text/plain' if ($is_url || $is_patch);
$content_type = trim($content_type);
$content_type = clean_text($content_type);
# The subsets below cover all existing MIME types and charsets registered by IANA.
# (MIME type: RFC 2045 section 5.1; charset: RFC 2278 section 3.3)
my $legal_types = join('|', LEGAL_CONTENT_TYPES);
if (!$content_type or $content_type !~ /^($legal_types)\/.+$/) {
if (!$content_type
|| $content_type !~ /^($legal_types)\/[a-z0-9_\-\+\.]+(;.+)?$/i)
{
ThrowUserError("invalid_content_type", { contenttype => $content_type });
}
trick_taint($content_type);
......@@ -615,7 +619,7 @@ sub _check_filename {
# No file is attached, so it has no name.
return '' if $is_url;
$filename = trim($filename);
$filename = clean_text($filename);
$filename || ThrowUserError('file_not_specified');
# Remove path info (if any) from the file name. The browser should do this
......
......@@ -207,7 +207,8 @@ sub _handle_login_result {
# 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});
my $template = Bugzilla->template_inner(
$default_settings->{lang}->{default_value});
my $vars = {
locked_user => $user,
attempts => $attempts,
......
......@@ -526,8 +526,8 @@ sub possible_duplicates {
FROM bugs
INNER JOIN bugs_fulltext ON bugs.bug_id = bugs_fulltext.bug_id
WHERE ($where_sql) $product_sql
ORDER BY relevance DESC, bug_id DESC
LIMIT $sql_limit", {Slice=>{}});
ORDER BY relevance DESC, bug_id DESC " .
$dbh->sql_limit($sql_limit), {Slice=>{}});
my @actual_dupe_ids;
# Resolve duplicates into their ultimate target duplicates.
......
......@@ -438,7 +438,7 @@ sub dump {
require Data::Dumper;
print "<pre>Bugzilla::Chart object:\n";
print Data::Dumper::Dumper($self);
print html_quote(Data::Dumper::Dumper($self));
print "</pre>";
}
......
......@@ -46,6 +46,12 @@ sub get_param_list {
default => '0'
},
{
name => 'ajax_user_autocompletion',
type => 'b',
default => '1',
},
{
name => 'maxusermatches',
type => 't',
......
......@@ -197,7 +197,7 @@ use File::Basename;
# CONSTANTS
#
# Bugzilla version
use constant BUGZILLA_VERSION => "4.0.2";
use constant BUGZILLA_VERSION => "4.0.3";
# 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
......
......@@ -365,8 +365,11 @@ sub sql_string_concat {
sub sql_string_until {
my ($self, $string, $substring) = @_;
return "SUBSTRING($string FROM 1 FOR " .
$self->sql_position($substring, $string) . " - 1)";
my $position = $self->sql_position($substring, $string);
return "CASE WHEN $position != 0"
. " THEN SUBSTR($string, 1, $position - 1)"
. " ELSE $string END";
}
sub sql_in {
......
......@@ -40,6 +40,8 @@ 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;
......@@ -50,6 +52,8 @@ use Bugzilla::Util;
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 {
......@@ -68,8 +72,8 @@ sub new {
my $dsn = "dbi:Oracle:host=$host;sid=$dbname";
$dsn .= ";port=$port" if $port;
my $attrs = { FetchHashKeyName => 'NAME_lc',
LongReadLen => ( Bugzilla->params->{'maxattachmentsize'}
|| 1000 ) * 1024,
LongReadLen => max(Bugzilla->params->{'maxattachmentsize'},
MIN_LONG_READ_LEN) * 1024,
};
my $self = $class->db_new({ dsn => $dsn, user => $user,
pass => $pass, attrs => $attrs });
......@@ -156,13 +160,6 @@ sub sql_string_concat {
return 'CONCAT(' . join(', ', @params) . ')';
}
sub sql_string_until {
my ($self, $string, $substring) = @_;
return "SUBSTR($string, 1, "
. $self->sql_position($substring, $string)
. " - 1)";
}
sub sql_to_days {
my ($self, $date) = @_;
......@@ -177,7 +174,7 @@ sub sql_fulltext_search {
my ($self, $column, $text, $label) = @_;
$text = $self->quote($text);
trick_taint($text);
return "CONTAINS($column,$text,$label)", "SCORE($label)";
return "CONTAINS($column,$text,$label) > 0", "SCORE($label)";
}
sub sql_date_format {
......@@ -377,20 +374,17 @@ sub adjust_statement {
if ($new_sql !~ /\bWHERE\b/) {
$new_sql = $new_sql." WHERE 1=1";
}
my ($before_where, $after_where) = split /\bWHERE\b/i,$new_sql;
if (defined($offset)) {
if ($new_sql =~ /(.*\s+)FROM(\s+.*)/i) {
my ($before_from,$after_from) = ($1,$2);
$before_where = "$before_from FROM ($before_from,"
. " ROW_NUMBER() OVER (ORDER BY 1) R "
. " FROM $after_from ) ";
$after_where = " R BETWEEN $offset+1 AND $limit+$offset";
}
} else {
$after_where = " rownum <=$limit AND ".$after_where;
}
$new_sql = $before_where." WHERE ".$after_where;
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;
}
......
......@@ -192,18 +192,6 @@ sub sql_string_concat {
return '(CAST(' . join(' AS text) || CAST(', @params) . ' AS text))';
}
sub sql_string_until {
my ($self, $string, $substring) = @_;
# PostgreSQL does not permit a negative substring length; therefore we
# use CASE to only perform the SUBSTRING operation when $substring can
# be found withing $string.
my $position = $self->sql_position($substring, $string);
return "CASE WHEN $position != 0"
. " THEN SUBSTRING($string FROM 1 FOR $position - 1)"
. " ELSE $string END";
}
# Tell us whether or not a particular sequence exists in the DB.
sub bz_sequence_exists {
my ($self, $seq_name) = @_;
......
......@@ -698,8 +698,9 @@ sub _checking_for {
# show "ok" or "not found".
if (exists $params->{found}) {
my $found_string;
# We do a string compare in case it's non-numeric.
if ($found and $found eq "-1") {
# 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) {
......
......@@ -294,6 +294,7 @@ sub check_requirements {
my $missing = Bugzilla::Install::Requirements::_check_missing(
$self->REQUIRED_MODULES, 1);
my %results = (
apache => [],
pass => @$missing ? 0 : 1,
missing => $missing,
any_missing => @$missing ? 1 : 0,
......
......@@ -54,7 +54,7 @@ use Bugzilla::Keyword;
use Date::Format;
use Date::Parse;
use Scalar::Util qw(blessed);
use Storable qw(dclone);
# If you specify a search type in the boolean charts, this describes
......@@ -279,9 +279,14 @@ use constant SPECIAL_ORDER_JOIN => {
# and we don't want it to happen at compile time, so we have it as a
# subroutine.
sub COLUMNS {
my $invocant = shift;
my $user = blessed($invocant) ? $invocant->{user} : Bugzilla->user;
my $dbh = Bugzilla->dbh;
my $cache = Bugzilla->request_cache;
return $cache->{search_columns} if defined $cache->{search_columns};
if (defined $cache->{search_columns}->{$user->id}) {
return $cache->{search_columns}->{$user->id};
}
# These are columns that don't exist in fielddefs, but are valid buglist
# columns. (Also see near the bottom of this function for the definition
......@@ -296,7 +301,7 @@ sub COLUMNS {
# Next we define columns that have special SQL instead of just something
# like "bugs.bug_id".
my $actual_time = '(SUM(ldtime.work_time)'
. ' * COUNT(DISTINCT ldtime.bug_when)/COUNT(bugs.bug_id))';
. ' * COUNT(DISTINCT ldtime.comment_id)/COUNT(bugs.bug_id))';
my %special_sql = (
deadline => $dbh->sql_date_format('bugs.deadline', '%Y-%m-%d'),
actual_time => $actual_time,
......@@ -333,7 +338,7 @@ sub COLUMNS {
foreach my $col (@email_fields) {
my $sql = "map_${col}.login_name";
if (!Bugzilla->user->id) {
if (!$user->id) {
$sql = $dbh->sql_string_until($sql, $dbh->quote('@'));
}
$special_sql{$col} = $sql;
......@@ -367,8 +372,8 @@ sub COLUMNS {
Bugzilla::Hook::process('buglist_columns', { columns => \%columns });
$cache->{search_columns} = \%columns;
return $cache->{search_columns};
$cache->{search_columns}->{$user->id} = \%columns;
return $cache->{search_columns}->{$user->id};
}
sub REPORT_COLUMNS {
......@@ -1012,7 +1017,7 @@ sub init {
# to other parts of the query, so we want to create it before we
# write the FROM clause.
foreach my $orderitem (@inputorder) {
BuildOrderBy(\%special_order, $orderitem, \@orderby);
$self->_build_order_by(\%special_order, $orderitem, \@orderby);
}
# Now JOIN the correct tables in the FROM clause.
# This is done separately from the above because it's
......@@ -1056,7 +1061,7 @@ sub init {
# Aliases cannot contain dots in them. We convert them to underscores.
$alias =~ s/\./_/g;
my $sql_field = ($field eq EMPTY_COLUMN) ? EMPTY_COLUMN
: COLUMNS->{$field}->{name} . " AS $alias";
: $self->COLUMNS->{$field}->{name} . " AS $alias";
push(@sql_fields, $sql_field);
}
my $query = "SELECT " . join(', ', @sql_fields) .
......@@ -1096,7 +1101,7 @@ sub init {
push(@skip_group_by, map { $_->name } @multi_select_fields);
next if grep { $_ eq $field } @skip_group_by;
my $col = COLUMNS->{$field}->{name};
my $col = $self->COLUMNS->{$field}->{name};
push(@groupby, $col) if !grep($_ eq $col, @groupby);
}
# And all items from ORDER BY must be in the GROUP BY. The above loop
......@@ -1352,7 +1357,7 @@ sub IsValidQueryType
return 0;
}
# BuildOrderBy - Private Subroutine
# _build_order_by - Private Subroutine
# This function converts the input order to an "output" order,
# suitable for concatenation to form an ORDER BY clause. Basically,
# it just handles fields that have non-standard sort orders from
......@@ -1373,10 +1378,10 @@ sub IsValidQueryType
# Let's say that we had a field "A" that normally translates to a sort
# order of "B ASC, C DESC". If we sort by "A DESC", what we really then
# mean is "B DESC, C ASC". So $reverseorder is only used if we call
# BuildOrderBy recursively, to let it know that we're "reversing" the
# _build_order_by recursively, to let it know that we're "reversing" the
# order. That is, that we wanted "A DESC", not "A".
sub BuildOrderBy {
my ($special_order, $orderitem, $stringlist, $reverseorder) = (@_);
sub _build_order_by {
my ($self, $special_order, $orderitem, $stringlist, $reverseorder) = @_;
my ($orderfield, $orderdirection) = split_order_term($orderitem);
......@@ -1396,13 +1401,13 @@ sub BuildOrderBy {
foreach my $subitem (@{$special_order->{$orderfield}}) {
# DESC on a field with non-standard sort order means
# "reverse the normal order for each field that we map to."
BuildOrderBy($special_order, $subitem, $stringlist,
$orderdirection =~ m/desc/i);
$self->_build_order_by($special_order, $subitem, $stringlist,
$orderdirection =~ m/desc/i);
}
return;
}
# Aliases cannot contain dots in them. We convert them to underscores.
$orderfield =~ s/\./_/g if exists COLUMNS->{$orderfield};
$orderfield =~ s/\./_/g if exists $self->COLUMNS->{$orderfield};
push(@$stringlist, trim($orderfield . ' ' . $orderdirection));
}
......@@ -1658,11 +1663,11 @@ sub _content_matches {
#
# We build the relevance SQL by modifying the COLUMNS list directly,
# which is kind of a hack but works.
my $current = COLUMNS->{'relevance'}->{name};
my $current = $self->COLUMNS->{'relevance'}->{name};
$current = $current ? "$current + " : '';
# For NOT searches, we just add 0 to the relevance.
my $select_term = $$t =~ /not/ ? 0 : "($current$rterm1 + $rterm2)";
COLUMNS->{'relevance'}->{name} = $select_term;
$self->COLUMNS->{'relevance'}->{name} = $select_term;
}
sub _timestamp_translate {
......
......@@ -448,26 +448,21 @@ sub save_last_search {
return if !@$bug_ids;
my $search;
if ($self->id) {
on_main_db {
my $search;
if ($list_id) {
# Use eval so that people can still use old search links or
# links that don't belong to them.
$search = eval { Bugzilla::Search::Recent->check(
{ id => $list_id }) };
$search = Bugzilla::Search::Recent->check_quietly({ id => $list_id });
}
if ($search) {
# We only update placeholders. (Placeholders are
# Saved::Search::Recent objects with empty bug lists.)
# Otherwise, we could just keep creating new searches
# for the same refreshed list over and over.
if (!@{ $search->bug_list }) {
$search->set_list_order($order);
if (join(',', @{$search->bug_list}) ne join(',', @$bug_ids)) {
$search->set_bug_list($bug_ids);
$search->update();
}
if (!$search->list_order || $order ne $search->list_order) {
$search->set_list_order($order);
}
$search->update();
}
else {
# If we already have an existing search with a totally
......@@ -480,11 +475,14 @@ sub save_last_search {
user_id => $self->id, bug_list => $list_string });
if (!scalar(@$existing_search)) {
Bugzilla::Search::Recent->create({
$search = Bugzilla::Search::Recent->create({
user_id => $self->id,
bug_list => $bug_ids,
list_order => $order });
}
else {
$search = $existing_search->[0];
}
}
};
delete $self->{recent_searches};
......@@ -506,6 +504,7 @@ sub save_last_search {
$vars->{'toolong'} = 1;
}
}
return $search;
}
sub settings {
......@@ -1853,6 +1852,32 @@ sub is_available_username {
return 1;
}
sub check_account_creation_enabled {
my $self = shift;
# If we're using e.g. LDAP for login, then we can't create a new account.
$self->authorizer->user_can_create_account
|| ThrowUserError('auth_cant_create_account');
Bugzilla->params->{'createemailregexp'}
|| ThrowUserError('account_creation_disabled');
}
sub check_and_send_account_creation_confirmation {
my ($self, $login) = @_;
$login = $self->check_login_name_for_creation($login);
my $creation_regexp = Bugzilla->params->{'createemailregexp'};
if ($login !~ /$creation_regexp/i) {
ThrowUserError('account_creation_restricted');
}
# Create and send a token for this new account.
require Bugzilla::Token;
Bugzilla::Token::issue_new_user_account_token($login);
}
sub login_to_id {
my ($login, $throw_error) = @_;
my $dbh = Bugzilla->dbh;
......@@ -2316,12 +2341,6 @@ Returns true if the user wants mail for a given set of events. This method is
more general than C<wants_bug_mail>, allowing you to check e.g. permissions
for flag mail.
=item C<is_mover>
Returns true if the user is in the list of users allowed to move bugs
to another database. Note that this method doesn't check whether bug
moving is enabled.
=item C<is_insider>
Returns true if the user can access private comments and attachments,
......@@ -2362,6 +2381,17 @@ Params: login_name - B<Required> The login name for the new user.
Takes a username as its only argument. Throws an error if there is no
user with that username. Returns a C<Bugzilla::User> object.
=item C<check_account_creation_enabled>
Checks that users can create new user accounts, and throws an error
if user creation is disabled.
=item C<check_and_send_account_creation_confirmation($login)>
If the user request for a new account passes validation checks, an email
is sent to this user for confirmation. Otherwise an error is thrown
indicating why the request has been rejected.
=item C<is_available_username>
Returns a boolean indicating whether or not the supplied username is
......
......@@ -639,9 +639,12 @@ sub add_attachment {
my @created;
$dbh->bz_start_transaction();
my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
foreach my $bug (@bugs) {
my $attachment = Bugzilla::Attachment->create({
bug => $bug,
creation_ts => $timestamp,
data => $params->{data},
description => $params->{summary},
filename => $params->{file_name},
......@@ -657,7 +660,7 @@ sub add_attachment {
extra_data => $attachment->id });
push(@created, $attachment);
}
$_->bug->update($_->attached) foreach @created;
$_->bug->update($timestamp) foreach @created;
$dbh->bz_commit_transaction();
$_->send_changes() foreach @bugs;
......@@ -1022,7 +1025,7 @@ containing the following keys:
=item C<id>
C<int> An integer id uniquely idenfifying this field in this installation only.
C<int> An integer id uniquely identifying this field in this installation only.
=item C<type>
......@@ -2504,7 +2507,7 @@ This allows you to add a comment to a bug in Bugzilla.
=over
=item C<id> (int) B<Required> - The id or alias of the bug to append a
=item C<id> (int or string) B<Required> - The id or alias of the bug to append a
comment to.
=item C<comment> (string) B<Required> - The comment to append to the bug.
......
......@@ -130,6 +130,7 @@ use constant WS_ERROR_CODE => {
# User errors are 500-600.
account_exists => 500,
illegal_email_address => 501,
auth_cant_create_account => 501,
account_creation_disabled => 501,
account_creation_restricted => 501,
password_too_short => 502,
......
......@@ -28,7 +28,6 @@ use Bugzilla::Error;
use Bugzilla::Group;
use Bugzilla::User;
use Bugzilla::Util qw(trim);
use Bugzilla::Token;
use Bugzilla::WebService::Util qw(filter validate);
# Don't need auth to login
......@@ -91,18 +90,8 @@ sub offer_account_by_email {
my $email = trim($params->{email})
|| ThrowCodeError('param_required', { param => 'email' });
my $createexp = Bugzilla->params->{'createemailregexp'};
if (!$createexp) {
ThrowUserError("account_creation_disabled");
}
elsif ($email !~ /$createexp/) {
ThrowUserError("account_creation_restricted");
}
$email = Bugzilla::User->check_login_name_for_creation($email);
# Create and send a token for this new account.
Bugzilla::Token::issue_new_user_account_token($email);
Bugzilla->user->check_account_creation_enabled;
Bugzilla->user->check_and_send_account_creation_confirmation($email);
return undef;
}
......@@ -396,14 +385,14 @@ This is the recommended way to create a Bugzilla account.
=over
=item 500 (Illegal Email Address)
=item 500 (Account Already Exists)
This Bugzilla does not allow you to create accounts with the format of
email address you specified. Account creation may be entirely disabled.
An account with that email address already exists in Bugzilla.
=item 501 (Account Already Exists)
=item 501 (Illegal Email Address)
An account with that email address already exists in Bugzilla.
This Bugzilla does not allow you to create accounts with the format of
email address you specified. Account creation may be entirely disabled.
=back
......
......@@ -177,14 +177,13 @@ my $params;
# If the user is retrieving the last bug list they looked at, hack the buffer
# storing the query string so that it looks like a query retrieving those bugs.
if (my $last_list = $cgi->param('regetlastlist')) {
my ($bug_ids, $order);
my $bug_ids;
# Logged-out users use the old cookie method for storing the last search.
if (!$user->id or $last_list eq 'cookie') {
$cgi->cookie('BUGLIST') || ThrowUserError("missing_cookie");
$order = "reuse last sort" unless $order;
$bug_ids = $cgi->cookie('BUGLIST');
$bug_ids = $cgi->cookie('BUGLIST') or ThrowUserError("missing_cookie");
$bug_ids =~ s/[:-]/,/g;
$order ||= "reuse last sort";
}
# But logged in users store the last X searches in the DB so they can
# have multiple bug lists available.
......@@ -192,10 +191,11 @@ if (my $last_list = $cgi->param('regetlastlist')) {
my $last_search = Bugzilla::Search::Recent->check(
{ id => $last_list });
$bug_ids = join(',', @{ $last_search->bug_list });
$order = $last_search->list_order if !$order;
$order ||= $last_search->list_order;