diff options
Diffstat (limited to 'eo/2')
0 files changed, 0 insertions, 0 deletions
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
################################################################################
# Module Initialization
################################################################################
# Make it harder for us to do dangerous things in Perl.
use strict;
# This module implements utilities for dealing with Bugzilla users.
package Bugzilla::User;
use Bugzilla::Error;
use Bugzilla::Util;
use Bugzilla::Constants;
use Bugzilla::Search::Recent;
use Bugzilla::User::Setting;
use Bugzilla::Product;
use Bugzilla::Classification;
use Bugzilla::Field;
use Bugzilla::Group;
use DateTime::TimeZone;
use List::Util qw(max);
use Scalar::Util qw(blessed);
use Storable qw(dclone);
use URI;
use URI::QueryParam;
use base qw(Bugzilla::Object Exporter);
@Bugzilla::User::EXPORT = qw(is_available_username
login_to_id user_id_to_login validate_password
USER_MATCH_MULTIPLE USER_MATCH_FAILED USER_MATCH_SUCCESS
MATCH_SKIP_CONFIRM
);
#####################################################################
# Constants
#####################################################################
use constant USER_MATCH_MULTIPLE => -1;
use constant USER_MATCH_FAILED => 0;
use constant USER_MATCH_SUCCESS => 1;
use constant MATCH_SKIP_CONFIRM => 1;
use constant DEFAULT_USER => {
'userid' => 0,
'realname' => '',
'login_name' => '',
'showmybugslink' => 0,
'disabledtext' => '',
'disable_mail' => 0,
'is_enabled' => 1,
};
use constant DB_TABLE => 'profiles';
# XXX Note that Bugzilla::User->name does not return the same thing
# that you passed in for "name" to new(). That's because historically
# Bugzilla::User used "name" for the realname field. This should be
# fixed one day.
sub DB_COLUMNS {
my $dbh = Bugzilla->dbh;
return (
'profiles.userid',
'profiles.login_name',
'profiles.realname',
'profiles.mybugslink AS showmybugslink',
'profiles.disabledtext',
'profiles.disable_mail',
'profiles.extern_id',
'profiles.is_enabled',
$dbh->sql_date_format('last_seen_date', '%Y-%m-%d') . ' AS last_seen_date',
),
}
use constant NAME_FIELD => 'login_name';
use constant ID_FIELD => 'userid';
use constant LIST_ORDER => NAME_FIELD;
use constant VALIDATORS => {
cryptpassword => \&_check_password,
disable_mail => \&_check_disable_mail,
disabledtext => \&_check_disabledtext,
login_name => \&check_login_name_for_creation,
realname => \&_check_realname,
extern_id => \&_check_extern_id,
is_enabled => \&_check_is_enabled,
};
sub UPDATE_COLUMNS {
my $self = shift;
my @cols = qw(
disable_mail
disabledtext
login_name
realname
extern_id
is_enabled
);
push(@cols, 'cryptpassword') if exists $self->{cryptpassword};
return @cols;
};
use constant VALIDATOR_DEPENDENCIES => {
is_enabled => ['disabledtext'],
};
use constant EXTRA_REQUIRED_FIELDS => qw(is_enabled);
################################################################################
# Functions
################################################################################
sub new {
my $invocant = shift;
my $class = ref($invocant) || $invocant;
my ($param) = @_;
my $user = DEFAULT_USER;
bless ($user, $class);
return $user unless $param;
if (ref($param) eq 'HASH') {
if (defined $param->{extern_id}) {
$param = { condition => 'extern_id = ?' , values => [$param->{extern_id}] };
$_[0] = $param;
}
}
return $class->SUPER::new(@_);
}
sub super_user {
my $invocant = shift;
my $class = ref($invocant) || $invocant;
my ($param) = @_;
my $user = dclone(DEFAULT_USER);
$user->{groups} = [Bugzilla::Group->get_all];
$user->{bless_groups} = [Bugzilla::Group->get_all];
bless $user, $class;
return $user;
}
sub update {
my $self = shift;
my $changes = $self->SUPER::update(@_);
my $dbh = Bugzilla->dbh;
if (exists $changes->{login_name}) {
# If we changed the login, silently delete any tokens.
$dbh->do('DELETE FROM tokens WHERE userid = ?', undef, $self->id);
# And rederive regex groups
$self->derive_regexp_groups();
}
# Logout the user if necessary.
Bugzilla->logout_user($self)
if (exists $changes->{login_name} || exists $changes->{disabledtext}
|| exists $changes->{cryptpassword});
# XXX Can update profiles_activity here as soon as it understands
# field names like login_name.
return $changes;
}
################################################################################
# Validators
################################################################################
sub _check_disable_mail { return $_[1] ? 1 : 0; }
sub _check_disabledtext { return trim($_[1]) || ''; }
# Check whether the extern_id is unique.
sub _check_extern_id {
my ($invocant, $extern_id) = @_;
$extern_id = trim($extern_id);
return undef unless defined($extern_id) && $extern_id ne "";
if (!ref($invocant) || $invocant->extern_id ne $extern_id) {
my $existing_login = $invocant->new({ extern_id => $extern_id });
if ($existing_login) {
ThrowUserError( 'extern_id_exists',
{ extern_id => $extern_id,
existing_login_name => $existing_login->login });
}
}
return $extern_id;
}
# This is public since createaccount.cgi needs to use it before issuing
# a token for account creation.
sub check_login_name_for_creation {
my ($invocant, $name) = @_;
$name = trim($name);
$name || ThrowUserError('user_login_required');
check_email_syntax($name);
# Check the name if it's a new user, or if we're changing the name.
if (!ref($invocant) || $invocant->login ne $name) {
is_available_username($name)
|| ThrowUserError('account_exists', { email => $name });
}
return $name;
}
sub _check_password {
my ($self, $pass) = @_;
# If the password is '*', do not encrypt it or validate it further--we
# are creating a user who should not be able to log in using DB
# authentication.
return $pass if $pass eq '*';
validate_password($pass);
my $cryptpassword = bz_crypt($pass);
return $cryptpassword;
}
sub _check_realname { return trim($_[1]) || ''; }
sub _check_is_enabled {
my ($invocant, $is_enabled, undef, $params) = @_;
# is_enabled is set automatically on creation depending on whether
# disabledtext is empty (enabled) or not empty (disabled).
# When updating the user, is_enabled is set by calling set_disabledtext().
# Any value passed into this validator is ignored.
my $disabledtext = ref($invocant) ? $invocant->disabledtext : $params->{disabledtext};
return $disabledtext ? 0 : 1;
}
################################################################################
# Mutators
################################################################################
sub set_disable_mail { $_[0]->set('disable_mail', $_[1]); }
sub set_extern_id { $_[0]->set('extern_id', $_[1]); }
sub set_login {
my ($self, $login) = @_;
$self->set('login_name', $login);
delete $self->{identity};
delete $self->{nick};
}
sub set_name {
my ($self, $name) = @_;
$self->set('realname', $name);
delete $self->{identity};
}
sub set_password { $_[0]->set('cryptpassword', $_[1]); }
sub set_disabledtext {
$_[0]->set('disabledtext', $_[1]);
$_[0]->set('is_enabled', $_[1] ? 0 : 1);
}
sub update_last_seen_date {
my $self = shift;
return unless $self->id;
my $dbh = Bugzilla->dbh;
my $date = $dbh->selectrow_array(
'SELECT ' . $dbh->sql_date_format('NOW()', '%Y-%m-%d'));
if (!$self->last_seen_date or $date ne $self->last_seen_date) {
$self->{last_seen_date} = $date;
# We don't use the normal update() routine here as we only
# want to update the last_seen_date column, not any other
# pending changes
$dbh->do("UPDATE profiles SET last_seen_date = ? WHERE userid = ?",
undef, $date, $self->id);
}
}
################################################################################
# Methods
################################################################################
# Accessors for user attributes
sub name { $_[0]->{realname}; }
sub login { $_[0]->{login_name}; }
sub extern_id { $_[0]->{extern_id}; }
sub email { $_[0]->login . Bugzilla->params->{'emailsuffix'}; }
sub disabledtext { $_[0]->{'disabledtext'}; }
sub is_enabled { $_[0]->{'is_enabled'} ? 1 : 0; }
sub showmybugslink { $_[0]->{showmybugslink}; }
sub email_disabled { $_[0]->{disable_mail}; }
sub email_enabled { !($_[0]->{disable_mail}); }
sub last_seen_date { $_[0]->{last_seen_date}; }
sub cryptpassword {
my $self = shift;
# We don't store it because we never want it in the object (we
# don't want to accidentally dump even the hash somewhere).
my ($pw) = Bugzilla->dbh->selectrow_array(
'SELECT cryptpassword FROM profiles WHERE userid = ?',
undef, $self->id);
return $pw;
}
sub set_authorizer {
my ($self, $authorizer) = @_;
$self->{authorizer} = $authorizer;
}
sub authorizer {
my ($self) = @_;
if (!$self->{authorizer}) {
require Bugzilla::Auth;
$self->{authorizer} = new Bugzilla::Auth();
}
return $self->{authorizer};
}
# Generate a string to identify the user by name + login if the user
# has a name or by login only if she doesn't.
sub identity {
my $self = shift;
return "" unless $self->id;
if (!defined $self->{identity}) {
$self->{identity} =
$self->name ? $self->name . " <" . $self->login. ">" : $self->login;
}
return $self->{identity};
}
sub nick {
my $self = shift;
return "" unless $self->id;
if (!defined $self->{nick}) {
$self->{nick} = (split(/@/, $self->login, 2))[0];
}
return $self->{nick};
}
sub queries {
my $self = shift;
return $self->{queries} if defined $self->{queries};
return [] unless $self->id;
my $dbh = Bugzilla->dbh;
my $query_ids = $dbh->selectcol_arrayref(
'SELECT id FROM namedqueries WHERE userid = ?', undef, $self->id);
require Bugzilla::Search::Saved;
$self->{queries} = Bugzilla::Search::Saved->new_from_list($query_ids);
# We preload link_in_footer from here as this information is always requested.
# This only works if the user object represents the current logged in user.
Bugzilla::Search::Saved::preload($self->{queries}) if $self->id == Bugzilla->user->id;
return $self->{queries};
}
sub queries_subscribed {
my $self = shift;
return $self->{queries_subscribed} if defined $self->{queries_subscribed};
return [] unless $self->id;
# Exclude the user's own queries.
my @my_query_ids = map($_->id, @{$self->queries});
my $query_id_string = join(',', @my_query_ids) || '-1';
# Only show subscriptions that we can still actually see. If a
# user changes the shared group of a query, our subscription
# will remain but we won't have access to the query anymore.
my $subscribed_query_ids = Bugzilla->dbh->selectcol_arrayref(
"SELECT lif.namedquery_id
FROM namedqueries_link_in_footer lif
INNER JOIN namedquery_group_map ngm
ON ngm.namedquery_id = lif.namedquery_id
WHERE lif.user_id = ?
AND lif.namedquery_id NOT IN ($query_id_string)
AND " . $self->groups_in_sql,
undef, $self->id);
require Bugzilla::Search::Saved;
$self->{queries_subscribed} =
Bugzilla::Search::Saved->new_from_list($subscribed_query_ids);
return $self->{queries_subscribed};
}
sub queries_available {
my $self = shift;
return $self->{queries_available} if defined $self->{queries_available};
return [] unless $self->id;
# Exclude the user's own queries.
my @my_query_ids = map($_->id, @{$self->queries});
my $query_id_string = join(',', @my_query_ids) || '-1';
my $avail_query_ids = Bugzilla->dbh->selectcol_arrayref(
'SELECT namedquery_id FROM namedquery_group_map
WHERE ' . $self->groups_in_sql . "
AND namedquery_id NOT IN ($query_id_string)");
require Bugzilla::Search::Saved;
$self->{queries_available} =
Bugzilla::Search::Saved->new_from_list($avail_query_ids);
return $self->{queries_available};
}
sub tags {
my $self = shift;
my $dbh = Bugzilla->dbh;
if (!defined $self->{tags}) {
# We must use LEFT JOIN instead of INNER JOIN as we may be
# in the process of inserting a new tag to some bugs,
# in which case there are no bugs with this tag yet.
$self->{tags} = $dbh->selectall_hashref(
'SELECT name, id, COUNT(bug_id) AS bug_count
FROM tag
LEFT JOIN bug_tag ON bug_tag.tag_id = tag.id
WHERE user_id = ? ' . $dbh->sql_group_by('id', 'name'),
'name', undef, $self->id);
}
return $self->{tags};
}
##########################
# Saved Recent Bug Lists #
##########################
sub recent_searches {
my $self = shift;
$self->{recent_searches} ||=
Bugzilla::Search::Recent->match({ user_id => $self->id });
return $self->{recent_searches};
}
sub recent_search_containing {
my ($self, $bug_id) = @_;
my $searches = $self->recent_searches;
foreach my $search (@$searches) {
return $search if grep($_ == $bug_id, @{ $search->bug_list });
}
return undef;
}
sub recent_search_for {
my ($self, $bug) = @_;
my $params = Bugzilla->input_params;
my $cgi = Bugzilla->cgi;
if ($self->id) {
# First see if there's a list_id parameter in the query string.
my $list_id = $params->{list_id};
if (!$list_id) {
# If not, check for "list_id" in the query string of the referer.
my $referer = $cgi->referer;
if ($referer) {
my $uri = URI->new($referer);
if ($uri->path =~ /buglist\.cgi$/) {
$list_id = $uri->query_param('list_id')
|| $uri->query_param('regetlastlist');
}
}
}
if ($list_id && $list_id ne 'cookie') {
# If we got a bad list_id (either some other user's or an expired
# one) don't crash, just don't return that list.
my $search = Bugzilla::Search::Recent->check_quietly(
{ id => $list_id });
return $search if $search;
}
# If there's no list_id, see if the current bug's id is contained
# in any of the user's saved lists.
my $search = $self->recent_search_containing($bug->id);
return $search if $search;
}
# Finally (or always, if we're logged out), if there's a BUGLIST cookie
# and the selected bug is in the list, then return the cookie as a fake
# Search::Recent object.
if (my $list = $cgi->cookie('BUGLIST')) {
# Also split on colons, which was used as a separator in old cookies.
my @bug_ids = split(/[:-]/, $list);
if (grep { $_ == $bug->id } @bug_ids) {
my $search = Bugzilla::Search::Recent->new_from_cookie(\@bug_ids);
return $search;
}
}
return undef;
}
sub save_last_search {
my ($self, $params) = @_;
my ($bug_ids, $order, $vars, $list_id) =
@$params{qw(bugs order vars list_id)};
my $cgi = Bugzilla->cgi;
if ($order) {
$cgi->send_cookie(-name => 'LASTORDER',
-value => $order,
-expires => 'Fri, 01-Jan-2038 00:00:00 GMT');
}
return if !@$bug_ids;
my $search;
if ($self->id) {
on_main_db {
if ($list_id) {
$search = Bugzilla::Search::Recent->check_quietly({ id => $list_id });
}
if ($search) {
if (join(',', @{$search->bug_list}) ne join(',', @$bug_ids)) {
$search->set_bug_list($bug_ids);
}
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
# identical bug list, then don't create a new one. This
# prevents people from writing over their whole
# recent-search list by just refreshing a saved search
# (which doesn't have list_id in the header) over and over.
my $list_string = join(',', @$bug_ids);
my $existing_search = Bugzilla::Search::Recent->match({
user_id => $self->id, bug_list => $list_string });
if (!scalar(@$existing_search)) {
$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};
}
# Logged-out users use a cookie to store a single last search. We don't
# override that cookie with the logged-in user's latest search, because
# if they did one search while logged out and another while logged in,
# they may still want to navigate through the search they made while
# logged out.
else {
my $bug_list = join('-', @$bug_ids);
if (length($bug_list) < 4000) {
$cgi->send_cookie(-name => 'BUGLIST',
-value => $bug_list,
-expires => 'Fri, 01-Jan-2038 00:00:00 GMT');
}
else {
$cgi->remove_cookie('BUGLIST');
$vars->{'toolong'} = 1;
}
}
return $search;
}
sub settings {
my ($self) = @_;
return $self->{'settings'} if (defined $self->{'settings'});
# IF the user is logged in
# THEN get the user's settings
# ELSE get default settings
if ($self->id) {
$self->{'settings'} = get_all_settings($self->id);
} else {
$self->{'settings'} = get_defaults();
}
return $self->{'settings'};
}
sub setting {
my ($self, $name) = @_;
return $self->settings->{$name}->{'value'};
}
sub timezone {
my $self = shift;
if (!defined $self->{timezone}) {
my $tz = $self->setting('timezone');
if ($tz eq 'local') {
# The user wants the local timezone of the server.
$self->{timezone} = Bugzilla->local_timezone;
}
else {
$self->{timezone} = DateTime::TimeZone->new(name => $tz);
}
}
return $self->{timezone};
}
sub flush_queries_cache {
my $self = shift;
delete $self->{queries};
delete $self->{queries_subscribed};
delete $self->{queries_available};
}
sub groups {
my $self = shift;
return $self->{groups} if defined $self->{groups};
return [] unless $self->id;
my $dbh = Bugzilla->dbh;
my $groups_to_check = $dbh->selectcol_arrayref(
q{SELECT DISTINCT group_id
FROM user_group_map
WHERE user_id = ? AND isbless = 0}, undef, $self->id);
my $rows = $dbh->selectall_arrayref(
"SELECT DISTINCT grantor_id, member_id
FROM group_group_map
WHERE grant_type = " . GROUP_MEMBERSHIP);
my %group_membership;
foreach my $row (@$rows) {
my ($grantor_id, $member_id) = @$row;
push (@{ $group_membership{$member_id} }, $grantor_id);
}
# Let's walk the groups hierarchy tree (using FIFO)
# On the first iteration it's pre-filled with direct groups
# membership. Later on, each group can add its own members into the
# FIFO. Circular dependencies are eliminated by checking
# $checked_groups{$member_id} hash values.
# As a result, %groups will have all the groups we are the member of.
my %checked_groups;
my %groups;
while (scalar(@$groups_to_check) > 0) {
# Pop the head group from FIFO
my $member_id = shift @$groups_to_check;
# Skip the group if we have already checked it
if (!$checked_groups{$member_id}) {
# Mark group as checked
$checked_groups{$member_id} = 1;
# Add all its members to the FIFO check list
# %group_membership contains arrays of group members
# for all groups. Accessible by group number.
my $members = $group_membership{$member_id};
my @new_to_check = grep(!$checked_groups{$_}, @$members);
push(@$groups_to_check, @new_to_check);
$groups{$member_id} = 1;
}
}
$self->{groups} = Bugzilla::Group->new_from_list([keys %groups]);
return $self->{groups};
}
# It turns out that calling ->id on objects a few hundred thousand
# times is pretty slow. (It showed up as a significant time contributor
# when profiling xt/search.t.) So we cache the group ids separately from
# groups for functions that need the group ids.
sub _group_ids {
my ($self) = @_;
$self->{group_ids} ||= [map { $_->id } @{ $self->groups }];
return $self->{group_ids};
}
sub groups_as_string {
my $self = shift;
my $ids = $self->_group_ids;
return scalar(@$ids) ? join(',', @$ids) : '-1';
}
sub groups_in_sql {
my ($self, $field) = @_;
$field ||= 'group_id';
my $ids = $self->_group_ids;
$ids = [-1] if !scalar @$ids;
return Bugzilla->dbh->sql_in($field, $ids);
}
sub bless_groups {
my $self = shift;
return $self->{'bless_groups'} if defined $self->{'bless_groups'};
return [] unless $self->id;
if ($self->in_group('editusers')) {
# Users having editusers permissions may bless all groups.
$self->{'bless_groups'} = [Bugzilla::Group->get_all];
return $self->{'bless_groups'};
}
my $dbh = Bugzilla->dbh;
# Get all groups for the user where:
# + They have direct bless privileges
# + They are a member of a group that inherits bless privs.
my @group_ids = map {$_->id} @{ $self->groups };
@group_ids = (-1) if !@group_ids;
my $query =
'SELECT DISTINCT groups.id
FROM groups, user_group_map, group_group_map AS ggm
WHERE user_group_map.user_id = ?
AND ( (user_group_map.isbless = 1
AND groups.id=user_group_map.group_id)
OR (groups.id = ggm.grantor_id
AND ggm.grant_type = ' . GROUP_BLESS . '
AND ' . $dbh->sql_in('ggm.member_id', \@group_ids)
. ') )';
# If visibilitygroups are used, restrict the set of groups.
if (Bugzilla->params->{'usevisibilitygroups'}) {
return [] if !$self->visible_groups_as_string;
# Users need to see a group in order to bless it.
$query .= " AND "
. $dbh->sql_in('groups.id', $self->visible_groups_inherited);
}
my $ids = $dbh->selectcol_arrayref($query, undef, $self->id);
return $self->{'bless_groups'} = Bugzilla::Group->new_from_list($ids);
}
sub in_group {
my ($self, $group, $product_id) = @_;
$group = $group->name if blessed $group;
if (scalar grep($_->name eq $group, @{ $self->groups })) {
return 1;
}
elsif ($product_id && detaint_natural($product_id)) {
# Make sure $group exists on a per-product basis.
return 0 unless (grep {$_ eq $group} PER_PRODUCT_PRIVILEGES);
$self->{"product_$product_id"} = {} unless exists $self->{"product_$product_id"};
if (!defined $self->{"product_$product_id"}->{$group}) {
my $dbh = Bugzilla->dbh;
my $in_group = $dbh->selectrow_array(
"SELECT 1
FROM group_control_map
WHERE product_id = ?
AND $group != 0
AND " . $self->groups_in_sql . ' ' .
$dbh->sql_limit(1),
undef, $product_id);
$self->{"product_$product_id"}->{$group} = $in_group ? 1 : 0;
}
return $self->{"product_$product_id"}->{$group};
}
# If we come here, then the user is not in the requested group.
return 0;
}
sub in_group_id {
my ($self, $id) = @_;
return grep($_->id == $id, @{ $self->groups }) ? 1 : 0;
}
sub get_products_by_permission {
my ($self, $group) = @_;
# Make sure $group exists on a per-product basis.
return [] unless (grep {$_ eq $group} PER_PRODUCT_PRIVILEGES);
my $product_ids = Bugzilla->dbh->selectcol_arrayref(
"SELECT DISTINCT product_id
FROM group_control_map
WHERE $group != 0