Commit dcf6afc5 authored by Per Cederqvist's avatar Per Cederqvist

Imported Bugzilla 4.4.3.

parent 3e31437c
This is a Bazaar control directory.
Do not change any files in this directory.
See http://bazaar-vcs.org/ for more information about Bazaar.
See http://bazaar.canonical.com/ for more information about Bazaar.
No preview for this file type
.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
fc3bcda75a19dd381af0f9d54d98645a332bccbf
\ No newline at end of file
......@@ -108,6 +108,15 @@ sub can_logout {
return $getter->can_logout;
}
sub login_token {
my ($self) = @_;
my $getter = $self->{_info_getter}->{successful};
if ($getter && $getter->isa('Bugzilla::Auth::Login::Cookie')) {
return $getter->login_token;
}
return undef;
}
sub user_can_create_account {
my ($self) = @_;
my $verifier = $self->{_verifier}->{successful};
......@@ -143,7 +152,7 @@ sub _handle_login_result {
if ($self->{_info_getter}->{successful}->requires_persistence
and !Bugzilla->request_cache->{auth_no_automatic_login})
{
$self->{_persister}->persist_login($user);
$user->{_login_token} = $self->{_persister}->persist_login($user);
}
}
elsif ($fail_code == AUTH_ERROR) {
......@@ -409,6 +418,14 @@ Params: None
Returns: C<true> if users can change their own email address,
C<false> otherwise.
=item C<login_token>
Description: If a login token was used instead of a cookie then this
will return the current login token data such as user id
and the token itself.
Params: None
Returns: A hash containing C<login_token> and C<user_id>.
=back
=head1 STRUCTURE
......
......@@ -14,19 +14,52 @@ use Bugzilla::Constants;
use Bugzilla::WebService::Constants;
use Bugzilla::Util;
use Bugzilla::Error;
use Bugzilla::Token;
sub get_login_info {
my ($self) = @_;
my $params = Bugzilla->input_params;
my $cgi = Bugzilla->cgi;
my $login = trim(delete $params->{'Bugzilla_login'});
my $password = delete $params->{'Bugzilla_password'};
# The token must match the cookie to authenticate the request.
my $login_token = delete $params->{'Bugzilla_login_token'};
my $login_cookie = $cgi->cookie('Bugzilla_login_request_cookie');
my $username = trim(delete $params->{"Bugzilla_login"});
my $password = delete $params->{"Bugzilla_password"};
my $valid = 0;
# If the web browser accepts cookies, use them.
if ($login_token && $login_cookie) {
my ($time, undef) = split(/-/, $login_token);
# Regenerate the token based on the information we have.
my $expected_token = issue_hash_token(['login_request', $login_cookie], $time);
$valid = 1 if $expected_token eq $login_token;
$cgi->remove_cookie('Bugzilla_login_request_cookie');
}
# WebServices and other local scripts can bypass this check.
# This is safe because we won't store a login cookie in this case.
elsif (Bugzilla->usage_mode != USAGE_MODE_BROWSER) {
$valid = 1;
}
# Else falls back to the Referer header and accept local URLs.
# Attachments are served from a separate host (ideally), and so
# an evil attachment cannot abuse this check with a redirect.
elsif (my $referer = $cgi->referer) {
my $urlbase = correct_urlbase();
$valid = 1 if $referer =~ /^\Q$urlbase\E/;
}
# If the web browser doesn't accept cookies and the Referer header
# is missing, we have no way to make sure that the authentication
# request comes from the user.
elsif ($login && $password) {
ThrowUserError('auth_untrusted_request', { login => $login });
}
if (!defined $username || !defined $password) {
if (!$login || !$password || !$valid) {
return { failure => AUTH_NODATA };
}
return { username => $username, password => $password };
return { username => $login, password => $password };
}
sub fail_nodata {
......
......@@ -6,8 +6,11 @@
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::Auth::Login::Cookie;
use strict;
use base qw(Bugzilla::Auth::Login);
use fields qw(_login_token);
use Bugzilla::Constants;
use Bugzilla::Util;
......@@ -17,7 +20,8 @@ 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;
sub is_automatic { return $_[0]->login_token ? 0 : 1; }
# Note that Cookie never consults the Verifier, it always assumes
# it has a valid DB account or it fails.
......@@ -25,24 +29,35 @@ sub get_login_info {
my ($self) = @_;
my $cgi = Bugzilla->cgi;
my $dbh = Bugzilla->dbh;
my ($user_id, $login_cookie);
my $ip_addr = remote_ip();
my $login_cookie = $cgi->cookie("Bugzilla_logincookie");
my $user_id = $cgi->cookie("Bugzilla_login");
if (!Bugzilla->request_cache->{auth_no_automatic_login}) {
$login_cookie = $cgi->cookie("Bugzilla_logincookie");
$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;
# 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;
}
}
unless ($user_id) {
my $cookie = first {$_->name eq 'Bugzilla_login'}
@{$cgi->{'Bugzilla_cookie_list'}};
$user_id = $cookie->value if $cookie;
# If no cookies were provided, we also look for a login token
# passed in the parameters of a webservice
my $token = $self->login_token;
if ($token && (!$login_cookie || !$user_id)) {
($user_id, $login_cookie) = ($token->{'user_id'}, $token->{'login_token'});
}
my $ip_addr = remote_ip();
if ($login_cookie && $user_id) {
# Anything goes for these params - they're just strings which
# we're going to verify against the db
......@@ -75,4 +90,32 @@ sub get_login_info {
return { failure => AUTH_NODATA };
}
sub login_token {
my ($self) = @_;
my $input = Bugzilla->input_params;
my $usage_mode = Bugzilla->usage_mode;
return $self->{'_login_token'} if exists $self->{'_login_token'};
if ($usage_mode ne USAGE_MODE_XMLRPC
&& $usage_mode ne USAGE_MODE_JSON)
{
return $self->{'_login_token'} = undef;
}
# Check if a token was passed in via requests for WebServices
my $token = trim(delete $input->{'Bugzilla_token'});
return $self->{'_login_token'} = undef if !$token;
my ($user_id, $login_token) = split('-', $token, 2);
if (!detaint_natural($user_id) || !$login_token) {
return $self->{'_login_token'} = undef;
}
return $self->{'_login_token'} = {
user_id => $user_id,
login_token => $login_token
};
}
1;
......@@ -52,6 +52,10 @@ sub persist_login {
$dbh->bz_commit_transaction();
# We do not want WebServices to generate login cookies.
# All we need is the login token for User.login.
return $login_cookie if i_am_webservice();
# Prevent JavaScript from accessing login cookies.
my %cookieargs = ('-httponly' => 1);
......@@ -84,6 +88,7 @@ sub logout {
my $dbh = Bugzilla->dbh;
my $cgi = Bugzilla->cgi;
my $input = Bugzilla->input_params;
$param = {} unless $param;
my $user = $param->{user} || Bugzilla->user;
my $type = $param->{type} || LOGOUT_ALL;
......@@ -97,16 +102,24 @@ sub logout {
# 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 @login_cookies;
my $cookie = first {$_->name eq 'Bugzilla_logincookie'}
@{$cgi->{'Bugzilla_cookie_list'}};
my $login_cookie;
if ($cookie) {
$login_cookie = $cookie->value;
push(@login_cookies, $cookie->value);
}
elsif ($cookie = $cgi->cookie("Bugzilla_logincookie")) {
push(@login_cookies, $cookie);
}
else {
$login_cookie = $cgi->cookie("Bugzilla_logincookie") || '';
# If we are a webservice using a token instead of cookie
# then add that as well to the login cookies to delete
if (my $login_token = $user->authorizer->login_token) {
push(@login_cookies, $login_token->{'login_token'});
}
trick_taint($login_cookie);
# Make sure that @login_cookies is not empty to not break SQL statements.
push(@login_cookies, '') unless @login_cookies;
# 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
......@@ -115,12 +128,18 @@ sub logout {
# 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
map { trick_taint($_) } @login_cookies;
@login_cookies = map { $dbh->quote($_) } @login_cookies;
if ($type == LOGOUT_KEEP_CURRENT) {
$dbh->do("DELETE FROM logincookies WHERE cookie != ? AND userid = ?",
undef, $login_cookie, $user->id);
$dbh->do("DELETE FROM logincookies WHERE " .
$dbh->sql_in('cookie', \@login_cookies, 1) .
" AND userid = ?",
undef, $user->id);
} elsif ($type == LOGOUT_CURRENT) {
$dbh->do("DELETE FROM logincookies WHERE cookie = ? AND userid = ?",
undef, $login_cookie, $user->id);
$dbh->do("DELETE FROM logincookies WHERE " .
$dbh->sql_in('cookie', \@login_cookies) .
" AND userid = ?",
undef, $user->id);
} else {
die("Invalid type $type supplied to logout()");
}
......@@ -128,7 +147,6 @@ sub logout {
if ($type != LOGOUT_KEEP_CURRENT) {
clear_browser_cookies();
}
}
sub clear_browser_cookies {
......
......@@ -511,8 +511,10 @@ sub possible_duplicates {
my $dbh = Bugzilla->dbh;
my $user = Bugzilla->user;
my @words = split(/[\b\s]+/, $short_desc || '');
# Exclude punctuation from the array.
@words = map { /(\w+)/; $1 } @words;
# Remove leading/trailing punctuation from words
foreach my $word (@words) {
$word =~ s/(?:^\W+|\W+$)//g;
}
# And make sure that each word is longer than 2 characters.
@words = grep { defined $_ and length($_) > 2 } @words;
......@@ -2747,31 +2749,23 @@ sub add_comment {
push(@{$self->{added_comments}}, $params);
}
# There was a lot of duplicate code when I wrote this as three separate
# functions, so I just combined them all into one. This is also easier for
# process_bug to use.
sub modify_keywords {
my ($self, $keywords, $action) = @_;
$action ||= 'set';
if (!grep($action eq $_, qw(add remove set))) {
if (!$action || !grep { $action eq $_ } qw(add remove set)) {
$action = 'set';
}
$keywords = $self->_check_keywords($keywords);
my @old_keywords = @{ $self->keyword_objects };
my @result;
my (@result, $any_changes);
if ($action eq 'set') {
@result = @$keywords;
# Check if anything was added or removed.
my @old_ids = map { $_->id } @{$self->keyword_objects};
my @new_ids = map { $_->id } @result;
my ($removed, $added) = diff_arrays(\@old_ids, \@new_ids);
$any_changes = scalar @$removed || scalar @$added;
}
else {
# We're adding or deleting specific keywords.
my %keys = map {$_->id => $_} @{$self->keyword_objects};
my %keys = map { $_->id => $_ } @old_keywords;
if ($action eq 'add') {
$keys{$_->id} = $_ foreach @$keywords;
}
......@@ -2779,11 +2773,17 @@ sub modify_keywords {
delete $keys{$_->id} foreach @$keywords;
}
@result = values %keys;
$any_changes = scalar @$keywords;
}
# Check if anything was added or removed.
my @old_ids = map { $_->id } @old_keywords;
my @new_ids = map { $_->id } @result;
my ($removed, $added) = diff_arrays(\@old_ids, \@new_ids);
my $any_changes = scalar @$removed || scalar @$added;
# Make sure we retain the sort order.
@result = sort {lc($a->name) cmp lc($b->name)} @result;
if ($any_changes) {
my $privs;
my $new = join(', ', (map {$_->name} @result));
......@@ -3776,17 +3776,24 @@ sub editable_bug_fields {
# Join with bug_status and bugs tables to show bugs with open statuses first,
# and then the others
sub EmitDependList {
my ($myfield, $targetfield, $bug_id) = (@_);
my ($my_field, $target_field, $bug_id, $exclude_resolved) = @_;
my $cache = Bugzilla->request_cache->{bug_dependency_list} ||= {};
my $dbh = Bugzilla->dbh;
my $list_ref = $dbh->selectcol_arrayref(
"SELECT $targetfield
$exclude_resolved = $exclude_resolved ? 1 : 0;
my $is_open_clause = $exclude_resolved ? 'AND is_open = 1' : '';
$cache->{"${target_field}_sth_$exclude_resolved"} ||= $dbh->prepare(
"SELECT $target_field
FROM dependencies
INNER JOIN bugs ON dependencies.$targetfield = bugs.bug_id
INNER JOIN bugs ON dependencies.$target_field = bugs.bug_id
INNER JOIN bug_status ON bugs.bug_status = bug_status.value
WHERE $myfield = ?
ORDER BY is_open DESC, $targetfield",
undef, $bug_id);
return $list_ref;
WHERE $my_field = ? $is_open_clause
ORDER BY is_open DESC, $target_field");
return $dbh->selectcol_arrayref(
$cache->{"${target_field}_sth_$exclude_resolved"},
undef, $bug_id);
}
# Creates a lot of bug objects in the same order as the input array.
......
......@@ -286,6 +286,7 @@ sub close_standby_message {
# Override header so we can add the cookies in
sub header {
my $self = shift;
my $user = Bugzilla->user;
# If there's only one parameter, then it's a Content-Type.
if (scalar(@_) == 1) {
......@@ -293,6 +294,18 @@ sub header {
unshift(@_, '-type' => shift(@_));
}
if (!$user->id && $user->authorizer->can_login
&& !$self->cookie('Bugzilla_login_request_cookie'))
{
my %args;
$args{'-secure'} = 1 if Bugzilla->params->{ssl_redirect};
$self->send_cookie(-name => 'Bugzilla_login_request_cookie',
-value => generate_random_password(),
-httponly => 1,
%args);
}
# Add the cookies in if we have any
if (scalar(@{$self->{Bugzilla_cookie_list}})) {
unshift(@_, '-cookie' => $self->{Bugzilla_cookie_list});
......
......@@ -182,7 +182,7 @@ use Memoize;
# CONSTANTS
#
# Bugzilla version
use constant BUGZILLA_VERSION => "4.4.2";
use constant BUGZILLA_VERSION => "4.4.3";
# Location of the remote and local XML files to track new releases.
use constant REMOTE_FILE => 'http://updates.bugzilla.org/bugzilla-update.xml';
......
......@@ -645,6 +645,17 @@ sub create {
my ($data) = @_;
return encode_base64($data);
},
# Strips out control characters excepting whitespace
strip_control_chars => sub {
my ($data) = @_;
# Only run for utf8 to avoid issues with other multibyte encodings
# that may be reassigning meaning to ascii characters.
if (Bugzilla->params->{'utf8'}) {
$data =~ s/(?![\t\r\n])[[:cntrl:]]//g;
}
return $data;
},
# HTML collapses newlines in element attributes to a single space,
# so form elements which may have whitespace (ie comments) need
......@@ -889,6 +900,11 @@ sub create {
# Allow templates to generate a token themselves.
'issue_hash_token' => \&Bugzilla::Token::issue_hash_token,
'get_login_request_token' => sub {
my $cookie = Bugzilla->cgi->cookie('Bugzilla_login_request_cookie');
return $cookie ? issue_hash_token(['login_request', $cookie]) : '';
},
# A way for all templates to get at Field data, cached.
'bug_fields' => sub {
my $cache = Bugzilla->request_cache;
......
......@@ -13,8 +13,8 @@ use base qw(Exporter);
@Bugzilla::Util::EXPORT = qw(trick_taint detaint_natural detaint_signed
html_quote url_quote xml_quote
css_class_quote html_light_quote
i_am_cgi correct_urlbase remote_ip validate_ip
do_ssl_redirect_if_required use_attachbase
i_am_cgi i_am_webservice correct_urlbase remote_ip
validate_ip do_ssl_redirect_if_required use_attachbase
diff_arrays on_main_db say
trim wrap_hard wrap_comment find_wrap_point
format_time validate_date validate_time datetime_from
......@@ -68,6 +68,10 @@ sub html_quote {
# Obscure '@'.
$var =~ s/\@/\&#64;/g;
if (Bugzilla->params->{'utf8'}) {
# Remove control characters if the encoding is utf8.
# Other multibyte encodings may be using this range; so ignore if not utf8.
$var =~ s/(?![\t\r\n])[[:cntrl:]]//g;
# Remove the following characters because they're
# influencing BiDi:
# --------------------------------------------------------
......@@ -230,6 +234,12 @@ sub i_am_cgi {
return exists $ENV{'SERVER_SOFTWARE'} ? 1 : 0;
}
sub i_am_webservice {
my $usage_mode = Bugzilla->usage_mode;
return $usage_mode == USAGE_MODE_XMLRPC
|| $usage_mode == USAGE_MODE_JSON;
}
# This exists as a separate function from Bugzilla::CGI::redirect_to_https
# because we don't want to create a CGI object during XML-RPC calls
# (doing so can mess up XML-RPC).
......@@ -862,6 +872,7 @@ Bugzilla::Util - Generic utility functions for bugzilla
# Functions that tell you about your environment
my $is_cgi = i_am_cgi();
my $is_webservice = i_am_webservice();
my $urlbase = correct_urlbase();
# Data manipulation
......@@ -989,6 +1000,11 @@ Tells you whether or not you are being run as a CGI script in a web
server. For example, it would return false if the caller is running
in a command-line script.
=item C<i_am_webservice()>
Tells you whether or not the current usage mode is WebServices related
such as JSONRPC or XMLRPC.
=item C<correct_urlbase()>
Returns either the C<sslbase> or C<urlbase> parameter, depending on the
......
......@@ -128,9 +128,7 @@ There are various ways to log in:
=item C<User.login>
You can use L<Bugzilla::WebService::User/login> to log in as a Bugzilla
user. This issues standard HTTP cookies that you must then use in future
calls, so your client must be capable of receiving and transmitting
cookies.
user. This issues a token that you must then use in future calls.
=item C<Bugzilla_login> and C<Bugzilla_password>
......@@ -150,19 +148,21 @@ WebService method to perform a login:
=item C<Bugzilla_restrictlogin> (boolean) - Optional. If true,
then your login will only be valid for your IP address.
=item C<Bugzilla_rememberlogin> (boolean) - Optional. If true,
then the cookie sent back to you with the method response will
not expire.
=back
The C<Bugzilla_restrictlogin> and C<Bugzilla_rememberlogin> options
are only used when you have also specified C<Bugzilla_login> and
C<Bugzilla_password>.
The C<Bugzilla_restrictlogin> option is only used when you have also
specified C<Bugzilla_login> and C<Bugzilla_password>.
=item C<Bugzilla_token>
B<Added in Bugzilla 5.0 and backported to 4.4.3>
You can specify C<Bugzilla_token> as argument to any WebService method,
and you will be logged in as that user if the token is correct. This is
the token returned when calling C<User.login> mentioned above.
Note that Bugzilla will return HTTP cookies along with the method
response when you use these arguments (just like the C<User.login> method
above).
Support for using login cookies for authentication has been dropped
for security reasons.