X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=rt%2Flib%2FRT%2FUser.pm;h=b8a51f064a97d3aed95d0f34b92e4e2f0bfc24e3;hp=cbc10f5b4c38b9972c49da498198e6ee771aedcd;hb=9aee669886202be7035e6c6049fc71bc99dd3013;hpb=b5c4237a34aef94976bc343c8d9e138664fc3984 diff --git a/rt/lib/RT/User.pm b/rt/lib/RT/User.pm index cbc10f5b4..b8a51f064 100755 --- a/rt/lib/RT/User.pm +++ b/rt/lib/RT/User.pm @@ -1,76 +1,1948 @@ -# BEGIN LICENSE BLOCK -# -# Copyright (c) 1996-2003 Jesse Vincent -# -# (Except where explictly superceded by other copyright notices) -# +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +# +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# # This work is made available to you under the terms of Version 2 of # the GNU General Public License. A copy of that license should have # been provided with this software, but in any event can be snarfed # from www.gnu.org. -# +# # This work is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. -# -# Unless otherwise specified, all modifications, corrections or -# extensions to this work which alter its source code become the -# property of Best Practical Solutions, LLC when submitted for -# inclusion in the work. -# -# -# END LICENSE BLOCK -# Autogenerated by DBIx::SearchBuilder factory (by ) -# WARNING: THIS FILE IS AUTOGENERATED. ALL CHANGES TO THIS FILE WILL BE LOST. -# -# !! DO NOT EDIT THIS FILE !! # +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +=head1 NAME + + RT::User - RT User object + +=head1 SYNOPSIS + + use RT::User; + +=head1 DESCRIPTION + +=head1 METHODS + +=cut + + +package RT::User; + +use strict; +use warnings; + +use Scalar::Util qw(blessed); + +use base 'RT::Record'; + +sub Table {'Users'} + + + + + + +use Digest::SHA; +use Digest::MD5; +use Crypt::Eksblowfish::Bcrypt qw(); +use RT::Principals; +use RT::ACE; +use RT::Interface::Email; +use Text::Password::Pronounceable; + +sub _OverlayAccessible { + { + + Name => { public => 1, admin => 1 }, # loc_left_pair + Password => { read => 0 }, + EmailAddress => { public => 1 }, # loc_left_pair + Organization => { public => 1, admin => 1 }, # loc_left_pair + RealName => { public => 1 }, # loc_left_pair + NickName => { public => 1 }, # loc_left_pair + Lang => { public => 1 }, # loc_left_pair + EmailEncoding => { public => 1 }, + WebEncoding => { public => 1 }, + ExternalContactInfoId => { public => 1, admin => 1 }, + ContactInfoSystem => { public => 1, admin => 1 }, + ExternalAuthId => { public => 1, admin => 1 }, + AuthSystem => { public => 1, admin => 1 }, + Gecos => { public => 1, admin => 1 }, # loc_left_pair + PGPKey => { public => 1, admin => 1 }, # loc_left_pair + SMIMECertificate => { public => 1, admin => 1 }, # loc_left_pair + City => { public => 1 }, # loc_left_pair + Country => { public => 1 }, # loc_left_pair + Timezone => { public => 1 }, # loc_left_pair + } +} + + + +=head2 Create { PARAMHASH } + + + +=cut + + +sub Create { + my $self = shift; + my %args = ( + Privileged => 0, + Disabled => 0, + EmailAddress => '', + _RecordTransaction => 1, + @_ # get the real argumentlist + ); + + # remove the value so it does not cripple SUPER::Create + my $record_transaction = delete $args{'_RecordTransaction'}; + + #Check the ACL + unless ( $self->CurrentUser->HasRight(Right => 'AdminUsers', Object => $RT::System) ) { + return ( 0, $self->loc('Permission Denied') ); + } + + + unless ($self->CanonicalizeUserInfo(\%args)) { + return ( 0, $self->loc("Could not set user info") ); + } + + $args{'EmailAddress'} = $self->CanonicalizeEmailAddress($args{'EmailAddress'}); + + # if the user doesn't have a name defined, set it to the email address + $args{'Name'} = $args{'EmailAddress'} unless ($args{'Name'}); + + + + my $privileged = delete $args{'Privileged'}; + + + if ($args{'CryptedPassword'} ) { + $args{'Password'} = $args{'CryptedPassword'}; + delete $args{'CryptedPassword'}; + } elsif ( !$args{'Password'} ) { + $args{'Password'} = '*NO-PASSWORD*'; + } else { + my ($ok, $msg) = $self->ValidatePassword($args{'Password'}); + return ($ok, $msg) if !$ok; + + $args{'Password'} = $self->_GeneratePassword($args{'Password'}); + } + + #TODO Specify some sensible defaults. + + unless ( $args{'Name'} ) { + return ( 0, $self->loc("Must specify 'Name' attribute") ); + } + + my ( $val, $msg ) = $self->ValidateName( $args{'Name'} ); + return ( 0, $msg ) unless $val; + ( $val, $msg ) = $self->ValidateEmailAddress( $args{'EmailAddress'} ); + return ( 0, $msg ) unless ($val); + + $RT::Handle->BeginTransaction(); + # Groups deal with principal ids, rather than user ids. + # When creating this user, set up a principal Id for it. + my $principal = RT::Principal->new($self->CurrentUser); + my $principal_id = $principal->Create(PrincipalType => 'User', + Disabled => $args{'Disabled'}, + ObjectId => '0'); + # If we couldn't create a principal Id, get the fuck out. + unless ($principal_id) { + $RT::Handle->Rollback(); + $RT::Logger->crit("Couldn't create a Principal on new user create."); + $RT::Logger->crit("Strange things are afoot at the circle K"); + return ( 0, $self->loc('Could not create user') ); + } + + $principal->__Set(Field => 'ObjectId', Value => $principal_id); + delete $args{'Disabled'}; + + $self->SUPER::Create(id => $principal_id , %args); + my $id = $self->Id; + + #If the create failed. + unless ($id) { + $RT::Handle->Rollback(); + $RT::Logger->error("Could not create a new user - " .join('-', %args)); + + return ( 0, $self->loc('Could not create user') ); + } + + my $aclstash = RT::Group->new($self->CurrentUser); + my $stash_id = $aclstash->_CreateACLEquivalenceGroup($principal); + + unless ($stash_id) { + $RT::Handle->Rollback(); + $RT::Logger->crit("Couldn't stash the user in groupmembers"); + return ( 0, $self->loc('Could not create user') ); + } + + + my $everyone = RT::Group->new($self->CurrentUser); + $everyone->LoadSystemInternalGroup('Everyone'); + unless ($everyone->id) { + $RT::Logger->crit("Could not load Everyone group on user creation."); + $RT::Handle->Rollback(); + return ( 0, $self->loc('Could not create user') ); + } + + + my ($everyone_id, $everyone_msg) = $everyone->_AddMember( InsideTransaction => 1, PrincipalId => $self->PrincipalId); + unless ($everyone_id) { + $RT::Logger->crit("Could not add user to Everyone group on user creation."); + $RT::Logger->crit($everyone_msg); + $RT::Handle->Rollback(); + return ( 0, $self->loc('Could not create user') ); + } + + + my $access_class = RT::Group->new($self->CurrentUser); + if ($privileged) { + $access_class->LoadSystemInternalGroup('Privileged'); + } else { + $access_class->LoadSystemInternalGroup('Unprivileged'); + } + + unless ($access_class->id) { + $RT::Logger->crit("Could not load Privileged or Unprivileged group on user creation"); + $RT::Handle->Rollback(); + return ( 0, $self->loc('Could not create user') ); + } + + + my ($ac_id, $ac_msg) = $access_class->_AddMember( InsideTransaction => 1, PrincipalId => $self->PrincipalId); + + unless ($ac_id) { + $RT::Logger->crit("Could not add user to Privileged or Unprivileged group on user creation. Aborted"); + $RT::Logger->crit($ac_msg); + $RT::Handle->Rollback(); + return ( 0, $self->loc('Could not create user') ); + } + + + if ( $record_transaction ) { + $self->_NewTransaction( Type => "Create" ); + } + + $RT::Handle->Commit; + + return ( $id, $self->loc('User created') ); +} + +=head2 ValidateName STRING + +Returns either (0, "failure reason") or 1 depending on whether the given +name is valid. + +=cut + +sub ValidateName { + my $self = shift; + my $name = shift; + + return ( 0, $self->loc('empty name') ) unless defined $name && length $name; + + my $TempUser = RT::User->new( RT->SystemUser ); + $TempUser->Load($name); + + if ( $TempUser->id && ( !$self->id || $TempUser->id != $self->id ) ) { + return ( 0, $self->loc('Name in use') ); + } + else { + return 1; + } +} + +=head2 ValidatePassword STRING + +Returns either (0, "failure reason") or 1 depending on whether the given +password is valid. + +=cut + +sub ValidatePassword { + my $self = shift; + my $password = shift; + + if ( length($password) < RT->Config->Get('MinimumPasswordLength') ) { + return ( 0, $self->loc("Password needs to be at least [quant,_1,character,characters] long", RT->Config->Get('MinimumPasswordLength')) ); + } + + return 1; +} + +=head2 SetPrivileged BOOL + +If passed a true value, makes this user a member of the "Privileged" PseudoGroup. +Otherwise, makes this user a member of the "Unprivileged" pseudogroup. + +Returns a standard RT tuple of (val, msg); + + +=cut + +sub SetPrivileged { + my $self = shift; + my $val = shift; + + #Check the ACL + unless ( $self->CurrentUser->HasRight(Right => 'AdminUsers', Object => $RT::System) ) { + return ( 0, $self->loc('Permission Denied') ); + } + + $self->_SetPrivileged($val); +} + +sub _SetPrivileged { + my $self = shift; + my $val = shift; + my $priv = RT::Group->new($self->CurrentUser); + $priv->LoadSystemInternalGroup('Privileged'); + unless ($priv->Id) { + $RT::Logger->crit("Could not find Privileged pseudogroup"); + return(0,$self->loc("Failed to find 'Privileged' users pseudogroup.")); + } + + my $unpriv = RT::Group->new($self->CurrentUser); + $unpriv->LoadSystemInternalGroup('Unprivileged'); + unless ($unpriv->Id) { + $RT::Logger->crit("Could not find unprivileged pseudogroup"); + return(0,$self->loc("Failed to find 'Unprivileged' users pseudogroup")); + } + + my $principal = $self->PrincipalId; + if ($val) { + if ($priv->HasMember($principal)) { + #$RT::Logger->debug("That user is already privileged"); + return (0,$self->loc("That user is already privileged")); + } + if ($unpriv->HasMember($principal)) { + $unpriv->_DeleteMember($principal); + } else { + # if we had layered transactions, life would be good + # sadly, we have to just go ahead, even if something + # bogus happened + $RT::Logger->crit("User ".$self->Id." is neither privileged nor ". + "unprivileged. something is drastically wrong."); + } + my ($status, $msg) = $priv->_AddMember( InsideTransaction => 1, PrincipalId => $principal); + if ($status) { + return (1, $self->loc("That user is now privileged")); + } else { + return (0, $msg); + } + } else { + if ($unpriv->HasMember($principal)) { + #$RT::Logger->debug("That user is already unprivileged"); + return (0,$self->loc("That user is already unprivileged")); + } + if ($priv->HasMember($principal)) { + $priv->_DeleteMember( $principal ); + } else { + # if we had layered transactions, life would be good + # sadly, we have to just go ahead, even if something + # bogus happened + $RT::Logger->crit("User ".$self->Id." is neither privileged nor ". + "unprivileged. something is drastically wrong."); + } + my ($status, $msg) = $unpriv->_AddMember( InsideTransaction => 1, PrincipalId => $principal); + if ($status) { + return (1, $self->loc("That user is now unprivileged")); + } else { + return (0, $msg); + } + } +} + +=head2 Privileged + +Returns true if this user is privileged. Returns undef otherwise. + +=cut + +sub Privileged { + my $self = shift; + if ( RT->PrivilegedUsers->HasMember( $self->id ) ) { + return(1); + } else { + return(undef); + } +} + +#create a user without validating _any_ data. + +#To be used only on database init. +# We can't localize here because it's before we _have_ a loc framework + +sub _BootstrapCreate { + my $self = shift; + my %args = (@_); + + $args{'Password'} = '*NO-PASSWORD*'; + + + $RT::Handle->BeginTransaction(); + + # Groups deal with principal ids, rather than user ids. + # When creating this user, set up a principal Id for it. + my $principal = RT::Principal->new($self->CurrentUser); + my $principal_id = $principal->Create(PrincipalType => 'User', ObjectId => '0'); + $principal->__Set(Field => 'ObjectId', Value => $principal_id); + + # If we couldn't create a principal Id, get the fuck out. + unless ($principal_id) { + $RT::Handle->Rollback(); + $RT::Logger->crit("Couldn't create a Principal on new user create. Strange things are afoot at the circle K"); + return ( 0, 'Could not create user' ); + } + $self->SUPER::Create(id => $principal_id, %args); + my $id = $self->Id; + #If the create failed. + unless ($id) { + $RT::Handle->Rollback(); + return ( 0, 'Could not create user' ) ; #never loc this + } + + my $aclstash = RT::Group->new($self->CurrentUser); + my $stash_id = $aclstash->_CreateACLEquivalenceGroup($principal); + + unless ($stash_id) { + $RT::Handle->Rollback(); + $RT::Logger->crit("Couldn't stash the user in groupmembers"); + return ( 0, $self->loc('Could not create user') ); + } + + $RT::Handle->Commit(); + + return ( $id, 'User created' ); +} + +sub Delete { + my $self = shift; + + return ( 0, $self->loc('Deleting this object would violate referential integrity') ); + +} + +=head2 Load + +Load a user object from the database. Takes a single argument. +If the argument is numerical, load by the column 'id'. If a user +object or its subclass passed then loads the same user by id. +Otherwise, load by the "Name" column which is the user's textual +username. + +=cut + +sub Load { + my $self = shift; + my $identifier = shift || return undef; + + if ( $identifier !~ /\D/ ) { + return $self->SUPER::LoadById( $identifier ); + } elsif ( UNIVERSAL::isa( $identifier, 'RT::User' ) ) { + return $self->SUPER::LoadById( $identifier->Id ); + } else { + return $self->LoadByCol( "Name", $identifier ); + } +} + +=head2 LoadByEmail + +Tries to load this user object from the database by the user's email address. + +=cut + +sub LoadByEmail { + my $self = shift; + my $address = shift; + + # Never load an empty address as an email address. + unless ($address) { + return (undef); + } + + $address = $self->CanonicalizeEmailAddress($address); + + #$RT::Logger->debug("Trying to load an email address: $address"); + return $self->LoadByCol( "EmailAddress", $address ); +} + +=head2 LoadOrCreateByEmail ADDRESS + +Attempts to find a user who has the provided email address. If that fails, creates an unprivileged user with +the provided email address and loads them. Address can be provided either as L object +or string which is parsed using the module. + +Returns a tuple of the user's id and a status message. +0 will be returned in place of the user's id in case of failure. + +=cut + +sub LoadOrCreateByEmail { + my $self = shift; + my $email = shift; + + my ($message, $name); + if ( UNIVERSAL::isa( $email => 'Email::Address' ) ) { + ($email, $name) = ($email->address, $email->phrase); + } else { + ($email, $name) = RT::Interface::Email::ParseAddressFromHeader( $email ); + } + + $self->LoadByEmail( $email ); + $self->Load( $email ) unless $self->Id; + $message = $self->loc('User loaded'); + + unless( $self->Id ) { + my $val; + ($val, $message) = $self->Create( + Name => $email, + EmailAddress => $email, + RealName => $name, + Privileged => 0, + Comments => 'Autocreated when added as a watcher', + ); + unless ( $val ) { + # Deal with the race condition of two account creations at once + $self->LoadByEmail( $email ); + unless ( $self->Id ) { + sleep 5; + $self->LoadByEmail( $email ); + } + if ( $self->Id ) { + $RT::Logger->error("Recovered from creation failure due to race condition"); + $message = $self->loc("User loaded"); + } else { + $RT::Logger->crit("Failed to create user ". $email .": " .$message); + } + } + } + return wantarray ? (0, $message) : 0 unless $self->id; + return wantarray ? ($self->Id, $message) : $self->Id; +} + +=head2 ValidateEmailAddress ADDRESS + +Returns true if the email address entered is not in use by another user or is +undef or ''. Returns false if it's in use. + +=cut + +sub ValidateEmailAddress { + my $self = shift; + my $Value = shift; + + # if the email address is null, it's always valid + return (1) if ( !$Value || $Value eq "" ); + + if ( RT->Config->Get('ValidateUserEmailAddresses') ) { + # We only allow one valid email address + my @addresses = Email::Address->parse($Value); + return ( 0, $self->loc('Invalid syntax for email address') ) unless ( ( scalar (@addresses) == 1 ) && ( $addresses[0]->address ) ); + } + + + my $TempUser = RT::User->new(RT->SystemUser); + $TempUser->LoadByEmail($Value); + + if ( $TempUser->id && ( !$self->id || $TempUser->id != $self->id ) ) + { # if we found a user with that address + # it's invalid to set this user's address to it + return ( 0, $self->loc('Email address in use') ); + } else { #it's a valid email address + return (1); + } +} + +=head2 SetName + +Check to make sure someone else isn't using this name already + +=cut + +sub SetName { + my $self = shift; + my $Value = shift; + + my ( $val, $message ) = $self->ValidateName($Value); + if ($val) { + return $self->_Set( Field => 'Name', Value => $Value ); + } + else { + return ( 0, $message ); + } +} + +=head2 SetEmailAddress + +Check to make sure someone else isn't using this email address already +so that a better email address can be returned + +=cut + +sub SetEmailAddress { + my $self = shift; + my $Value = shift; + $Value = '' unless defined $Value; + + my ($val, $message) = $self->ValidateEmailAddress( $Value ); + if ( $val ) { + return $self->_Set( Field => 'EmailAddress', Value => $Value ); + } else { + return ( 0, $message ) + } + +} + +=head2 EmailFrequency + +Takes optional Ticket argument in paramhash. Returns a string, suitable +for localization, describing any notable properties about email delivery +to the user. This includes lack of email address, ticket-level +squelching (if C is provided in the paramhash), or user email +delivery preferences. + +Returns the empty string if there are no notable properties. + +=cut + +sub EmailFrequency { + my $self = shift; + my %args = ( + Ticket => undef, + @_ + ); + return '' unless $self->id && $self->id != RT->Nobody->id + && $self->id != RT->SystemUser->id; + return 'no email address set' # loc + unless my $email = $self->EmailAddress; + return 'email disabled for ticket' # loc + if $args{'Ticket'} && + grep lc $email eq lc $_->Content, $args{'Ticket'}->SquelchMailTo; + my $frequency = RT->Config->Get( 'EmailFrequency', $self ) || ''; + return 'receives daily digests' # loc + if $frequency =~ /daily/i; + return 'receives weekly digests' # loc + if $frequency =~ /weekly/i; + return 'email delivery suspended' # loc + if $frequency =~ /suspend/i; + return ''; +} + +=head2 CanonicalizeEmailAddress ADDRESS + +CanonicalizeEmailAddress converts email addresses into canonical form. +it takes one email address in and returns the proper canonical +form. You can dump whatever your proper local config is in here. Note +that it may be called as a static method; in this case the first argument +is class name not an object. + +=cut + +sub CanonicalizeEmailAddress { + my $self = shift; + my $email = shift; + # Example: the following rule would treat all email + # coming from a subdomain as coming from second level domain + # foo.com + if ( my $match = RT->Config->Get('CanonicalizeEmailAddressMatch') and + my $replace = RT->Config->Get('CanonicalizeEmailAddressReplace') ) + { + $email =~ s/$match/$replace/gi; + } + return ($email); +} + +=head2 CanonicalizeUserInfo HASH of ARGS + +CanonicalizeUserInfo can convert all User->Create options. +it takes a hashref of all the params sent to User->Create and +returns that same hash, by default nothing is done. + +This function is intended to allow users to have their info looked up via +an outside source and modified upon creation. + +=cut + +sub CanonicalizeUserInfo { + my $self = shift; + my $args = shift; + my $success = 1; + + return ($success); +} + + +=head2 Password and authentication related functions + +=head3 SetRandomPassword + +Takes no arguments. Returns a status code and a new password or an error message. +If the status is 1, the second value returned is the new password. +If the status is anything else, the new value returned is the error code. + +=cut + +sub SetRandomPassword { + my $self = shift; + + unless ( $self->CurrentUserCanModify('Password') ) { + return ( 0, $self->loc("Permission Denied") ); + } + + + my $min = ( RT->Config->Get('MinimumPasswordLength') > 6 ? RT->Config->Get('MinimumPasswordLength') : 6); + my $max = ( RT->Config->Get('MinimumPasswordLength') > 8 ? RT->Config->Get('MinimumPasswordLength') : 8); + + my $pass = $self->GenerateRandomPassword( $min, $max) ; + + # If we have "notify user on + + my ( $val, $msg ) = $self->SetPassword($pass); + + #If we got an error return the error. + return ( 0, $msg ) unless ($val); + + #Otherwise, we changed the password, lets return it. + return ( 1, $pass ); + +} + +=head3 ResetPassword + +Returns status, [ERROR or new password]. Resets this user's password to +a randomly generated pronouncable password and emails them, using a +global template called "PasswordChange". + +This function is currently unused in the UI, but available for local scripts. + +=cut + +sub ResetPassword { + my $self = shift; + + unless ( $self->CurrentUserCanModify('Password') ) { + return ( 0, $self->loc("Permission Denied") ); + } + my ( $status, $pass ) = $self->SetRandomPassword(); + + unless ($status) { + return ( 0, "$pass" ); + } + + my $ret = RT::Interface::Email::SendEmailUsingTemplate( + To => $self->EmailAddress, + Template => 'PasswordChange', + Arguments => { + NewPassword => $pass, + }, + ); + + if ($ret) { + return ( 1, $self->loc('New password notification sent') ); + } else { + return ( 0, $self->loc('Notification could not be sent') ); + } + +} + +=head3 GenerateRandomPassword MIN_LEN and MAX_LEN + +Returns a random password between MIN_LEN and MAX_LEN characters long. + +=cut + +sub GenerateRandomPassword { + my $self = shift; # just to drop it + return Text::Password::Pronounceable->generate(@_); +} + +sub SafeSetPassword { + my $self = shift; + my %args = ( + Current => undef, + New => undef, + Confirmation => undef, + @_, + ); + return (1) unless defined $args{'New'} && length $args{'New'}; + + my %cond = $self->CurrentUserRequireToSetPassword; + + unless ( $cond{'CanSet'} ) { + return (0, $self->loc('You can not set password.') .' '. $cond{'Reason'} ); + } + + my $error = ''; + if ( $cond{'RequireCurrent'} && !$self->CurrentUser->IsPassword($args{'Current'}) ) { + if ( defined $args{'Current'} && length $args{'Current'} ) { + $error = $self->loc("Please enter your current password correctly."); + } else { + $error = $self->loc("Please enter your current password."); + } + } elsif ( $args{'New'} ne $args{'Confirmation'} ) { + $error = $self->loc("Passwords do not match."); + } + + if ( $error ) { + $error .= ' '. $self->loc('Password has not been set.'); + return (0, $error); + } + + return $self->SetPassword( $args{'New'} ); +} + +=head3 SetPassword + +Takes a string. Checks the string's length and sets this user's password +to that string. + +=cut + +sub SetPassword { + my $self = shift; + my $password = shift; + + unless ( $self->CurrentUserCanModify('Password') ) { + return ( 0, $self->loc('Password: Permission Denied') ); + } + + if ( !$password ) { + return ( 0, $self->loc("No password set") ); + } else { + my ($val, $msg) = $self->ValidatePassword($password); + return ($val, $msg) if !$val; + + my $new = !$self->HasPassword; + $password = $self->_GeneratePassword($password); + + ( $val, $msg ) = $self->_Set(Field => 'Password', Value => $password); + if ($val) { + return ( 1, $self->loc("Password set") ) if $new; + return ( 1, $self->loc("Password changed") ); + } else { + return ( $val, $msg ); + } + } + +} + +sub _GeneratePassword_bcrypt { + my $self = shift; + my ($password, @rest) = @_; + + my $salt; + my $rounds; + if (@rest) { + # The first split is the number of rounds + $rounds = $rest[0]; + + # The salt is the first 22 characters, b64 encoded usign the + # special bcrypt base64. + $salt = Crypt::Eksblowfish::Bcrypt::de_base64( substr($rest[1], 0, 22) ); + } else { + $rounds = RT->Config->Get('BcryptCost'); + + # Generate a random 16-octet base64 salt + $salt = ""; + $salt .= pack("C", int rand(256)) for 1..16; + } + + my $hash = Crypt::Eksblowfish::Bcrypt::bcrypt_hash({ + key_nul => 1, + cost => $rounds, + salt => $salt, + }, Digest::SHA::sha512( Encode::encode( 'UTF-8', $password) ) ); + + return join("!", "", "bcrypt", sprintf("%02d", $rounds), + Crypt::Eksblowfish::Bcrypt::en_base64( $salt ). + Crypt::Eksblowfish::Bcrypt::en_base64( $hash ) + ); +} + +sub _GeneratePassword_sha512 { + my $self = shift; + my ($password, $salt) = @_; + + # Generate a 16-character base64 salt + unless ($salt) { + $salt = ""; + $salt .= ("a".."z", "A".."Z","0".."9", "+", "/")[rand 64] + for 1..16; + } + + my $sha = Digest::SHA->new(512); + $sha->add($salt); + $sha->add(Encode::encode( 'UTF-8', $password)); + return join("!", "", "sha512", $salt, $sha->b64digest); +} + +=head3 _GeneratePassword PASSWORD [, SALT] + +Returns a string to store in the database. This string takes the form: + + !method!salt!hash + +By default, the method is currently C. + +=cut + +sub _GeneratePassword { + my $self = shift; + return $self->_GeneratePassword_bcrypt(@_); +} + +=head3 HasPassword + +Returns true if the user has a valid password, otherwise returns false. + +=cut + +sub HasPassword { + my $self = shift; + my $pwd = $self->__Value('Password'); + return undef if !defined $pwd + || $pwd eq '' + || $pwd eq '*NO-PASSWORD*'; + return 1; +} + +=head3 IsPassword + +Returns true if the passed in value is this user's password. +Returns undef otherwise. + +=cut + +sub IsPassword { + my $self = shift; + my $value = shift; + + #TODO there isn't any apparent way to legitimately ACL this + + # RT does not allow null passwords + if ( ( !defined($value) ) or ( $value eq '' ) ) { + return (undef); + } + + if ( $self->PrincipalObj->Disabled ) { + $RT::Logger->info( + "Disabled user " . $self->Name . " tried to log in" ); + return (undef); + } + + unless ($self->HasPassword) { + return(undef); + } + + my $stored = $self->__Value('Password'); + if ($stored =~ /^!/) { + # If it's a new-style (>= RT 4.0) password, it starts with a '!' + my (undef, $method, @rest) = split /!/, $stored; + if ($method eq "bcrypt") { + return 0 unless $self->_GeneratePassword_bcrypt($value, @rest) eq $stored; + # Upgrade to a larger number of rounds if necessary + return 1 unless $rest[0] < RT->Config->Get('BcryptCost'); + } elsif ($method eq "sha512") { + return 0 unless $self->_GeneratePassword_sha512($value, @rest) eq $stored; + } else { + $RT::Logger->warn("Unknown hash method $method"); + return 0; + } + } elsif (length $stored == 40) { + # The truncated SHA256(salt,MD5(passwd)) form from 2010/12 is 40 characters long + my $hash = MIME::Base64::decode_base64($stored); + # Decoding yields 30 byes; first 4 are the salt, the rest are substr(SHA256,0,26) + my $salt = substr($hash, 0, 4, ""); + return 0 unless substr(Digest::SHA::sha256($salt . Digest::MD5::md5(Encode::encode( "UTF-8", $value))), 0, 26) eq $hash; + } elsif (length $stored == 32) { + # Hex nonsalted-md5 + return 0 unless Digest::MD5::md5_hex(Encode::encode( "UTF-8", $value)) eq $stored; + } elsif (length $stored == 22) { + # Base64 nonsalted-md5 + return 0 unless Digest::MD5::md5_base64(Encode::encode( "UTF-8", $value)) eq $stored; + } elsif (length $stored == 13) { + # crypt() output + return 0 unless crypt(Encode::encode( "UTF-8", $value), $stored) eq $stored; + } else { + $RT::Logger->warning("Unknown password form"); + return 0; + } + + # We got here by validating successfully, but with a legacy + # password form. Update to the most recent form. + my $obj = $self->isa("RT::CurrentUser") ? $self->UserObj : $self; + $obj->_Set(Field => 'Password', Value => $self->_GeneratePassword($value) ); + return 1; +} + +sub CurrentUserRequireToSetPassword { + my $self = shift; + + my %res = ( + CanSet => 1, + Reason => '', + RequireCurrent => 1, + ); + + if ( RT->Config->Get('WebRemoteUserAuth') + && !RT->Config->Get('WebFallbackToRTLogin') + ) { + $res{'CanSet'} = 0; + $res{'Reason'} = $self->loc("External authentication enabled."); + } elsif ( !$self->CurrentUser->HasPassword ) { + if ( $self->CurrentUser->id == ($self->id||0) ) { + # don't require current password if user has no + $res{'RequireCurrent'} = 0; + } else { + $res{'CanSet'} = 0; + $res{'Reason'} = $self->loc("Your password is not set."); + } + } + + return %res; +} + +=head3 AuthToken + +Returns an authentication string associated with the user. This +string can be used to generate passwordless URLs to integrate +RT with services and programms like callendar managers, rss +readers and other. + +=cut + +sub AuthToken { + my $self = shift; + my $secret = $self->_Value( AuthToken => @_ ); + return $secret if $secret; + + $secret = substr(Digest::MD5::md5_hex(time . {} . rand()),0,16); + + my $tmp = RT::User->new( RT->SystemUser ); + $tmp->Load( $self->id ); + my ($status, $msg) = $tmp->SetAuthToken( $secret ); + unless ( $status ) { + $RT::Logger->error( "Couldn't set auth token: $msg" ); + return undef; + } + return $secret; +} + +=head3 GenerateAuthToken + +Generate a random authentication string for the user. + +=cut + +sub GenerateAuthToken { + my $self = shift; + my $token = substr(Digest::MD5::md5_hex(time . {} . rand()),0,16); + return $self->SetAuthToken( $token ); +} + +=head3 GenerateAuthString + +Takes a string and returns back a hex hash string. Later you can use +this pair to make sure it's generated by this user using L + +=cut + +sub GenerateAuthString { + my $self = shift; + my $protect = shift; + + my $str = Encode::encode( "UTF-8", $self->AuthToken . $protect ); + + return substr(Digest::MD5::md5_hex($str),0,16); +} + +=head3 ValidateAuthString + +Takes auth string and protected string. Returns true is protected string +has been protected by user's L. See also L. + +=cut + +sub ValidateAuthString { + my $self = shift; + my $auth_string = shift; + my $protected = shift; + + my $str = Encode::encode( "UTF-8", $self->AuthToken . $protected ); + + return $auth_string eq substr(Digest::MD5::md5_hex($str),0,16); +} + +=head2 SetDisabled + +Toggles the user's disabled flag. +If this flag is +set, all password checks for this user will fail. All ACL checks for this +user will fail. The user will appear in no user listings. + +=cut + +sub SetDisabled { + my $self = shift; + my $val = shift; + unless ( $self->CurrentUser->HasRight(Right => 'AdminUsers', Object => $RT::System) ) { + return (0, $self->loc('Permission Denied')); + } + + $RT::Handle->BeginTransaction(); + my ($status, $msg) = $self->PrincipalObj->SetDisabled($val); + unless ($status) { + $RT::Handle->Rollback(); + $RT::Logger->warning(sprintf("Couldn't %s user %s", ($val == 1) ? "disable" : "enable", $self->PrincipalObj->Id)); + return ($status, $msg); + } + $self->_NewTransaction( Type => ($val == 1) ? "Disabled" : "Enabled" ); + + $RT::Handle->Commit(); + + if ( $val == 1 ) { + return (1, $self->loc("User disabled")); + } else { + return (1, $self->loc("User enabled")); + } + +} + +=head2 Disabled + +Returns true if user is disabled or false otherwise + +=cut + +sub Disabled { + my $self = shift; + return $self->PrincipalObj->Disabled(@_); +} + +=head2 PrincipalObj + +Returns the principal object for this user. returns an empty RT::Principal +if there's no principal object matching this user. +The response is cached. PrincipalObj should never ever change. + +=cut + +sub PrincipalObj { + my $self = shift; + + unless ( $self->id ) { + $RT::Logger->error("Couldn't get principal for an empty user"); + return undef; + } + + if ( !$self->{_principal_obj} ) { + + my $obj = RT::Principal->new( $self->CurrentUser ); + $obj->LoadById( $self->id ); + if (! $obj->id ) { + $RT::Logger->crit( 'No principal for user #' . $self->id ); + return undef; + } elsif ( $obj->PrincipalType ne 'User' ) { + $RT::Logger->crit( 'User #' . $self->id . ' has principal of ' . $obj->PrincipalType . ' type' ); + return undef; + } + $self->{_principal_obj} = $obj; + } + return $self->{_principal_obj}; +} + + +=head2 PrincipalId + +Returns this user's PrincipalId + +=cut + +sub PrincipalId { + my $self = shift; + return $self->Id; +} + +=head2 HasGroupRight + +Takes a paramhash which can contain +these items: + GroupObj => RT::Group or Group => integer + Right => 'Right' + + +Returns 1 if this user has the right specified in the paramhash for the Group +passed in. + +Returns undef if they don't. + +=cut + +sub HasGroupRight { + my $self = shift; + my %args = ( + GroupObj => undef, + Group => undef, + Right => undef, + @_ + ); + + + if ( defined $args{'Group'} ) { + $args{'GroupObj'} = RT::Group->new( $self->CurrentUser ); + $args{'GroupObj'}->Load( $args{'Group'} ); + } + + # Validate and load up the GroupId + unless ( ( defined $args{'GroupObj'} ) and ( $args{'GroupObj'}->Id ) ) { + return undef; + } + + # Figure out whether a user has the right we're asking about. + my $retval = $self->HasRight( + Object => $args{'GroupObj'}, + Right => $args{'Right'}, + ); + + return ($retval); +} + +=head2 OwnGroups + +Returns a group collection object containing the groups of which this +user is a member. + +=cut + +sub OwnGroups { + my $self = shift; + my $groups = RT::Groups->new($self->CurrentUser); + $groups->LimitToUserDefinedGroups; + $groups->WithMember( + PrincipalId => $self->Id, + Recursively => 1 + ); + return $groups; +} + +=head2 HasRight + +Shim around PrincipalObj->HasRight. See L. + +=cut -use strict; +sub HasRight { + my $self = shift; + return $self->PrincipalObj->HasRight(@_); +} +=head2 CurrentUserCanSee [FIELD] -=head1 NAME +Returns true if the current user can see the user, based on if it is +public, ourself, or we have AdminUsers -RT::User +=cut +sub CurrentUserCanSee { + my $self = shift; + my ($what, $txn) = @_; -=head1 SYNOPSIS + # If it's a public property, fine + return 1 if $self->_Accessible( $what, 'public' ); -=head1 DESCRIPTION + # Users can see all of their own properties + return 1 if defined($self->Id) and $self->CurrentUser->Id == $self->Id; -=head1 METHODS + # If the user has the admin users right, that's also enough + return 1 if $self->CurrentUserHasRight( 'AdminUsers' ); + + # Transactions of public properties are visible to users with ShowUserHistory + if ($what eq "Transaction" and $self->CurrentUserHasRight( 'ShowUserHistory' )) { + my $type = $txn->__Value('Type'); + my $field = $txn->__Value('Field'); + return 1 if $type eq "Set" and $self->CurrentUserCanSee($field, $txn); + + # RT::Transaction->CurrentUserCanSee deals with ensuring we meet + # the ACLs on CFs, so allow them here + return 1 if $type eq "CustomField"; + } + + return 0; +} + +=head2 CurrentUserCanModify RIGHT + +If the user has rights for this object, either because +he has 'AdminUsers' or (if he's trying to edit himself and the right isn't an +admin right) 'ModifySelf', return 1. otherwise, return undef. =cut -package RT::User; -use RT::Record; +sub CurrentUserCanModify { + my $self = shift; + my $field = shift; + + if ( $self->CurrentUser->HasRight(Right => 'AdminUsers', Object => $RT::System) ) { + return (1); + } + + #If the field is marked as an "administrators only" field, + # don't let the user touch it. + elsif ( $self->_Accessible( $field, 'admin' ) ) { + return (undef); + } + + #If the current user is trying to modify themselves + elsif ( ( $self->id == $self->CurrentUser->id ) + and ( $self->CurrentUser->HasRight(Right => 'ModifySelf', Object => $RT::System) ) ) + { + return (1); + } + + #If we don't have a good reason to grant them rights to modify + # by now, they lose + else { + return (undef); + } + +} + +=head2 CurrentUserHasRight + +Takes a single argument. returns 1 if $Self->CurrentUser +has the requested right. returns undef otherwise + +=cut + +sub CurrentUserHasRight { + my $self = shift; + my $right = shift; + + return ( $self->CurrentUser->HasRight(Right => $right, Object => $RT::System) ); +} + +sub _PrefName { + my $name = shift; + if (ref $name) { + $name = ref($name).'-'.$name->Id; + } + + return 'Pref-'. $name; +} + +=head2 Preferences NAME/OBJ DEFAULT + +Obtain user preferences associated with given object or name. +Returns DEFAULT if no preferences found. If DEFAULT is a hashref, +override the entries with user preferences. + +=cut + +sub Preferences { + my $self = shift; + my $name = _PrefName(shift); + my $default = shift; + + my ($attr) = $self->Attributes->Named( $name ); + my $content = $attr ? $attr->Content : undef; + unless ( ref $content eq 'HASH' ) { + return defined $content ? $content : $default; + } + + if (ref $default eq 'HASH') { + for (keys %$default) { + exists $content->{$_} or $content->{$_} = $default->{$_}; + } + } elsif (defined $default) { + $RT::Logger->error("Preferences $name for user #".$self->Id." is hash but default is not"); + } + return $content; +} + +=head2 SetPreferences NAME/OBJ VALUE + +Set user preferences associated with given object or name. + +=cut + +sub SetPreferences { + my $self = shift; + my $name = _PrefName( shift ); + my $value = shift; + + return (0, $self->loc("No permission to set preferences")) + unless $self->CurrentUserCanModify('Preferences'); + + my ($attr) = $self->Attributes->Named( $name ); + if ( $attr ) { + my ($ok, $msg) = $attr->SetContent( $value ); + return (1, "No updates made") + if $msg eq "That is already the current value"; + return ($ok, $msg); + } else { + return $self->AddAttribute( Name => $name, Content => $value ); + } +} + +=head2 DeletePreferences NAME/OBJ VALUE + +Delete user preferences associated with given object or name. + +=cut + +sub DeletePreferences { + my $self = shift; + my $name = _PrefName( shift ); + + return (0, $self->loc("No permission to set preferences")) + unless $self->CurrentUserCanModify('Preferences'); + + my ($attr) = $self->DeleteAttribute( $name ); + return (0, $self->loc("Preferences were not found")) + unless $attr; + + return 1; +} + +=head2 Stylesheet + +Returns a list of valid stylesheets take from preferences. + +=cut + +sub Stylesheet { + my $self = shift; + + my $style = RT->Config->Get('WebDefaultStylesheet', $self->CurrentUser); + + if (RT::Interface::Web->ComponentPathIsSafe($style)) { + for my $root (RT::Interface::Web->StaticRoots) { + if (-d "$root/css/$style") { + return $style + } + } + } + + # Fall back to the system stylesheet. + return RT->Config->Get('WebDefaultStylesheet'); +} + +=head2 WatchedQueues ROLE_LIST + +Returns a RT::Queues object containing every queue watched by the user. + +Takes a list of roles which is some subset of ('Cc', 'AdminCc'). Defaults to: + +$user->WatchedQueues('Cc', 'AdminCc'); + +=cut + +sub WatchedQueues { + + my $self = shift; + my @roles = @_ ? @_ : ('Cc', 'AdminCc'); + + $RT::Logger->debug('WatcheQueues got user ' . $self->Name); + + my $watched_queues = RT::Queues->new($self->CurrentUser); + + my $group_alias = $watched_queues->Join( + ALIAS1 => 'main', + FIELD1 => 'id', + TABLE2 => 'Groups', + FIELD2 => 'Instance', + ); + + $watched_queues->Limit( + ALIAS => $group_alias, + FIELD => 'Domain', + VALUE => 'RT::Queue-Role', + ENTRYAGGREGATOR => 'AND', + CASESENSITIVE => 0, + ); + if (grep { $_ eq 'Cc' } @roles) { + $watched_queues->Limit( + SUBCLAUSE => 'LimitToWatchers', + ALIAS => $group_alias, + FIELD => 'Name', + VALUE => 'Cc', + ENTRYAGGREGATOR => 'OR', + ); + } + if (grep { $_ eq 'AdminCc' } @roles) { + $watched_queues->Limit( + SUBCLAUSE => 'LimitToWatchers', + ALIAS => $group_alias, + FIELD => 'Name', + VALUE => 'AdminCc', + ENTRYAGGREGATOR => 'OR', + ); + } + + my $queues_alias = $watched_queues->Join( + ALIAS1 => $group_alias, + FIELD1 => 'id', + TABLE2 => 'CachedGroupMembers', + FIELD2 => 'GroupId', + ); + $watched_queues->Limit( + ALIAS => $queues_alias, + FIELD => 'MemberId', + VALUE => $self->PrincipalId, + ); + $watched_queues->Limit( + ALIAS => $queues_alias, + FIELD => 'Disabled', + VALUE => 0, + ); + + + $RT::Logger->debug("WatchedQueues got " . $watched_queues->Count . " queues"); + + return $watched_queues; + +} + +sub _Set { + my $self = shift; + + my %args = ( + Field => undef, + Value => undef, + TransactionType => 'Set', + RecordTransaction => 1, + @_ + ); + + # Nobody is allowed to futz with RT_System or Nobody + + if ( ($self->Id == RT->SystemUser->Id ) || + ($self->Id == RT->Nobody->Id)) { + return ( 0, $self->loc("Can not modify system users") ); + } + unless ( $self->CurrentUserCanModify( $args{'Field'} ) ) { + return ( 0, $self->loc("Permission Denied") ); + } + + my $Old = $self->SUPER::_Value("$args{'Field'}"); + + my ($ret, $msg) = $self->SUPER::_Set( Field => $args{'Field'}, + Value => $args{'Value'} ); + + #If we can't actually set the field to the value, don't record + # a transaction. instead, get out of here. + if ( $ret == 0 ) { return ( 0, $msg ); } + + if ( $args{'RecordTransaction'} == 1 ) { + if ($args{'Field'} eq "Password") { + $args{'Value'} = $Old = '********'; + } + my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction( + Type => $args{'TransactionType'}, + Field => $args{'Field'}, + NewValue => $args{'Value'}, + OldValue => $Old, + TimeTaken => $args{'TimeTaken'}, + ); + return ( $Trans, scalar $TransObj->BriefDescription ); + } else { + return ( $ret, $msg ); + } +} + +=head2 _Value + +Takes the name of a table column. +Returns its value as a string, if the user passes an ACL check + +=cut + +sub _Value { + + my $self = shift; + my $field = shift; + + # Defer to the abstraction above to know if the field can be read + return $self->SUPER::_Value($field) if $self->CurrentUserCanSee($field); + return undef; +} + +=head2 FriendlyName + +Return the friendly name + +=cut + +sub FriendlyName { + my $self = shift; + return $self->RealName if defined $self->RealName and length $self->RealName; + return $self->Name; +} + +=head2 Format + +Class or object method. + +Returns a string describing a user in the current user's preferred format. + +May be invoked in three ways: + + $UserObj->Format; + RT::User->Format( User => $UserObj ); # same as above + RT::User->Format( Address => $AddressObj, CurrentUser => $CurrentUserObj ); + +Possible arguments are: + +=over + +=item User + +An L object representing the user to format. Preferred to Address. + +=item Address + +An L object representing the user address to format. Address +will be used to lookup an L if possible. + +=item CurrentUser + +Required when Format is called as a class method with an Address argument. +Otherwise, this argument is ignored in preference to the CurrentUser of the +involved L object. + +=item Format + +Specifies the format to use, overriding any set from the config or current +user's preferences. + +=back + +=cut + +sub Format { + my $self = shift; + my %args = ( + User => undef, + Address => undef, + CurrentUser => undef, + Format => undef, + @_ + ); + + if (blessed($self) and $self->id) { + @args{"User", "CurrentUser"} = ($self, $self->CurrentUser); + } + elsif ($args{User} and $args{User}->id) { + $args{CurrentUser} = $args{User}->CurrentUser; + } + elsif ($args{Address} and $args{CurrentUser}) { + $args{User} = RT::User->new( $args{CurrentUser} ); + $args{User}->LoadByEmail( $args{Address}->address ); + if ($args{User}->id) { + delete $args{Address}; + } else { + delete $args{User}; + } + } + else { + RT->Logger->warning("Invalid arguments to RT::User->Format at @{[join '/', caller]}"); + return ""; + } + + $args{Format} ||= RT->Config->Get("UsernameFormat", $args{CurrentUser}); + $args{Format} =~ s/[^A-Za-z0-9_]+//g; + + my $method = "_FormatUser" . ucfirst lc $args{Format}; + my $formatter = $self->can($method); + + unless ($formatter) { + RT->Logger->error( + "Either system config or user #" . $args{CurrentUser}->id . + " picked UsernameFormat $args{Format}, but RT::User->$method doesn't exist" + ); + $formatter = $self->can("_FormatUserRole"); + } + return $formatter->( $self, map { $_ => $args{$_} } qw(User Address) ); +} + +sub _FormatUserRole { + my $self = shift; + my %args = @_; + + my $user = $args{User}; + return $self->_FormatUserVerbose(@_) + unless $user and $user->Privileged; + + my $name = $user->Name; + $name .= " (".$user->RealName.")" + if $user->RealName and lc $user->RealName ne lc $user->Name; + return $name; +} + +sub _FormatUserConcise { + my $self = shift; + my %args = @_; + return $args{User} ? $args{User}->FriendlyName : $args{Address}->address; +} + +sub _FormatUserVerbose { + my $self = shift; + my %args = @_; + my ($user, $address) = @args{"User", "Address"}; + + my $email = ''; + my $phrase = ''; + my $comment = ''; + + if ($user) { + $email = $user->EmailAddress || ''; + $phrase = $user->RealName if $user->RealName and lc $user->RealName ne lc $email; + $comment = $user->Name if lc $user->Name ne lc $email; + } else { + ($email, $phrase, $comment) = (map { $address->$_ } "address", "phrase", "comment"); + } + + return join " ", grep { $_ } ($phrase || $comment || ''), ($email ? "<$email>" : ""); +} + +=head2 PreferredKey + +Returns the preferred key of the user. If none is set, then this will query +GPG and set the preferred key to the maximally trusted key found (and then +return it). Returns C if no preferred key can be found. + +=cut + +sub PreferredKey +{ + my $self = shift; + return undef unless RT->Config->Get('GnuPG')->{'Enable'}; + + if ( ($self->CurrentUser->Id != $self->Id ) && + !$self->CurrentUser->HasRight(Right =>'AdminUsers', Object => $RT::System) ) { + return undef; + } + + + + my $prefkey = $self->FirstAttribute('PreferredKey'); + return $prefkey->Content if $prefkey; + + # we don't have a preferred key for this user, so now we must query GPG + my %res = RT::Crypt->GetKeysForEncryption($self->EmailAddress); + return undef unless defined $res{'info'}; + my @keys = @{ $res{'info'} }; + return undef if @keys == 0; + + if (@keys == 1) { + $prefkey = $keys[0]->{'Fingerprint'}; + } else { + # prefer the maximally trusted key + @keys = sort { $b->{'TrustLevel'} <=> $a->{'TrustLevel'} } @keys; + $prefkey = $keys[0]->{'Fingerprint'}; + } + + $self->SetAttribute(Name => 'PreferredKey', Content => $prefkey); + return $prefkey; +} + +sub PrivateKey { + my $self = shift; + + + #If the user wants to see their own values, let them. + #If the user is an admin, let them. + #Otherwwise, don't let them. + # + if ( ($self->CurrentUser->Id != $self->Id ) && + !$self->CurrentUser->HasRight(Right =>'AdminUsers', Object => $RT::System) ) { + return undef; + } + + my $key = $self->FirstAttribute('PrivateKey') or return undef; + return $key->Content; +} + +sub SetPrivateKey { + my $self = shift; + my $key = shift; + + # Users should not be able to change their own PrivateKey values + unless ( $self->CurrentUser->HasRight(Right => 'AdminUsers', Object => $RT::System) ) { + return (0, $self->loc("Permission Denied")); + } + + unless ( $key ) { + my ($status, $msg) = $self->DeleteAttribute('PrivateKey'); + unless ( $status ) { + $RT::Logger->error( "Couldn't delete attribute: $msg" ); + return ($status, $self->loc("Couldn't unset private key")); + } + return ($status, $self->loc("Unset private key")); + } + + # check that it's really private key + { + my %tmp = RT::Crypt->GetKeysForSigning( Signer => $key, Protocol => 'GnuPG' ); + return (0, $self->loc("No such key or it's not suitable for signing")) + if $tmp{'exit_code'} || !$tmp{'info'}; + } + + my ($status, $msg) = $self->SetAttribute( + Name => 'PrivateKey', + Content => $key, + ); + return ($status, $self->loc("Couldn't set private key")) + unless $status; + return ($status, $self->loc("Set private key")); +} + +sub SetLang { + my $self = shift; + my ($lang) = @_; + + unless ($self->CurrentUserCanModify('Lang')) { + return (0, $self->loc("Permission Denied")); + } + + # Local hack to cause the result message to be in the _new_ language + # if we're updating ourselves + $self->CurrentUser->{LangHandle} = RT::I18N->get_handle( $lang ) + if $self->CurrentUser->id == $self->id; + return $self->_Set( Field => 'Lang', Value => $lang ); +} + +sub BasicColumns { + ( + [ Name => 'Username' ], + [ EmailAddress => 'Email' ], + [ RealName => 'Name' ], + [ Organization => 'Organization' ], + ); +} +=head2 Bookmarks -use vars qw( @ISA ); -@ISA= qw( RT::Record ); +Returns an unordered list of IDs representing the user's bookmarked tickets. -sub _Init { - my $self = shift; +=cut + +sub Bookmarks { + my $self = shift; + my $bookmarks = $self->FirstAttribute('Bookmarks'); + return if !$bookmarks; + + $bookmarks = $bookmarks->Content; + return if !$bookmarks; + + return keys %$bookmarks; +} + +=head2 HasBookmark TICKET + +Returns whether the provided ticket is bookmarked by the user. - $self->Table('Users'); - $self->SUPER::_Init(@_); +=cut + +sub HasBookmark { + my $self = shift; + my $ticket = shift; + my $id = $ticket->id; + + # maintain bookmarks across merges + my @ids = ($id, $ticket->Merged); + + my $bookmarks = $self->FirstAttribute('Bookmarks'); + $bookmarks = $bookmarks ? $bookmarks->Content : {}; + + my @bookmarked = grep { $bookmarks->{ $_ } } @ids; + return @bookmarked ? 1 : 0; } +=head2 ToggleBookmark TICKET + +Toggles whether the provided ticket is bookmarked by the user. + +=cut + +sub ToggleBookmark { + my $self = shift; + my $ticket = shift; + my $id = $ticket->id; + + # maintain bookmarks across merges + my @ids = ($id, $ticket->Merged); + + my $bookmarks = $self->FirstAttribute('Bookmarks'); + $bookmarks = $bookmarks ? $bookmarks->Content : {}; + + my $is_bookmarked; + if ( grep { $bookmarks->{ $_ } } @ids ) { + delete $bookmarks->{ $_ } foreach @ids; + $is_bookmarked = 0; + } else { + $bookmarks->{ $id } = 1; + $is_bookmarked = 1; + } + $self->SetAttribute( + Name => 'Bookmarks', + Content => $bookmarks, + ); + return $is_bookmarked; +} -=item Create PARAMHASH +=head2 Create PARAMHASH Create takes a hash of values and creates a row in the database: varchar(200) 'Name'. - varchar(40) 'Password'. - blob 'Comments'. - blob 'Signature'. + varbinary(256) 'Password'. + varchar(16) 'AuthToken'. + text 'Comments'. + text 'Signature'. varchar(120) 'EmailAddress'. - blob 'FreeformContactInfo'. + text 'FreeformContactInfo'. varchar(200) 'Organization'. varchar(120) 'RealName'. varchar(16) 'NickName'. @@ -100,77 +1972,7 @@ Create takes a hash of values and creates a row in the database: -sub Create { - my $self = shift; - my %args = ( - Name => '', - Password => '', - Comments => '', - Signature => '', - EmailAddress => '', - FreeformContactInfo => '', - Organization => '', - RealName => '', - NickName => '', - Lang => '', - EmailEncoding => '', - WebEncoding => '', - ExternalContactInfoId => '', - ContactInfoSystem => '', - ExternalAuthId => '', - AuthSystem => '', - Gecos => '', - HomePhone => '', - WorkPhone => '', - MobilePhone => '', - PagerPhone => '', - Address1 => '', - Address2 => '', - City => '', - State => '', - Zip => '', - Country => '', - Timezone => '', - PGPKey => '', - - @_); - $self->SUPER::Create( - Name => $args{'Name'}, - Password => $args{'Password'}, - Comments => $args{'Comments'}, - Signature => $args{'Signature'}, - EmailAddress => $args{'EmailAddress'}, - FreeformContactInfo => $args{'FreeformContactInfo'}, - Organization => $args{'Organization'}, - RealName => $args{'RealName'}, - NickName => $args{'NickName'}, - Lang => $args{'Lang'}, - EmailEncoding => $args{'EmailEncoding'}, - WebEncoding => $args{'WebEncoding'}, - ExternalContactInfoId => $args{'ExternalContactInfoId'}, - ContactInfoSystem => $args{'ContactInfoSystem'}, - ExternalAuthId => $args{'ExternalAuthId'}, - AuthSystem => $args{'AuthSystem'}, - Gecos => $args{'Gecos'}, - HomePhone => $args{'HomePhone'}, - WorkPhone => $args{'WorkPhone'}, - MobilePhone => $args{'MobilePhone'}, - PagerPhone => $args{'PagerPhone'}, - Address1 => $args{'Address1'}, - Address2 => $args{'Address2'}, - City => $args{'City'}, - State => $args{'State'}, - Zip => $args{'Zip'}, - Country => $args{'Country'}, - Timezone => $args{'Timezone'}, - PGPKey => $args{'PGPKey'}, -); - -} - - - -=item id +=head2 id Returns the current value of id. (In the database, id is stored as int(11).) @@ -179,14 +1981,14 @@ Returns the current value of id. =cut -=item Name +=head2 Name Returns the current value of Name. (In the database, Name is stored as varchar(200).) -=item SetName VALUE +=head2 SetName VALUE Set Name to VALUE. @@ -197,68 +1999,86 @@ Returns (1, 'Status message') on success and (0, 'Error Message') on failure. =cut -=item Password +=head2 Password Returns the current value of Password. -(In the database, Password is stored as varchar(40).) +(In the database, Password is stored as varchar(256).) -=item SetPassword VALUE +=head2 SetPassword VALUE Set Password to VALUE. Returns (1, 'Status message') on success and (0, 'Error Message') on failure. -(In the database, Password will be stored as a varchar(40).) +(In the database, Password will be stored as a varchar(256).) + + +=cut + + +=head2 AuthToken + +Returns the current value of AuthToken. +(In the database, AuthToken is stored as varchar(16).) + + + +=head2 SetAuthToken VALUE + + +Set AuthToken to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, AuthToken will be stored as a varchar(16).) =cut -=item Comments +=head2 Comments Returns the current value of Comments. -(In the database, Comments is stored as blob.) +(In the database, Comments is stored as text.) -=item SetComments VALUE +=head2 SetComments VALUE Set Comments to VALUE. Returns (1, 'Status message') on success and (0, 'Error Message') on failure. -(In the database, Comments will be stored as a blob.) +(In the database, Comments will be stored as a text.) =cut -=item Signature +=head2 Signature Returns the current value of Signature. -(In the database, Signature is stored as blob.) +(In the database, Signature is stored as text.) -=item SetSignature VALUE +=head2 SetSignature VALUE Set Signature to VALUE. Returns (1, 'Status message') on success and (0, 'Error Message') on failure. -(In the database, Signature will be stored as a blob.) +(In the database, Signature will be stored as a text.) =cut -=item EmailAddress +=head2 EmailAddress Returns the current value of EmailAddress. (In the database, EmailAddress is stored as varchar(120).) -=item SetEmailAddress VALUE +=head2 SetEmailAddress VALUE Set EmailAddress to VALUE. @@ -269,32 +2089,32 @@ Returns (1, 'Status message') on success and (0, 'Error Message') on failure. =cut -=item FreeformContactInfo +=head2 FreeformContactInfo Returns the current value of FreeformContactInfo. -(In the database, FreeformContactInfo is stored as blob.) +(In the database, FreeformContactInfo is stored as text.) -=item SetFreeformContactInfo VALUE +=head2 SetFreeformContactInfo VALUE Set FreeformContactInfo to VALUE. Returns (1, 'Status message') on success and (0, 'Error Message') on failure. -(In the database, FreeformContactInfo will be stored as a blob.) +(In the database, FreeformContactInfo will be stored as a text.) =cut -=item Organization +=head2 Organization Returns the current value of Organization. (In the database, Organization is stored as varchar(200).) -=item SetOrganization VALUE +=head2 SetOrganization VALUE Set Organization to VALUE. @@ -305,14 +2125,14 @@ Returns (1, 'Status message') on success and (0, 'Error Message') on failure. =cut -=item RealName +=head2 RealName Returns the current value of RealName. (In the database, RealName is stored as varchar(120).) -=item SetRealName VALUE +=head2 SetRealName VALUE Set RealName to VALUE. @@ -323,14 +2143,14 @@ Returns (1, 'Status message') on success and (0, 'Error Message') on failure. =cut -=item NickName +=head2 NickName Returns the current value of NickName. (In the database, NickName is stored as varchar(16).) -=item SetNickName VALUE +=head2 SetNickName VALUE Set NickName to VALUE. @@ -341,14 +2161,14 @@ Returns (1, 'Status message') on success and (0, 'Error Message') on failure. =cut -=item Lang +=head2 Lang Returns the current value of Lang. (In the database, Lang is stored as varchar(16).) -=item SetLang VALUE +=head2 SetLang VALUE Set Lang to VALUE. @@ -359,14 +2179,14 @@ Returns (1, 'Status message') on success and (0, 'Error Message') on failure. =cut -=item EmailEncoding +=head2 EmailEncoding Returns the current value of EmailEncoding. (In the database, EmailEncoding is stored as varchar(16).) -=item SetEmailEncoding VALUE +=head2 SetEmailEncoding VALUE Set EmailEncoding to VALUE. @@ -377,14 +2197,14 @@ Returns (1, 'Status message') on success and (0, 'Error Message') on failure. =cut -=item WebEncoding +=head2 WebEncoding Returns the current value of WebEncoding. (In the database, WebEncoding is stored as varchar(16).) -=item SetWebEncoding VALUE +=head2 SetWebEncoding VALUE Set WebEncoding to VALUE. @@ -395,14 +2215,14 @@ Returns (1, 'Status message') on success and (0, 'Error Message') on failure. =cut -=item ExternalContactInfoId +=head2 ExternalContactInfoId Returns the current value of ExternalContactInfoId. (In the database, ExternalContactInfoId is stored as varchar(100).) -=item SetExternalContactInfoId VALUE +=head2 SetExternalContactInfoId VALUE Set ExternalContactInfoId to VALUE. @@ -413,14 +2233,14 @@ Returns (1, 'Status message') on success and (0, 'Error Message') on failure. =cut -=item ContactInfoSystem +=head2 ContactInfoSystem Returns the current value of ContactInfoSystem. (In the database, ContactInfoSystem is stored as varchar(30).) -=item SetContactInfoSystem VALUE +=head2 SetContactInfoSystem VALUE Set ContactInfoSystem to VALUE. @@ -431,14 +2251,14 @@ Returns (1, 'Status message') on success and (0, 'Error Message') on failure. =cut -=item ExternalAuthId +=head2 ExternalAuthId Returns the current value of ExternalAuthId. (In the database, ExternalAuthId is stored as varchar(100).) -=item SetExternalAuthId VALUE +=head2 SetExternalAuthId VALUE Set ExternalAuthId to VALUE. @@ -449,14 +2269,14 @@ Returns (1, 'Status message') on success and (0, 'Error Message') on failure. =cut -=item AuthSystem +=head2 AuthSystem Returns the current value of AuthSystem. (In the database, AuthSystem is stored as varchar(30).) -=item SetAuthSystem VALUE +=head2 SetAuthSystem VALUE Set AuthSystem to VALUE. @@ -467,14 +2287,14 @@ Returns (1, 'Status message') on success and (0, 'Error Message') on failure. =cut -=item Gecos +=head2 Gecos Returns the current value of Gecos. (In the database, Gecos is stored as varchar(16).) -=item SetGecos VALUE +=head2 SetGecos VALUE Set Gecos to VALUE. @@ -485,14 +2305,14 @@ Returns (1, 'Status message') on success and (0, 'Error Message') on failure. =cut -=item HomePhone +=head2 HomePhone Returns the current value of HomePhone. (In the database, HomePhone is stored as varchar(30).) -=item SetHomePhone VALUE +=head2 SetHomePhone VALUE Set HomePhone to VALUE. @@ -503,14 +2323,14 @@ Returns (1, 'Status message') on success and (0, 'Error Message') on failure. =cut -=item WorkPhone +=head2 WorkPhone Returns the current value of WorkPhone. (In the database, WorkPhone is stored as varchar(30).) -=item SetWorkPhone VALUE +=head2 SetWorkPhone VALUE Set WorkPhone to VALUE. @@ -521,14 +2341,14 @@ Returns (1, 'Status message') on success and (0, 'Error Message') on failure. =cut -=item MobilePhone +=head2 MobilePhone Returns the current value of MobilePhone. (In the database, MobilePhone is stored as varchar(30).) -=item SetMobilePhone VALUE +=head2 SetMobilePhone VALUE Set MobilePhone to VALUE. @@ -539,14 +2359,14 @@ Returns (1, 'Status message') on success and (0, 'Error Message') on failure. =cut -=item PagerPhone +=head2 PagerPhone Returns the current value of PagerPhone. (In the database, PagerPhone is stored as varchar(30).) -=item SetPagerPhone VALUE +=head2 SetPagerPhone VALUE Set PagerPhone to VALUE. @@ -557,14 +2377,14 @@ Returns (1, 'Status message') on success and (0, 'Error Message') on failure. =cut -=item Address1 +=head2 Address1 Returns the current value of Address1. (In the database, Address1 is stored as varchar(200).) -=item SetAddress1 VALUE +=head2 SetAddress1 VALUE Set Address1 to VALUE. @@ -575,14 +2395,14 @@ Returns (1, 'Status message') on success and (0, 'Error Message') on failure. =cut -=item Address2 +=head2 Address2 Returns the current value of Address2. (In the database, Address2 is stored as varchar(200).) -=item SetAddress2 VALUE +=head2 SetAddress2 VALUE Set Address2 to VALUE. @@ -593,14 +2413,14 @@ Returns (1, 'Status message') on success and (0, 'Error Message') on failure. =cut -=item City +=head2 City Returns the current value of City. (In the database, City is stored as varchar(100).) -=item SetCity VALUE +=head2 SetCity VALUE Set City to VALUE. @@ -611,14 +2431,14 @@ Returns (1, 'Status message') on success and (0, 'Error Message') on failure. =cut -=item State +=head2 State Returns the current value of State. (In the database, State is stored as varchar(100).) -=item SetState VALUE +=head2 SetState VALUE Set State to VALUE. @@ -629,14 +2449,14 @@ Returns (1, 'Status message') on success and (0, 'Error Message') on failure. =cut -=item Zip +=head2 Zip Returns the current value of Zip. (In the database, Zip is stored as varchar(16).) -=item SetZip VALUE +=head2 SetZip VALUE Set Zip to VALUE. @@ -647,14 +2467,14 @@ Returns (1, 'Status message') on success and (0, 'Error Message') on failure. =cut -=item Country +=head2 Country Returns the current value of Country. (In the database, Country is stored as varchar(50).) -=item SetCountry VALUE +=head2 SetCountry VALUE Set Country to VALUE. @@ -665,14 +2485,14 @@ Returns (1, 'Status message') on success and (0, 'Error Message') on failure. =cut -=item Timezone +=head2 Timezone Returns the current value of Timezone. (In the database, Timezone is stored as varchar(50).) -=item SetTimezone VALUE +=head2 SetTimezone VALUE Set Timezone to VALUE. @@ -683,14 +2503,14 @@ Returns (1, 'Status message') on success and (0, 'Error Message') on failure. =cut -=item PGPKey +=head2 PGPKey Returns the current value of PGPKey. (In the database, PGPKey is stored as text.) -=item SetPGPKey VALUE +=head2 SetPGPKey VALUE Set PGPKey to VALUE. @@ -701,7 +2521,25 @@ Returns (1, 'Status message') on success and (0, 'Error Message') on failure. =cut -=item Creator +=head2 SMIMECertificate + +Returns the current value of SMIMECertificate. +(In the database, SMIMECertificate is stored as text.) + + + +=head2 SetSMIMECertificate VALUE + + +Set SMIMECertificate to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, SMIMECertificate will be stored as a text.) + + +=cut + + +=head2 Creator Returns the current value of Creator. (In the database, Creator is stored as int(11).) @@ -710,7 +2548,7 @@ Returns the current value of Creator. =cut -=item Created +=head2 Created Returns the current value of Created. (In the database, Created is stored as datetime.) @@ -719,7 +2557,7 @@ Returns the current value of Created. =cut -=item LastUpdatedBy +=head2 LastUpdatedBy Returns the current value of LastUpdatedBy. (In the database, LastUpdatedBy is stored as int(11).) @@ -728,7 +2566,7 @@ Returns the current value of LastUpdatedBy. =cut -=item LastUpdated +=head2 LastUpdated Returns the current value of LastUpdated. (In the database, LastUpdated is stored as datetime.) @@ -737,118 +2575,490 @@ Returns the current value of LastUpdated. =cut +# much false laziness w/Ticket.pm. now with RT 4! +our %LINKDIRMAP = ( + MemberOf => { Base => 'MemberOf', + Target => 'HasMember', }, + RefersTo => { Base => 'RefersTo', + Target => 'ReferredToBy', }, + DependsOn => { Base => 'DependsOn', + Target => 'DependedOnBy', }, + MergedInto => { Base => 'MergedInto', + Target => 'MergedInto', }, + +); + +sub LINKDIRMAP { return \%LINKDIRMAP } + + +=head2 DeleteLink + +Delete a link. takes a paramhash of Base, Target and Type. +Either Base or Target must be null. The null value will +be replaced with this ticket\'s id + +=cut + +sub DeleteLink { + my $self = shift; + my %args = ( + Base => undef, + Target => undef, + Type => undef, + @_ + ); + + unless ( $args{'Target'} || $args{'Base'} ) { + $RT::Logger->error("Base or Target must be specified\n"); + return ( 0, $self->loc('Either base or target must be specified') ); + } + + #check acls + my $right = 0; + $right++ if $self->CurrentUserHasRight('AdminUsers'); + if ( !$right && $RT::StrictLinkACL ) { + return ( 0, $self->loc("Permission Denied") ); + } + +# # If the other URI is an RT::Ticket, we want to make sure the user +# # can modify it too... +# my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} ); +# return (0, $msg) unless $status; +# if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) { +# $right++; +# } +# if ( ( !$RT::StrictLinkACL && $right == 0 ) || +# ( $RT::StrictLinkACL && $right < 2 ) ) +# { +# return ( 0, $self->loc("Permission Denied") ); +# } + + my ($val, $Msg) = $self->SUPER::_DeleteLink(%args); + + if ( !$val ) { + $RT::Logger->debug("Couldn't find that link\n"); + return ( 0, $Msg ); + } + + my ($direction, $remote_link); + + if ( $args{'Base'} ) { + $remote_link = $args{'Base'}; + $direction = 'Target'; + } + elsif ( $args{'Target'} ) { + $remote_link = $args{'Target'}; + $direction='Base'; + } + + if ( $args{'Silent'} ) { + return ( $val, $Msg ); + } + else { + my $remote_uri = RT::URI->new( $self->CurrentUser ); + $remote_uri->FromURI( $remote_link ); + + my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction( + Type => 'DeleteLink', + Field => $LINKDIRMAP{$args{'Type'}}->{$direction}, + OldValue => $remote_uri->URI || $remote_link, + TimeTaken => 0 + ); + + if ( $remote_uri->IsLocal ) { + + my $OtherObj = $remote_uri->Object; + my ( $val, $Msg ) = $OtherObj->_NewTransaction(Type => 'DeleteLink', + Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base} + : $LINKDIRMAP{$args{'Type'}}->{Target}, + OldValue => $self->URI, + ActivateScrips => ! $RT::LinkTransactionsRun1Scrip, + TimeTaken => 0 ); + } + + return ( $Trans, $Msg ); + } +} + +sub AddLink { + my $self = shift; + my %args = ( Target => '', + Base => '', + Type => '', + Silent => undef, + @_ ); + + unless ( $args{'Target'} || $args{'Base'} ) { + $RT::Logger->error("Base or Target must be specified\n"); + return ( 0, $self->loc('Either base or target must be specified') ); + } + + my $right = 0; + $right++ if $self->CurrentUserHasRight('AdminUsers'); + if ( !$right && $RT::StrictLinkACL ) { + return ( 0, $self->loc("Permission Denied") ); + } + +# # If the other URI is an RT::Ticket, we want to make sure the user +# # can modify it too... +# my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} ); +# return (0, $msg) unless $status; +# if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) { +# $right++; +# } +# if ( ( !$RT::StrictLinkACL && $right == 0 ) || +# ( $RT::StrictLinkACL && $right < 2 ) ) +# { +# return ( 0, $self->loc("Permission Denied") ); +# } + + return $self->_AddLink(%args); +} + +=head2 _AddLink + +Private non-acled variant of AddLink so that links can be added during create. + +=cut + +sub _AddLink { + my $self = shift; + my %args = ( Target => '', + Base => '', + Type => '', + Silent => undef, + @_ ); + + my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args); + return ($val, $msg) if !$val || $exist; + + my ($direction, $remote_link); + if ( $args{'Target'} ) { + $remote_link = $args{'Target'}; + $direction = 'Base'; + } elsif ( $args{'Base'} ) { + $remote_link = $args{'Base'}; + $direction = 'Target'; + } + + # Don't write the transaction if we're doing this on create + if ( $args{'Silent'} ) { + return ( $val, $msg ); + } + else { + my $remote_uri = RT::URI->new( $self->CurrentUser ); + $remote_uri->FromURI( $remote_link ); + + #Write the transaction + my ( $Trans, $Msg, $TransObj ) = + $self->_NewTransaction(Type => 'AddLink', + Field => $LINKDIRMAP{$args{'Type'}}->{$direction}, + NewValue => $remote_uri->URI || $remote_link, + TimeTaken => 0 ); + + if ( $remote_uri->IsLocal ) { + + my $OtherObj = $remote_uri->Object; + my ( $val, $Msg ) = $OtherObj->_NewTransaction(Type => 'AddLink', + Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base} + : $LINKDIRMAP{$args{'Type'}}->{Target}, + NewValue => $self->URI, + ActivateScrips => ! $RT::LinkTransactionsRun1Scrip, + TimeTaken => 0 ); + } + return ( $val, $Msg ); + } + +} + -sub _ClassAccessible { +sub _CoreAccessible { { id => - {read => 1, type => 'int(11)', default => ''}, + {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''}, Name => - {read => 1, write => 1, type => 'varchar(200)', default => ''}, + {read => 1, write => 1, sql_type => 12, length => 200, is_blob => 0, is_numeric => 0, type => 'varchar(200)', default => ''}, Password => - {read => 1, write => 1, type => 'varchar(40)', default => ''}, + {read => 1, write => 1, sql_type => 12, length => 256, is_blob => 0, is_numeric => 0, type => 'varchar(256)', default => ''}, + AuthToken => + {read => 1, write => 1, sql_type => 12, length => 16, is_blob => 0, is_numeric => 0, type => 'varchar(16)', default => ''}, Comments => - {read => 1, write => 1, type => 'blob', default => ''}, + {read => 1, write => 1, sql_type => -4, length => 0, is_blob => 1, is_numeric => 0, type => 'text', default => ''}, Signature => - {read => 1, write => 1, type => 'blob', default => ''}, + {read => 1, write => 1, sql_type => -4, length => 0, is_blob => 1, is_numeric => 0, type => 'text', default => ''}, EmailAddress => - {read => 1, write => 1, type => 'varchar(120)', default => ''}, + {read => 1, write => 1, sql_type => 12, length => 120, is_blob => 0, is_numeric => 0, type => 'varchar(120)', default => ''}, FreeformContactInfo => - {read => 1, write => 1, type => 'blob', default => ''}, + {read => 1, write => 1, sql_type => -4, length => 0, is_blob => 1, is_numeric => 0, type => 'text', default => ''}, Organization => - {read => 1, write => 1, type => 'varchar(200)', default => ''}, + {read => 1, write => 1, sql_type => 12, length => 200, is_blob => 0, is_numeric => 0, type => 'varchar(200)', default => ''}, RealName => - {read => 1, write => 1, type => 'varchar(120)', default => ''}, + {read => 1, write => 1, sql_type => 12, length => 120, is_blob => 0, is_numeric => 0, type => 'varchar(120)', default => ''}, NickName => - {read => 1, write => 1, type => 'varchar(16)', default => ''}, + {read => 1, write => 1, sql_type => 12, length => 16, is_blob => 0, is_numeric => 0, type => 'varchar(16)', default => ''}, Lang => - {read => 1, write => 1, type => 'varchar(16)', default => ''}, + {read => 1, write => 1, sql_type => 12, length => 16, is_blob => 0, is_numeric => 0, type => 'varchar(16)', default => ''}, EmailEncoding => - {read => 1, write => 1, type => 'varchar(16)', default => ''}, + {read => 1, write => 1, sql_type => 12, length => 16, is_blob => 0, is_numeric => 0, type => 'varchar(16)', default => ''}, WebEncoding => - {read => 1, write => 1, type => 'varchar(16)', default => ''}, + {read => 1, write => 1, sql_type => 12, length => 16, is_blob => 0, is_numeric => 0, type => 'varchar(16)', default => ''}, ExternalContactInfoId => - {read => 1, write => 1, type => 'varchar(100)', default => ''}, + {read => 1, write => 1, sql_type => 12, length => 100, is_blob => 0, is_numeric => 0, type => 'varchar(100)', default => ''}, ContactInfoSystem => - {read => 1, write => 1, type => 'varchar(30)', default => ''}, + {read => 1, write => 1, sql_type => 12, length => 30, is_blob => 0, is_numeric => 0, type => 'varchar(30)', default => ''}, ExternalAuthId => - {read => 1, write => 1, type => 'varchar(100)', default => ''}, + {read => 1, write => 1, sql_type => 12, length => 100, is_blob => 0, is_numeric => 0, type => 'varchar(100)', default => ''}, AuthSystem => - {read => 1, write => 1, type => 'varchar(30)', default => ''}, + {read => 1, write => 1, sql_type => 12, length => 30, is_blob => 0, is_numeric => 0, type => 'varchar(30)', default => ''}, Gecos => - {read => 1, write => 1, type => 'varchar(16)', default => ''}, + {read => 1, write => 1, sql_type => 12, length => 16, is_blob => 0, is_numeric => 0, type => 'varchar(16)', default => ''}, HomePhone => - {read => 1, write => 1, type => 'varchar(30)', default => ''}, + {read => 1, write => 1, sql_type => 12, length => 30, is_blob => 0, is_numeric => 0, type => 'varchar(30)', default => ''}, WorkPhone => - {read => 1, write => 1, type => 'varchar(30)', default => ''}, + {read => 1, write => 1, sql_type => 12, length => 30, is_blob => 0, is_numeric => 0, type => 'varchar(30)', default => ''}, MobilePhone => - {read => 1, write => 1, type => 'varchar(30)', default => ''}, + {read => 1, write => 1, sql_type => 12, length => 30, is_blob => 0, is_numeric => 0, type => 'varchar(30)', default => ''}, PagerPhone => - {read => 1, write => 1, type => 'varchar(30)', default => ''}, + {read => 1, write => 1, sql_type => 12, length => 30, is_blob => 0, is_numeric => 0, type => 'varchar(30)', default => ''}, Address1 => - {read => 1, write => 1, type => 'varchar(200)', default => ''}, + {read => 1, write => 1, sql_type => 12, length => 200, is_blob => 0, is_numeric => 0, type => 'varchar(200)', default => ''}, Address2 => - {read => 1, write => 1, type => 'varchar(200)', default => ''}, + {read => 1, write => 1, sql_type => 12, length => 200, is_blob => 0, is_numeric => 0, type => 'varchar(200)', default => ''}, City => - {read => 1, write => 1, type => 'varchar(100)', default => ''}, + {read => 1, write => 1, sql_type => 12, length => 100, is_blob => 0, is_numeric => 0, type => 'varchar(100)', default => ''}, State => - {read => 1, write => 1, type => 'varchar(100)', default => ''}, + {read => 1, write => 1, sql_type => 12, length => 100, is_blob => 0, is_numeric => 0, type => 'varchar(100)', default => ''}, Zip => - {read => 1, write => 1, type => 'varchar(16)', default => ''}, + {read => 1, write => 1, sql_type => 12, length => 16, is_blob => 0, is_numeric => 0, type => 'varchar(16)', default => ''}, Country => - {read => 1, write => 1, type => 'varchar(50)', default => ''}, + {read => 1, write => 1, sql_type => 12, length => 50, is_blob => 0, is_numeric => 0, type => 'varchar(50)', default => ''}, Timezone => - {read => 1, write => 1, type => 'varchar(50)', default => ''}, + {read => 1, write => 1, sql_type => 12, length => 50, is_blob => 0, is_numeric => 0, type => 'varchar(50)', default => ''}, PGPKey => - {read => 1, write => 1, type => 'text', default => ''}, + {read => 1, write => 1, sql_type => -4, length => 0, is_blob => 1, is_numeric => 0, type => 'text', default => ''}, + SMIMECertificate => + {read => 1, write => 1, sql_type => -4, length => 0, is_blob => 1, is_numeric => 0, type => 'text', default => ''}, Creator => - {read => 1, auto => 1, type => 'int(11)', default => '0'}, + {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'}, Created => - {read => 1, auto => 1, type => 'datetime', default => ''}, + {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''}, LastUpdatedBy => - {read => 1, auto => 1, type => 'int(11)', default => '0'}, + {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'}, LastUpdated => - {read => 1, auto => 1, type => 'datetime', default => ''}, + {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''}, } }; +sub UID { + my $self = shift; + return undef unless defined $self->Name; + return "@{[ref $self]}-@{[$self->Name]}"; +} - eval "require RT::User_Overlay"; - if ($@ && $@ !~ qr{^Can't locate RT/User_Overlay.pm}) { - die $@; - }; - - eval "require RT::User_Vendor"; - if ($@ && $@ !~ qr{^Can't locate RT/User_Vendor.pm}) { - die $@; - }; - - eval "require RT::User_Local"; - if ($@ && $@ !~ qr{^Can't locate RT/User_Local.pm}) { - die $@; - }; - - - - -=head1 SEE ALSO - -This class allows "overlay" methods to be placed -into the following files _Overlay is for a System overlay by the original author, -_Vendor is for 3rd-party vendor add-ons, while _Local is for site-local customizations. +sub FindDependencies { + my $self = shift; + my ($walker, $deps) = @_; + + $self->SUPER::FindDependencies($walker, $deps); + + # ACL equivalence group + my $objs = RT::Groups->new( $self->CurrentUser ); + $objs->Limit( FIELD => 'Domain', VALUE => 'ACLEquivalence', CASESENSITIVE => 0 ); + $objs->Limit( FIELD => 'Instance', VALUE => $self->Id ); + $deps->Add( in => $objs ); + + # Memberships in SystemInternal groups + $objs = RT::GroupMembers->new( $self->CurrentUser ); + $objs->Limit( FIELD => 'MemberId', VALUE => $self->Id ); + my $principals = $objs->Join( + ALIAS1 => 'main', + FIELD1 => 'GroupId', + TABLE2 => 'Principals', + FIELD2 => 'id', + ); + my $groups = $objs->Join( + ALIAS1 => $principals, + FIELD1 => 'ObjectId', + TABLE2 => 'Groups', + FIELD2 => 'Id', + ); + $objs->Limit( + ALIAS => $groups, + FIELD => 'Domain', + VALUE => 'SystemInternal', + CASESENSITIVE => 0 + ); + $deps->Add( in => $objs ); + + # XXX: This ignores the myriad of "in" references from the Creator + # and LastUpdatedBy columns. +} -These overlay files can contain new subs or subs to replace existing subs in this module. +sub __DependsOn { + my $self = shift; + my %args = ( + Shredder => undef, + Dependencies => undef, + @_, + ); + my $deps = $args{'Dependencies'}; + my $list = []; + +# Principal + $deps->_PushDependency( + BaseObject => $self, + Flags => RT::Shredder::Constants::DEPENDS_ON | RT::Shredder::Constants::WIPE_AFTER, + TargetObject => $self->PrincipalObj, + Shredder => $args{'Shredder'} + ); + +# ACL equivalence group +# don't use LoadACLEquivalenceGroup cause it may not exists any more + my $objs = RT::Groups->new( $self->CurrentUser ); + $objs->Limit( FIELD => 'Domain', VALUE => 'ACLEquivalence', CASESENSITIVE => 0 ); + $objs->Limit( FIELD => 'Instance', VALUE => $self->Id ); + push( @$list, $objs ); + +# Cleanup user's membership + $objs = RT::GroupMembers->new( $self->CurrentUser ); + $objs->Limit( FIELD => 'MemberId', VALUE => $self->Id ); + push( @$list, $objs ); + + $deps->_PushDependencies( + BaseObject => $self, + Flags => RT::Shredder::Constants::DEPENDS_ON, + TargetObjects => $list, + Shredder => $args{'Shredder'} + ); + +# TODO: Almost all objects has Creator, LastUpdatedBy and etc. fields +# which are references on users(Principal actualy) + my @OBJECTS = qw( + ACL + Articles + Attachments + Attributes + CachedGroupMembers + Classes + CustomFieldValues + CustomFields + GroupMembers + Groups + Links + ObjectClasses + ObjectCustomFieldValues + ObjectCustomFields + ObjectScrips + Principals + Queues + ScripActions + ScripConditions + Scrips + Templates + Tickets + Transactions + Users + ); + my @var_objs; + foreach( @OBJECTS ) { + my $class = "RT::$_"; + foreach my $method ( qw(Creator LastUpdatedBy) ) { + my $objs = $class->new( $self->CurrentUser ); + next unless $objs->RecordClass->_Accessible( $method => 'read' ); + $objs->Limit( FIELD => $method, VALUE => $self->id ); + push @var_objs, $objs; + } + } + $deps->_PushDependencies( + BaseObject => $self, + Flags => RT::Shredder::Constants::DEPENDS_ON | RT::Shredder::Constants::VARIABLE, + TargetObjects => \@var_objs, + Shredder => $args{'Shredder'} + ); + + return $self->SUPER::__DependsOn( %args ); +} -If you'll be working with perl 5.6.0 or greater, each of these files should begin with the line +sub BeforeWipeout { + my $self = shift; + if( $self->Name =~ /^(RT_System|Nobody)$/ ) { + RT::Shredder::Exception::Info->throw('SystemObject'); + } + return $self->SUPER::BeforeWipeout( @_ ); +} - no warnings qw(redefine); +sub Serialize { + my $self = shift; + return ( + Disabled => $self->PrincipalObj->Disabled, + Principal => $self->PrincipalObj->UID, + PrincipalId => $self->PrincipalObj->Id, + $self->SUPER::Serialize(@_), + ); +} -so that perl does not kick and scream when you redefine a subroutine or variable in your overlay. +sub PreInflate { + my $class = shift; + my ($importer, $uid, $data) = @_; + + my $principal_uid = delete $data->{Principal}; + my $principal_id = delete $data->{PrincipalId}; + my $disabled = delete $data->{Disabled}; + + my $obj = RT::User->new( RT->SystemUser ); + $obj->LoadByCols( Name => $data->{Name} ); + $obj->LoadByEmail( $data->{EmailAddress} ) unless $obj->Id; + if ($obj->Id) { + # User already exists -- merge + + # XXX: We might be merging a privileged user into an unpriv one, + # in which case we should probably promote the unpriv user to + # being privileged. Of course, we don't know if the user being + # imported is privileged yet, as its group memberships show up + # later in the stream... + $importer->MergeValues($obj, $data); + $importer->SkipTransactions( $uid ); + + # Mark both the principal and the user object as resolved + $importer->Resolve( + $principal_uid, + ref($obj->PrincipalObj), + $obj->PrincipalObj->Id + ); + $importer->Resolve( $uid => ref($obj) => $obj->Id ); + return; + } + + # Create a principal first, so we know what ID to use + my $principal = RT::Principal->new( RT->SystemUser ); + my ($id) = $principal->Create( + PrincipalType => 'User', + Disabled => $disabled, + ObjectId => 0, + ); + + # Now we have a principal id, set the id for the user record + $data->{id} = $id; + + $importer->Resolve( $principal_uid => ref($principal), $id ); + + $importer->Postpone( + for => $uid, + uid => $principal_uid, + column => "ObjectId", + ); + + return $class->SUPER::PreInflate( $importer, $uid, $data ); +} -RT::User_Overlay, RT::User_Vendor, RT::User_Local +sub PostInflate { + my $self = shift; + RT->InitSystemObjects if $self->Name eq "RT_System"; +} -=cut +RT::Base->_ImportOverlays(); 1;