Bug 180635 - Enhance Bugzilla::User to store additional information

r=myk,jake


git-svn-id: svn://10.0.0.236/trunk@143228 18797224-902f-48f8-a5cc-f745e15eee43
This commit is contained in:
bbaetz%acm.org
2003-06-03 09:48:15 +00:00
parent 75019b0195
commit 726bcd99cc
31 changed files with 647 additions and 437 deletions

View File

@@ -111,14 +111,6 @@ sub Send($;$) {
# require abuse we do.
GetVersionTable();
# Since any email recipients must be rederived if the user has not
# been rederived since the most recent group change, figure out when that
# is once and determine the need to rederive users using the same DB
# access that gets the user's email address each time a person is
# processed.
SendSQL("SELECT MAX(last_changed) FROM groups");
($last_changed) = FetchSQLData();
# Make sure to clean up _all_ package vars here. Yuck...
$nametoexclude = $recipients->{'changer'} || "";
@{$force{'CClist'}} = (exists $recipients->{'cc'} &&
@@ -710,19 +702,13 @@ sub NewProcessOnePerson ($$$$$$$$$$$$$) {
return;
}
SendSQL("SELECT userid, (refreshed_when > " . SqlQuote($last_changed) .
") FROM profiles WHERE login_name = " . SqlQuote($person));
my ($userid, $current) = (FetchSQLData());
# This routine should really get passed a userid
# This rederives groups as a side effect
my $user = Bugzilla::User->new_from_login($person);
my $userid = $user->id;
$seen{$person} = 1;
detaint_natural($userid);
if (!$current) {
DeriveGroup($userid);
}
# if this person doesn't have permission to see info on this bug,
# return.
#
@@ -732,12 +718,11 @@ sub NewProcessOnePerson ($$$$$$$$$$$$$) {
# quietly disappear from their radar.
#
return unless CanSeeBug($id, $userid);
# Drop any non-insiders if the comment is private
return if (Param("insidergroup") &&
return if (Param("insidergroup") &&
($anyprivate != 0) &&
(!UserInGroup(Param("insidergroup"), $userid)));
(!$user->groups->{Param("insidergroup")}));
# We shouldn't send changedmail if this is a dependency mail, and any of
# the depending bugs is not visible to the user.
@@ -761,8 +746,8 @@ sub NewProcessOnePerson ($$$$$$$$$$$$$) {
}
# Don't send estimated_time if user not in the group, or not enabled
if ($f ne 'estimated_time' ||
UserInGroup(Param('timetrackinggroup'), $userid)) {
$user->groups->{Param('timetrackinggroup')}) {
my $desc = $fielddescription{$f};
$head .= FormatDouble($desc, $value);
}
@@ -781,7 +766,7 @@ sub NewProcessOnePerson ($$$$$$$$$$$$$) {
($diff->{'fieldname'} eq 'estimated_time' ||
$diff->{'fieldname'} eq 'remaining_time' ||
$diff->{'fieldname'} eq 'work_time')) {
if (UserInGroup(Param("timetrackinggroup"), $userid)) {
if ($user->groups->{Param("timetrackinggroup")}) {
$add_diff = 1;
}
} else {

View File

@@ -34,9 +34,6 @@ sub ThrowUserError {
$vars->{error} = $error;
# Need to do this until User.pm goes in, so that the footer is correct
$vars->{user} = $::vars->{user};
Bugzilla->dbh->do("UNLOCK TABLES") if $unlock_tables;
print Bugzilla->cgi->header();

View File

@@ -177,18 +177,14 @@ sub validate {
if ($requestee_email ne $flag->{'requestee'}->{'email'}) {
# We know the requestee exists because we ran
# Bugzilla::User::match_field before getting here.
# ConfirmGroup makes sure their group settings
# are up-to-date or calls DeriveGroups to update them.
my $requestee_id = &::DBname_to_id($requestee_email);
&::ConfirmGroup($requestee_id);
my $requestee = Bugzilla::User->new_from_login($requestee_email);
# Throw an error if the user can't see the bug.
if (!&::CanSeeBug($bug_id, $requestee_id))
if (!&::CanSeeBug($bug_id, $requestee->id))
{
ThrowUserError("flag_requestee_unauthorized",
{ flag_type => $flag->{'type'},
requestee =>
new Bugzilla::User($requestee_id),
requestee => $requestee,
bug_id => $bug_id,
attach_id =>
$flag->{target}->{attachment}->{id} });
@@ -198,13 +194,12 @@ sub validate {
# the requestee isn't in the group of insiders who can see it.
if ($flag->{target}->{attachment}->{exists}
&& $data->{'isprivate'}
&& &::Param("insidergroup")
&& !&::UserInGroup(&::Param("insidergroup"), $requestee_id))
&& Param("insidergroup")
&& !$requestee->in_group(Param("insidergroup")))
{
ThrowUserError("flag_requestee_unauthorized_attachment",
{ flag_type => $flag->{'type'},
requestee =>
new Bugzilla::User($requestee_id),
requestee => $requestee,
bug_id => $bug_id,
attach_id =>
$flag->{target}->{attachment}->{id} });
@@ -236,7 +231,7 @@ sub process {
my @old_summaries;
foreach my $flag (@$flags) {
my $summary = $flag->{'type'}->{'name'} . $flag->{'status'};
$summary .= "($flag->{'requestee'}->{'email'})" if $flag->{'requestee'};
$summary .= "(" . $flag->{'requestee'}->login . ")" if $flag->{'requestee'};
push(@old_summaries, $summary);
}
@@ -275,7 +270,7 @@ sub process {
my @new_summaries;
foreach my $flag (@$flags) {
my $summary = $flag->{'type'}->{'name'} . $flag->{'status'};
$summary .= "($flag->{'requestee'}->{'email'})" if $flag->{'requestee'};
$summary .= "(" . $flag->{'requestee'}->login . ")" if $flag->{'requestee'};
push(@new_summaries, $summary);
}
@@ -307,7 +302,7 @@ sub create {
# Insert a record for the flag into the flags table.
my $attach_id = $flag->{'target'}->{'attachment'}->{'id'} || "NULL";
my $requestee_id = $flag->{'requestee'} ? $flag->{'requestee'}->{'id'} : "NULL";
my $requestee_id = $flag->{'requestee'} ? $flag->{'requestee'}->id : "NULL";
&::SendSQL("INSERT INTO flags (id, type_id,
bug_id, attach_id,
requestee_id, setter_id, status,
@@ -317,7 +312,7 @@ sub create {
$flag->{'target'}->{'bug'}->{'id'},
$attach_id,
$requestee_id,
$flag->{'setter'}->{'id'},
" . $flag->{'setter'}->id . ",
'$flag->{'status'}',
$timestamp,
$timestamp)");
@@ -380,7 +375,7 @@ sub modify {
# the flag isn't specifically requestable
|| $status ne "?" # or the flag isn't being requested
|| ($flag->{'requestee'} # or the requestee hasn't changed
&& ($requestee_email eq $flag->{'requestee'}->{'email'})));
&& ($requestee_email eq $flag->{'requestee'}->login)));
# Since the status is validated, we know it's safe, but it's still
# tainted, so we have to detaint it before using it in a query.
@@ -568,14 +563,15 @@ sub notify {
{
my @new_cc_list;
foreach my $cc (split(/[, ]+/, $flag->{'type'}->{'cc_list'})) {
my $user_id = &::DBname_to_id($cc) || next;
# re-derive permissions if necessary
&::ConfirmGroup($user_id, TABLES_ALREADY_LOCKED);
my $ccuser = Bugzilla::User->new_from_login($cc,
TABLES_ALREADY_LOCKED)
|| next;
next if $flag->{'target'}->{'bug'}->{'restricted'}
&& !&::CanSeeBug($flag->{'target'}->{'bug'}->{'id'}, $user_id);
&& !&::CanSeeBug($flag->{'target'}->{'bug'}->{'id'}, $ccuser->id);
next if $flag->{'target'}->{'attachment'}->{'isprivate'}
&& Param("insidergroup")
&& !&::UserInGroup(Param("insidergroup"), $user_id);
&& !$ccuser->in_group(Param("insidergroup"));
push(@new_cc_list, $cc);
}
$flag->{'type'}->{'cc_list'} = join(", ", @new_cc_list);
@@ -646,7 +642,7 @@ sub perlify_record {
id => $id ,
type => Bugzilla::FlagType::get($type_id) ,
target => GetTarget($bug_id, $attach_id) ,
requestee => new Bugzilla::User($requestee_id) ,
requestee => $requestee_id ? new Bugzilla::User($requestee_id) : undef,
setter => new Bugzilla::User($setter_id) ,
status => $status ,
};

View File

@@ -219,20 +219,17 @@ sub validate {
&& trim($data->{"requestee_type-$id"}))
{
my $requestee_email = trim($data->{"requestee_type-$id"});
my $requestee_id = &::DBname_to_id($requestee_email);
# We know the requestee exists because we ran
# Bugzilla::User::match_field before getting here.
# ConfirmGroup makes sure their group settings
# are up-to-date or calls DeriveGroups to update them.
&::ConfirmGroup($requestee_id);
my $requestee = Bugzilla::User->new_from_login($requestee_email);
# Throw an error if the user can't see the bug.
if (!&::CanSeeBug($bug_id, $requestee_id))
if (!&::CanSeeBug($bug_id, $requestee->id))
{
ThrowUserError("flag_requestee_unauthorized",
{ flag_type => $flag_type,
requestee => new Bugzilla::User($requestee_id),
requestee => $requestee,
bug_id => $bug_id,
attach_id => $attach_id });
}
@@ -240,13 +237,13 @@ sub validate {
# Throw an error if the target is a private attachment and
# the requestee isn't in the group of insiders who can see it.
if ($attach_id
&& &::Param("insidergroup")
&& Param("insidergroup")
&& $data->{'isprivate'}
&& !&::UserInGroup(&::Param("insidergroup"), $requestee_id))
&& !$requestee->in_group(Param("insidergroup")))
{
ThrowUserError("flag_requestee_unauthorized_attachment",
{ flag_type => $flag_type,
requestee => new Bugzilla::User($requestee_id),
requestee => $requestee,
bug_id => $bug_id,
attach_id => $attach_id });
}

View File

@@ -926,28 +926,31 @@ sub init {
# Make sure we create a legal SQL query.
@andlist = ("1 = 1") if !@andlist;
my $user = Bugzilla->user;
my $query = "SELECT " . join(', ', @fields) .
" FROM $suppstring" .
" LEFT JOIN bug_group_map " .
" ON bug_group_map.bug_id = bugs.bug_id ";
if (defined @{$::vars->{user}{groupids}} && @{$::vars->{user}{groupids}} > 0) {
$query .= " AND bug_group_map.group_id NOT IN (" . join(',', @{$::vars->{user}{groupids}}) . ") ";
}
if ($user) {
if (%{$user->groups}) {
$query .= " AND bug_group_map.group_id NOT IN (" . join(',', values(%{$user->groups})) . ") ";
}
if ($::vars->{user}{userid}) {
$query .= " LEFT JOIN cc ON cc.bug_id = bugs.bug_id AND cc.who = $::userid ";
$query .= " LEFT JOIN cc ON cc.bug_id = bugs.bug_id AND cc.who = " . $user->id;
}
$query .= " WHERE " . join(' AND ', (@wherepart, @andlist)) .
" AND ((bug_group_map.group_id IS NULL)";
if ($::vars->{user}{userid}) {
$query .= " OR (bugs.reporter_accessible = 1 AND bugs.reporter = $::userid) " .
if ($user) {
my $userid = $user->id;
$query .= " OR (bugs.reporter_accessible = 1 AND bugs.reporter = $userid) " .
" OR (bugs.cclist_accessible = 1 AND cc.who IS NOT NULL) " .
" OR (bugs.assigned_to = $::userid) ";
" OR (bugs.assigned_to = $userid) ";
if (Param('useqacontact')) {
$query .= "OR (bugs.qa_contact = $::userid) ";
$query .= "OR (bugs.qa_contact = $userid) ";
}
}

View File

@@ -256,7 +256,10 @@ sub create {
# Generic linear search function
'lsearch' => \&Bugzilla::Util::lsearch,
# UserInGroup - you probably want to cache this
# Currently logged in user, if any
'user' => sub { return Bugzilla->user; },
# UserInGroup. Deprecated - use the user.* functions instead
'UserInGroup' => \&::UserInGroup,
# SendBugMail - sends mail about a bug, using Bugzilla::BugMail.pm

View File

@@ -19,6 +19,8 @@
#
# Contributor(s): Myk Melez <myk@mozilla.org>
# Erik Stambaugh <not_erik@dasbistro.com>
# Bradley Baetz <bbaetz@acm.org>
# Joel Peshkin <bugreport@peshkin.net>
################################################################################
# Module Initialization
@@ -30,57 +32,311 @@ use strict;
# This module implements utilities for dealing with Bugzilla users.
package Bugzilla::User;
use Bugzilla::Config;
use Bugzilla::Util;
################################################################################
# Functions
################################################################################
my $user_cache = {};
sub new {
# Returns a hash of information about a particular user.
my $invocant = shift;
return $invocant->_create("userid=?", @_);
}
# This routine is sort of evil. Nothing except the login stuff should
# be dealing with addresses as an input, and they can get the id as a
# side effect of the other sql they have to do anyway.
# Bugzilla::BugMail still does this, probably as a left over from the
# pre-id days. Provide this as a helper, but don't document it, and hope
# that it can go away.
# The request flag stuff also does this, but it really should be passing
# in the id its already had to validate (or the User.pm object, of course)
sub new_from_login {
my $invocant = shift;
return $invocant->_create("login_name=?", @_);
}
# Internal helper for the above |new| methods
# $cond is a string (including a placeholder ?) for the search
# requirement for the profiles table
sub _create {
my $invocant = shift;
my $class = ref($invocant) || $invocant;
my $exists = 1;
my ($id, $name, $email) = @_;
return undef if !$id;
return $user_cache->{$id} if exists($user_cache->{$id});
my $self = { 'id' => $id };
bless($self, $class);
if (!$name && !$email) {
&::PushGlobalSQLState();
&::SendSQL("SELECT 1, realname, login_name FROM profiles WHERE userid = $id");
($exists, $name, $email) = &::FetchSQLData();
&::PopGlobalSQLState();
}
$self->{'name'} = $name;
$self->{'email'} = $email || "__UNKNOWN__";
$self->{'exists'} = $exists;
# Generate a string to identify the user by name + email if the user
# has a name or by email only if she doesn't.
$self->{'identity'} = $name ? "$name <$email>" : $email;
# Generate a user "nickname" -- i.e. a shorter, not-necessarily-unique name
# by which to identify the user. Currently the part of the user's email
# address before the at sign (@), but that could change, especially if we
# implement usernames not dependent on email address.
my @email_components = split("@", $email);
$self->{'nick'} = $email_components[0];
$user_cache->{$id} = $self;
my $cond = shift;
my $val = shift;
# We're checking for validity here, so any value is OK
trick_taint($val);
my $tables_locked_for_derive_groups = shift;
my $dbh = Bugzilla->dbh;
my ($id,
$login,
$name,
$mybugslink) = $dbh->selectrow_array(qq{SELECT userid,
login_name,
realname,
mybugslink
FROM profiles
WHERE $cond},
undef,
$val);
return undef unless defined $id;
my $self = { id => $id,
name => $name,
login => $login,
showmybugslink => $mybugslink,
};
bless ($self, $class);
# Now update any old group information if needed
my $result = $dbh->selectrow_array(q{SELECT 1
FROM profiles, groups
WHERE userid=?
AND profiles.refreshed_when <=
groups.last_changed},
undef,
$id);
if ($result) {
$self->derive_groups($tables_locked_for_derive_groups);
}
return $self;
}
# Accessors for user attributes
sub id { $_[0]->{id}; }
sub login { $_[0]->{login}; }
sub email { $_[0]->{login}; }
sub name { $_[0]->{name}; }
sub showmybugslink { $_[0]->{showmybugslink}; }
# Generate a string to identify the user by name + email if the user
# has a name or by email only if she doesn't.
sub identity {
my $self = shift;
if (!defined $self->{identity}) {
$self->{identity} =
$self->{name} ? "$self->{name} <$self->{login}>" : $self->{login};
}
return $self->{identity};
}
sub nick {
my $self = shift;
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};
my $dbh = Bugzilla->dbh;
my $sth = $dbh->prepare(q{ SELECT name, query, linkinfooter
FROM namedqueries
WHERE userid=?
ORDER BY UPPER(name)});
$sth->execute($self->{id});
my @queries;
while (my $row = $sth->fetch) {
push (@queries, {
name => $row->[0],
query => $row->[1],
linkinfooter => $row->[2],
});
}
$self->{queries} = \@queries;
return $self->{queries};
}
sub flush_queries_cache {
my $self = shift;
delete $self->{queries};
}
sub groups {
my $self = shift;
return $self->{groups} if defined $self->{groups};
my $dbh = Bugzilla->dbh;
my $groups = $dbh->selectcol_arrayref(q{SELECT DISTINCT groups.name, group_id
FROM groups, user_group_map
WHERE groups.id=user_group_map.group_id
AND user_id=?
AND isbless=0},
{ Columns=>[1,2] },
$self->{id});
# The above gives us an arrayref [name, id, name, id, ...]
# Convert that into a hashref
my %groups = @$groups;
$self->{groups} = \%groups;
return $self->{groups};
}
sub in_group {
my ($self, $group) = @_;
# If we already have the info, just return it.
return defined($self->{groups}->{$group}) if defined $self->{groups};
# Otherwise, go check for it
my $dbh = Bugzilla->dbh;
my $res = $dbh->selectrow(q{SELECT 1
FROM groups, user_group_map
WHERE groups.id=user_group_map.group_id
AND user_group_map.user_id=?
AND isbless=0
AND groups.name=?},
undef,
$self->id,
$group);
return defined($res);
}
sub derive_groups {
my ($self, $already_locked) = @_;
my $id = $self->id;
my $dbh = Bugzilla->dbh;
my $sth;
$dbh->do(q{LOCK TABLES profiles WRITE,
user_group_map WRITE,
group_group_map READ,
groups READ}) unless $already_locked;
# avoid races, we are only up to date as of the BEGINNING of this process
my $time = $dbh->selectrow_array("SELECT NOW()");
# first remove any old derived stuff for this user
$dbh->do(q{DELETE FROM user_group_map
WHERE user_id = ?
AND isderived = 1},
undef,
$id);
my %groupidsadded = ();
# add derived records for any matching regexps
$sth = $dbh->prepare("SELECT id, userregexp FROM groups WHERE userregexp != ''");
$sth->execute;
my $group_insert;
while (my $row = $sth->fetch) {
if ($self->{login} =~ m/$row->[1]/i) {
$group_insert ||= $dbh->prepare(q{INSERT INTO user_group_map
(user_id, group_id, isbless, isderived)
VALUES (?, ?, 0, 1)});
$groupidsadded{$row->[0]} = 1;
$group_insert->execute($id, $row->[0]);
}
}
# Get a list of the groups of which the user is a member.
my %groupidschecked = ();
my @groupidstocheck = @{$dbh->selectcol_arrayref(q{SELECT group_id
FROM user_group_map
WHERE user_id=?},
undef,
$id)};
# Each group needs to be checked for inherited memberships once.
my $group_sth;
while (@groupidstocheck) {
my $group = shift @groupidstocheck;
if (!defined($groupidschecked{"$group"})) {
$groupidschecked{"$group"} = 1;
$group_sth ||= $dbh->prepare(q{SELECT grantor_id
FROM group_group_map
WHERE member_id=?
AND isbless=0});
$group_sth->execute($group);
while (my $groupid = $group_sth->fetchrow_array) {
if (!defined($groupidschecked{"$groupid"})) {
push(@groupidstocheck,$groupid);
}
if (!$groupidsadded{$groupid}) {
$groupidsadded{$groupid} = 1;
$group_insert ||= $dbh->prepare(q{INSERT INTO user_group_map
(user_id, group_id, isbless, isderived)
VALUES (?, ?, 0, 1)});
$group_insert->execute($id, $groupid);
}
}
}
}
$dbh->do(q{UPDATE profiles
SET refreshed_when = ?
WHERE userid=?},
undef,
$time,
$id);
$dbh->do("UNLOCK TABLES") unless $already_locked;
}
sub can_bless {
my $self = shift;
return $self->{can_bless} if defined $self->{can_bless};
my $dbh = Bugzilla->dbh;
# First check if the user can explicitly bless a group
my $res = $dbh->selectrow_arrayref(q{SELECT 1
FROM user_group_map
WHERE user_id=?
AND isbless=1},
undef,
$self->{id});
if (!$res) {
# Now check if user is a member of a group that can bless a group
$res = $dbh->selectrow_arrayref(q{SELECT 1
FROM user_group_map, group_group_map
WHERE user_group_map.user_id=?
AND user_group_map.group_id=member_id
AND group_group_map.isbless=1},
undef,
$self->{id});
}
$self->{can_bless} = $res ? 1 : 0;
return $self->{can_bless};
}
sub match {
# Generates a list of users whose login name (email address) or real name
# matches a substring or wildcard.
# This is also called if matches are disabled (for error checking), but
# in this case only the exact match code will end up running.
# $str contains the string to match, while $limit contains the
# maximum number of records to retrieve.
@@ -99,7 +355,8 @@ sub match {
my $wildstr = $str;
if ($wildstr =~ s/\*/\%/g) { # don't do wildcards if no '*' in the string
if ($wildstr =~ s/\*/\%/g && # don't do wildcards if no '*' in the string
Param('usermatchmode') ne 'off') { # or if we only want exact matches
# Build the query.
my $sqlstr = &::SqlQuote($wildstr);
@@ -159,7 +416,7 @@ sub match {
# order @users by alpha
@users = sort { uc($a->{'email'}) cmp uc($b->{'email'}) } @users;
@users = sort { uc($a->login) cmp uc($b->login) } @users;
return \@users;
}
@@ -251,9 +508,6 @@ sub match_field {
}
$fields = $expanded_fields;
# Skip all of this if the option has been turned off
return 1 if (&::Param('usermatchmode') eq 'off');
for my $field (keys %{$fields}) {
# Tolerate fields that do not exist.
@@ -312,14 +566,14 @@ sub match_field {
# skip confirmation for exact matches
if ((scalar(@{$users}) == 1)
&& (@{$users}[0]->{'email'} eq $query))
&& (@{$users}[0]->{'login'} eq $query))
{
# delimit with spaces if necessary
if ($vars->{'form'}->{$field}) {
$vars->{'form'}->{$field} .= " ";
}
$vars->{'form'}->{$field} .= @{$users}[0]->{'email'};
push @{$vars->{'mform'}->{$field}}, @{$users}[0]->{'email'};
$vars->{'form'}->{$field} .= @{$users}[0]->{'login'};
push @{$vars->{'mform'}->{$field}}, @{$users}[0]->{'login'};
next;
}
@@ -333,8 +587,8 @@ sub match_field {
if ($vars->{'form'}->{$field}) {
$vars->{'form'}->{$field} .= " ";
}
$vars->{'form'}->{$field} .= @{$users}[0]->{'email'};
push @{$vars->{'mform'}->{$field}}, @{$users}[0]->{'email'};
$vars->{'form'}->{$field} .= @{$users}[0]->{'login'};
push @{$vars->{'mform'}->{$field}}, @{$users}[0]->{'login'};
$need_confirm = 1 if &::Param('confirmuniqueusermatch');
}
@@ -443,3 +697,159 @@ sub email_prefs {
}
1;
__END__
=head1 NAME
Bugzilla::User - Object for a Bugzilla user
=head1 SYNOPSIS
use Bugzilla::User;
my $user = new Bugzilla::User($id);
=head1 DESCRIPTION
This package handles Bugzilla users. Data obtained from here is read-only;
there is currently no way to modify a user from this package.
Note that the currently logged in user (if any) is available via
L<Bugzilla-E<gt>user|Bugzilla/"user">.
=head1 METHODS
=over 4
=item C<new($userid)>
Creates a new C<Bugzilla::User> object for the given user id. Returns
C<undef> if no matching user is found.
=begin undocumented
=item C<new_from_login($login)>
Creates a new C<Bugzilla::User> object given the provided login. Returns
C<undef> if no matching user is found.
This routine should not be required in general; most scripts should be using
userids instead.
This routine and C<new> both take an extra optional argument, which is
passed as the argument to C<derive_groups> to avoid locking. See that
routine's documentation for details.
=end undocumented
=item C<id>
Returns the userid for this user.
=item C<login>
Returns the login name for this user.
=item C<email>
Returns the user's email address. Currently this is the same value as the
login.
=item C<name>
Returns the 'real' name for this user, if any.
=item C<showmybugslink>
Returns C<1> if the user has set his preference to show the 'My Bugs' link in
the page footer, and C<0> otherwise.
=item C<identity>
Retruns a string for the identity of the user. This will be of the form
C<name E<lt>emailE<gt>> if the user has specified a name, and C<email>
otherwise.
=item C<nick>
Returns a user "nickname" -- i.e. a shorter, not-necessarily-unique name by
which to identify the user. Currently the part of the user's email address
before the at sign (@), but that could change, especially if we implement
usernames not dependent on email address.
=item C<queries>
Returns an array of the user's named queries, sorted in a case-insensitive
order by name. Each entry is a hash with three keys:
=over
=item *
name - The name of the query
=item *
query - The text for the query
=item *
linkinfooter - Whether or not the query should be displayed in the footer.
=back
=item C<flush_queries_cache>
Some code modifies the set of stored queries. Because C<Bugzilla::User> does
not handle these modifications, but does cache the result of calling C<queries>
internally, such code must call this method to flush the cached result.
=item C<groups>
Returns a hashref of group names for groups the user is a member of. The keys
are the names of the groups, whilst the values are the respective group ids.
(This is so that a set of all groupids for groups the user is in can be
obtained by C<values(%{$user->groups})>.)
=item C<in_group>
Determines whether or not a user is in the given group. This method is mainly
intended for cases where we are not looking at the currently logged in user,
and only need to make a quick check for the group, where calling C<groups>
and getting all of the groups would be overkill.
=item C<derive_groups>
Bugzilla allows for group inheritance. When data about the user (or any of the
groups) changes, the database must be updated. Handling updated groups is taken
care of by the constructor. However, when updating the email address, the
user may be placed into different groups, based on a new email regexp. This
method should be called in such a case to force reresolution of these groups.
=begin undocumented
This routine takes an optional argument. If true, then this routine will not
lock the tables, but will rely on the caller to ahve done so itsself.
This is required because mysql will only execute a query if all of the tables
are locked, or if none of them are, not a mixture. If the caller has already
done some locking, then this routine would fail. Thus the caller needs to lock
all the tables required by this method, and then C<derive_groups> won't do
any locking.
This is a really ugly solution, and when Bugzilla supports transactions
instead of using the explicit table locking we were forced to do when thats
all MySQL supported, this will go away.
=end undocumented
=item C<can_bless>
Returns C<1> if the user can bless at least one group. Otherwise returns C<0>.
=back
=head1 SEE ALSO
L<Bugzilla|Bugzilla>