]> git.uio.no Git - usit-rt.git/blob - lib/RT/User.pm
c07cfc87583e9f2f167a515bb828ef3ed2858e5e
[usit-rt.git] / lib / RT / User.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
6 #                                          <sales@bestpractical.com>
7 #
8 # (Except where explicitly superseded by other copyright notices)
9 #
10 #
11 # LICENSE:
12 #
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
16 # from www.gnu.org.
17 #
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
21 # General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
28 #
29 #
30 # CONTRIBUTION SUBMISSION POLICY:
31 #
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
37 #
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
46 #
47 # END BPS TAGGED BLOCK }}}
48
49 =head1 NAME
50
51   RT::User - RT User object
52
53 =head1 SYNOPSIS
54
55   use RT::User;
56
57 =head1 DESCRIPTION
58
59 =head1 METHODS
60
61 =cut
62
63
64 package RT::User;
65
66 use strict;
67 use warnings;
68
69 use Scalar::Util qw(blessed);
70
71 use base 'RT::Record';
72
73 sub Table {'Users'}
74
75
76
77
78
79
80 use Digest::SHA;
81 use Digest::MD5;
82 use Crypt::Eksblowfish::Bcrypt qw();
83 use RT::Principals;
84 use RT::ACE;
85 use RT::Interface::Email;
86 use Encode;
87 use Text::Password::Pronounceable;
88
89 sub _OverlayAccessible {
90     {
91
92           Name                  => { public => 1,  admin => 1 },    # loc_left_pair
93           Password              => { read   => 0 },
94           EmailAddress          => { public => 1 },                 # loc_left_pair
95           Organization          => { public => 1,  admin => 1 },    # loc_left_pair
96           RealName              => { public => 1 },                 # loc_left_pair
97           NickName              => { public => 1 },                 # loc_left_pair
98           Lang                  => { public => 1 },                 # loc_left_pair
99           EmailEncoding         => { public => 1 },
100           WebEncoding           => { public => 1 },
101           ExternalContactInfoId => { public => 1,  admin => 1 },
102           ContactInfoSystem     => { public => 1,  admin => 1 },
103           ExternalAuthId        => { public => 1,  admin => 1 },
104           AuthSystem            => { public => 1,  admin => 1 },
105           Gecos                 => { public => 1,  admin => 1 },    # loc_left_pair
106           PGPKey                => { public => 1,  admin => 1 },    # loc_left_pair
107           SMIMECertificate      => { public => 1,  admin => 1 },    # loc_left_pair
108           PrivateKey            => {               admin => 1 },
109           City                  => { public => 1 },                 # loc_left_pair
110           Country               => { public => 1 },                 # loc_left_pair
111           Timezone              => { public => 1 },                 # loc_left_pair
112     }
113 }
114
115
116
117 =head2 Create { PARAMHASH }
118
119
120
121 =cut
122
123
124 sub Create {
125     my $self = shift;
126     my %args = (
127         Privileged => 0,
128         Disabled => 0,
129         EmailAddress => '',
130         _RecordTransaction => 1,
131         @_    # get the real argumentlist
132     );
133
134     # remove the value so it does not cripple SUPER::Create
135     my $record_transaction = delete $args{'_RecordTransaction'};
136
137     #Check the ACL
138     unless ( $self->CurrentUser->HasRight(Right => 'AdminUsers', Object => $RT::System) ) {
139         return ( 0, $self->loc('Permission Denied') );
140     }
141
142
143     unless ($self->CanonicalizeUserInfo(\%args)) {
144         return ( 0, $self->loc("Could not set user info") );
145     }
146
147     $args{'EmailAddress'} = $self->CanonicalizeEmailAddress($args{'EmailAddress'});
148
149     # if the user doesn't have a name defined, set it to the email address
150     $args{'Name'} = $args{'EmailAddress'} unless ($args{'Name'});
151
152
153
154     my $privileged = delete $args{'Privileged'};
155
156
157     if ($args{'CryptedPassword'} ) {
158         $args{'Password'} = $args{'CryptedPassword'};
159         delete $args{'CryptedPassword'};
160     } elsif ( !$args{'Password'} ) {
161         $args{'Password'} = '*NO-PASSWORD*';
162     } else {
163         my ($ok, $msg) = $self->ValidatePassword($args{'Password'});
164         return ($ok, $msg) if !$ok;
165
166         $args{'Password'} = $self->_GeneratePassword($args{'Password'});
167     }
168
169     #TODO Specify some sensible defaults.
170
171     unless ( $args{'Name'} ) {
172         return ( 0, $self->loc("Must specify 'Name' attribute") );
173     }
174
175     my ( $val, $msg ) = $self->ValidateName( $args{'Name'} );
176     return ( 0, $msg ) unless $val;
177     ( $val, $msg ) = $self->ValidateEmailAddress( $args{'EmailAddress'} );
178     return ( 0, $msg ) unless ($val);
179
180     $RT::Handle->BeginTransaction();
181     # Groups deal with principal ids, rather than user ids.
182     # When creating this user, set up a principal Id for it.
183     my $principal = RT::Principal->new($self->CurrentUser);
184     my $principal_id = $principal->Create(PrincipalType => 'User',
185                                 Disabled => $args{'Disabled'},
186                                 ObjectId => '0');
187     # If we couldn't create a principal Id, get the fuck out.
188     unless ($principal_id) {
189         $RT::Handle->Rollback();
190         $RT::Logger->crit("Couldn't create a Principal on new user create.");
191         $RT::Logger->crit("Strange things are afoot at the circle K");
192         return ( 0, $self->loc('Could not create user') );
193     }
194
195     $principal->__Set(Field => 'ObjectId', Value => $principal_id);
196     delete $args{'Disabled'};
197
198     $self->SUPER::Create(id => $principal_id , %args);
199     my $id = $self->Id;
200
201     #If the create failed.
202     unless ($id) {
203         $RT::Handle->Rollback();
204         $RT::Logger->error("Could not create a new user - " .join('-', %args));
205
206         return ( 0, $self->loc('Could not create user') );
207     }
208
209     my $aclstash = RT::Group->new($self->CurrentUser);
210     my $stash_id = $aclstash->_CreateACLEquivalenceGroup($principal);
211
212     unless ($stash_id) {
213         $RT::Handle->Rollback();
214         $RT::Logger->crit("Couldn't stash the user in groupmembers");
215         return ( 0, $self->loc('Could not create user') );
216     }
217
218
219     my $everyone = RT::Group->new($self->CurrentUser);
220     $everyone->LoadSystemInternalGroup('Everyone');
221     unless ($everyone->id) {
222         $RT::Logger->crit("Could not load Everyone group on user creation.");
223         $RT::Handle->Rollback();
224         return ( 0, $self->loc('Could not create user') );
225     }
226
227
228     my ($everyone_id, $everyone_msg) = $everyone->_AddMember( InsideTransaction => 1, PrincipalId => $self->PrincipalId);
229     unless ($everyone_id) {
230         $RT::Logger->crit("Could not add user to Everyone group on user creation.");
231         $RT::Logger->crit($everyone_msg);
232         $RT::Handle->Rollback();
233         return ( 0, $self->loc('Could not create user') );
234     }
235
236
237     my $access_class = RT::Group->new($self->CurrentUser);
238     if ($privileged)  {
239         $access_class->LoadSystemInternalGroup('Privileged');
240     } else {
241         $access_class->LoadSystemInternalGroup('Unprivileged');
242     }
243
244     unless ($access_class->id) {
245         $RT::Logger->crit("Could not load Privileged or Unprivileged group on user creation");
246         $RT::Handle->Rollback();
247         return ( 0, $self->loc('Could not create user') );
248     }
249
250
251     my ($ac_id, $ac_msg) = $access_class->_AddMember( InsideTransaction => 1, PrincipalId => $self->PrincipalId);
252
253     unless ($ac_id) {
254         $RT::Logger->crit("Could not add user to Privileged or Unprivileged group on user creation. Aborted");
255         $RT::Logger->crit($ac_msg);
256         $RT::Handle->Rollback();
257         return ( 0, $self->loc('Could not create user') );
258     }
259
260
261     if ( $record_transaction ) {
262         $self->_NewTransaction( Type => "Create" );
263     }
264
265     $RT::Handle->Commit;
266
267     return ( $id, $self->loc('User created') );
268 }
269
270 =head2 ValidateName STRING
271
272 Returns either (0, "failure reason") or 1 depending on whether the given
273 name is valid.
274
275 =cut
276
277 sub ValidateName {
278     my $self = shift;
279     my $name = shift;
280
281     return ( 0, $self->loc('empty name') ) unless defined $name && length $name;
282
283     my $TempUser = RT::User->new( RT->SystemUser );
284     $TempUser->Load($name);
285
286     if ( $TempUser->id && ( !$self->id || $TempUser->id != $self->id ) ) {
287         return ( 0, $self->loc('Name in use') );
288     }
289     else {
290         return 1;
291     }
292 }
293
294 =head2 ValidatePassword STRING
295
296 Returns either (0, "failure reason") or 1 depending on whether the given
297 password is valid.
298
299 =cut
300
301 sub ValidatePassword {
302     my $self = shift;
303     my $password = shift;
304
305     if ( length($password) < RT->Config->Get('MinimumPasswordLength') ) {
306         return ( 0, $self->loc("Password needs to be at least [_1] characters long", RT->Config->Get('MinimumPasswordLength')) );
307     }
308
309     return 1;
310 }
311
312 =head2 SetPrivileged BOOL
313
314 If passed a true value, makes this user a member of the "Privileged"  PseudoGroup.
315 Otherwise, makes this user a member of the "Unprivileged" pseudogroup.
316
317 Returns a standard RT tuple of (val, msg);
318
319
320 =cut
321
322 sub SetPrivileged {
323     my $self = shift;
324     my $val = shift;
325
326     #Check the ACL
327     unless ( $self->CurrentUser->HasRight(Right => 'AdminUsers', Object => $RT::System) ) {
328         return ( 0, $self->loc('Permission Denied') );
329     }
330
331     $self->_SetPrivileged($val);
332 }
333
334 sub _SetPrivileged {
335     my $self = shift;
336     my $val = shift;
337     my $priv = RT::Group->new($self->CurrentUser);
338     $priv->LoadSystemInternalGroup('Privileged');
339     unless ($priv->Id) {
340         $RT::Logger->crit("Could not find Privileged pseudogroup");
341         return(0,$self->loc("Failed to find 'Privileged' users pseudogroup."));
342     }
343
344     my $unpriv = RT::Group->new($self->CurrentUser);
345     $unpriv->LoadSystemInternalGroup('Unprivileged');
346     unless ($unpriv->Id) {
347         $RT::Logger->crit("Could not find unprivileged pseudogroup");
348         return(0,$self->loc("Failed to find 'Unprivileged' users pseudogroup"));
349     }
350
351     my $principal = $self->PrincipalId;
352     if ($val) {
353         if ($priv->HasMember($principal)) {
354             #$RT::Logger->debug("That user is already privileged");
355             return (0,$self->loc("That user is already privileged"));
356         }
357         if ($unpriv->HasMember($principal)) {
358             $unpriv->_DeleteMember($principal);
359         } else {
360         # if we had layered transactions, life would be good
361         # sadly, we have to just go ahead, even if something
362         # bogus happened
363             $RT::Logger->crit("User ".$self->Id." is neither privileged nor ".
364                 "unprivileged. something is drastically wrong.");
365         }
366         my ($status, $msg) = $priv->_AddMember( InsideTransaction => 1, PrincipalId => $principal);
367         if ($status) {
368             return (1, $self->loc("That user is now privileged"));
369         } else {
370             return (0, $msg);
371         }
372     } else {
373         if ($unpriv->HasMember($principal)) {
374             #$RT::Logger->debug("That user is already unprivileged");
375             return (0,$self->loc("That user is already unprivileged"));
376         }
377         if ($priv->HasMember($principal)) {
378             $priv->_DeleteMember( $principal );
379         } else {
380         # if we had layered transactions, life would be good
381         # sadly, we have to just go ahead, even if something
382         # bogus happened
383             $RT::Logger->crit("User ".$self->Id." is neither privileged nor ".
384                 "unprivileged. something is drastically wrong.");
385         }
386         my ($status, $msg) = $unpriv->_AddMember( InsideTransaction => 1, PrincipalId => $principal);
387         if ($status) {
388             return (1, $self->loc("That user is now unprivileged"));
389         } else {
390             return (0, $msg);
391         }
392     }
393 }
394
395 =head2 Privileged
396
397 Returns true if this user is privileged. Returns undef otherwise.
398
399 =cut
400
401 sub Privileged {
402     my $self = shift;
403     if ( RT->PrivilegedUsers->HasMember( $self->id ) ) {
404         return(1);
405     } else {
406         return(undef);
407     }
408 }
409
410 #create a user without validating _any_ data.
411
412 #To be used only on database init.
413 # We can't localize here because it's before we _have_ a loc framework
414
415 sub _BootstrapCreate {
416     my $self = shift;
417     my %args = (@_);
418
419     $args{'Password'} = '*NO-PASSWORD*';
420
421
422     $RT::Handle->BeginTransaction();
423
424     # Groups deal with principal ids, rather than user ids.
425     # When creating this user, set up a principal Id for it.
426     my $principal = RT::Principal->new($self->CurrentUser);
427     my $principal_id = $principal->Create(PrincipalType => 'User', ObjectId => '0');
428     $principal->__Set(Field => 'ObjectId', Value => $principal_id);
429
430     # If we couldn't create a principal Id, get the fuck out.
431     unless ($principal_id) {
432         $RT::Handle->Rollback();
433         $RT::Logger->crit("Couldn't create a Principal on new user create. Strange things are afoot at the circle K");
434         return ( 0, 'Could not create user' );
435     }
436     $self->SUPER::Create(id => $principal_id, %args);
437     my $id = $self->Id;
438     #If the create failed.
439       unless ($id) {
440       $RT::Handle->Rollback();
441       return ( 0, 'Could not create user' ) ; #never loc this
442     }
443
444     my $aclstash = RT::Group->new($self->CurrentUser);
445     my $stash_id  = $aclstash->_CreateACLEquivalenceGroup($principal);
446
447     unless ($stash_id) {
448         $RT::Handle->Rollback();
449         $RT::Logger->crit("Couldn't stash the user in groupmembers");
450         return ( 0, $self->loc('Could not create user') );
451     }
452
453     $RT::Handle->Commit();
454
455     return ( $id, 'User created' );
456 }
457
458 sub Delete {
459     my $self = shift;
460
461     return ( 0, $self->loc('Deleting this object would violate referential integrity') );
462
463 }
464
465 =head2 Load
466
467 Load a user object from the database. Takes a single argument.
468 If the argument is numerical, load by the column 'id'. If a user
469 object or its subclass passed then loads the same user by id.
470 Otherwise, load by the "Name" column which is the user's textual
471 username.
472
473 =cut
474
475 sub Load {
476     my $self = shift;
477     my $identifier = shift || return undef;
478
479     if ( $identifier !~ /\D/ ) {
480         return $self->SUPER::LoadById( $identifier );
481     } elsif ( UNIVERSAL::isa( $identifier, 'RT::User' ) ) {
482         return $self->SUPER::LoadById( $identifier->Id );
483     } else {
484         return $self->LoadByCol( "Name", $identifier );
485     }
486 }
487
488 =head2 LoadByEmail
489
490 Tries to load this user object from the database by the user's email address.
491
492 =cut
493
494 sub LoadByEmail {
495     my $self    = shift;
496     my $address = shift;
497
498     # Never load an empty address as an email address.
499     unless ($address) {
500         return (undef);
501     }
502
503     $address = $self->CanonicalizeEmailAddress($address);
504
505     #$RT::Logger->debug("Trying to load an email address: $address");
506     return $self->LoadByCol( "EmailAddress", $address );
507 }
508
509 =head2 LoadOrCreateByEmail ADDRESS
510
511 Attempts to find a user who has the provided email address. If that fails, creates an unprivileged user with
512 the provided email address and loads them. Address can be provided either as L<Email::Address> object
513 or string which is parsed using the module.
514
515 Returns a tuple of the user's id and a status message.
516 0 will be returned in place of the user's id in case of failure.
517
518 =cut
519
520 sub LoadOrCreateByEmail {
521     my $self = shift;
522     my $email = shift;
523
524     my ($message, $name);
525     if ( UNIVERSAL::isa( $email => 'Email::Address' ) ) {
526         ($email, $name) = ($email->address, $email->phrase);
527     } else {
528         ($email, $name) = RT::Interface::Email::ParseAddressFromHeader( $email );
529     }
530
531     $self->LoadByEmail( $email );
532     $self->Load( $email ) unless $self->Id;
533     $message = $self->loc('User loaded');
534
535     unless( $self->Id ) {
536         my $val;
537         ($val, $message) = $self->Create(
538             Name         => $email,
539             EmailAddress => $email,
540             RealName     => $name,
541             Privileged   => 0,
542             Comments     => 'Autocreated when added as a watcher',
543         );
544         unless ( $val ) {
545             # Deal with the race condition of two account creations at once
546             $self->LoadByEmail( $email );
547             unless ( $self->Id ) {
548                 sleep 5;
549                 $self->LoadByEmail( $email );
550             }
551             if ( $self->Id ) {
552                 $RT::Logger->error("Recovered from creation failure due to race condition");
553                 $message = $self->loc("User loaded");
554             } else {
555                 $RT::Logger->crit("Failed to create user ". $email .": " .$message);
556             }
557         }
558     }
559     return wantarray ? (0, $message) : 0 unless $self->id;
560     return wantarray ? ($self->Id, $message) : $self->Id;
561 }
562
563 =head2 ValidateEmailAddress ADDRESS
564
565 Returns true if the email address entered is not in use by another user or is
566 undef or ''. Returns false if it's in use.
567
568 =cut
569
570 sub ValidateEmailAddress {
571     my $self  = shift;
572     my $Value = shift;
573
574     # if the email address is null, it's always valid
575     return (1) if ( !$Value || $Value eq "" );
576
577     if ( RT->Config->Get('ValidateUserEmailAddresses') ) {
578         # We only allow one valid email address
579         my @addresses = Email::Address->parse($Value);
580         return ( 0, $self->loc('Invalid syntax for email address') ) unless ( ( scalar (@addresses) == 1 ) && ( $addresses[0]->address ) );
581     }
582
583
584     my $TempUser = RT::User->new(RT->SystemUser);
585     $TempUser->LoadByEmail($Value);
586
587     if ( $TempUser->id && ( !$self->id || $TempUser->id != $self->id ) )
588     {    # if we found a user with that address
589             # it's invalid to set this user's address to it
590         return ( 0, $self->loc('Email address in use') );
591     } else {    #it's a valid email address
592         return (1);
593     }
594 }
595
596 =head2 SetName
597
598 Check to make sure someone else isn't using this name already
599
600 =cut
601
602 sub SetName {
603     my $self  = shift;
604     my $Value = shift;
605
606     my ( $val, $message ) = $self->ValidateName($Value);
607     if ($val) {
608         return $self->_Set( Field => 'Name', Value => $Value );
609     }
610     else {
611         return ( 0, $message );
612     }
613 }
614
615 =head2 SetEmailAddress
616
617 Check to make sure someone else isn't using this email address already
618 so that a better email address can be returned
619
620 =cut
621
622 sub SetEmailAddress {
623     my $self  = shift;
624     my $Value = shift;
625     $Value = '' unless defined $Value;
626
627     my ($val, $message) = $self->ValidateEmailAddress( $Value );
628     if ( $val ) {
629         return $self->_Set( Field => 'EmailAddress', Value => $Value );
630     } else {
631         return ( 0, $message )
632     }
633
634 }
635
636 =head2 EmailFrequency
637
638 Takes optional Ticket argument in paramhash. Returns 'no email',
639 'squelched', 'daily', 'weekly' or empty string depending on
640 user preferences.
641
642 =over 4
643
644 =item 'no email' - user has no email, so can not recieve notifications.
645
646 =item 'squelched' - returned only when Ticket argument is provided and
647 notifications to the user has been supressed for this ticket.
648
649 =item 'daily' - retruned when user recieve daily messages digest instead
650 of immediate delivery.
651
652 =item 'weekly' - previous, but weekly.
653
654 =item empty string returned otherwise.
655
656 =back
657
658 =cut
659
660 sub EmailFrequency {
661     my $self = shift;
662     my %args = (
663         Ticket => undef,
664         @_
665     );
666     return '' unless $self->id && $self->id != RT->Nobody->id
667         && $self->id != RT->SystemUser->id;
668     return 'no email address' unless my $email = $self->EmailAddress;
669     return 'email disabled for ticket' if $args{'Ticket'} &&
670         grep lc $email eq lc $_->Content, $args{'Ticket'}->SquelchMailTo;
671     my $frequency = RT->Config->Get( 'EmailFrequency', $self ) || '';
672     return 'daily' if $frequency =~ /daily/i;
673     return 'weekly' if $frequency =~ /weekly/i;
674     return '';
675 }
676
677 =head2 CanonicalizeEmailAddress ADDRESS
678
679 CanonicalizeEmailAddress converts email addresses into canonical form.
680 it takes one email address in and returns the proper canonical
681 form. You can dump whatever your proper local config is in here.  Note
682 that it may be called as a static method; in this case the first argument
683 is class name not an object.
684
685 =cut
686
687 sub CanonicalizeEmailAddress {
688     my $self = shift;
689     my $email = shift;
690     # Example: the following rule would treat all email
691     # coming from a subdomain as coming from second level domain
692     # foo.com
693     if ( my $match   = RT->Config->Get('CanonicalizeEmailAddressMatch') and
694          my $replace = RT->Config->Get('CanonicalizeEmailAddressReplace') )
695     {
696         $email =~ s/$match/$replace/gi;
697     }
698     return ($email);
699 }
700
701 =head2 CanonicalizeUserInfo HASH of ARGS
702
703 CanonicalizeUserInfo can convert all User->Create options.
704 it takes a hashref of all the params sent to User->Create and
705 returns that same hash, by default nothing is done.
706
707 This function is intended to allow users to have their info looked up via
708 an outside source and modified upon creation.
709
710 =cut
711
712 sub CanonicalizeUserInfo {
713     my $self = shift;
714     my $args = shift;
715     my $success = 1;
716
717     return ($success);
718 }
719
720
721 =head2 Password and authentication related functions
722
723 =head3 SetRandomPassword
724
725 Takes no arguments. Returns a status code and a new password or an error message.
726 If the status is 1, the second value returned is the new password.
727 If the status is anything else, the new value returned is the error code.
728
729 =cut
730
731 sub SetRandomPassword {
732     my $self = shift;
733
734     unless ( $self->CurrentUserCanModify('Password') ) {
735         return ( 0, $self->loc("Permission Denied") );
736     }
737
738
739     my $min = ( RT->Config->Get('MinimumPasswordLength') > 6 ?  RT->Config->Get('MinimumPasswordLength') : 6);
740     my $max = ( RT->Config->Get('MinimumPasswordLength') > 8 ?  RT->Config->Get('MinimumPasswordLength') : 8);
741
742     my $pass = $self->GenerateRandomPassword( $min, $max) ;
743
744     # If we have "notify user on
745
746     my ( $val, $msg ) = $self->SetPassword($pass);
747
748     #If we got an error return the error.
749     return ( 0, $msg ) unless ($val);
750
751     #Otherwise, we changed the password, lets return it.
752     return ( 1, $pass );
753
754 }
755
756 =head3 ResetPassword
757
758 Returns status, [ERROR or new password].  Resets this user's password to
759 a randomly generated pronouncable password and emails them, using a
760 global template called "PasswordChange".
761
762 This function is currently unused in the UI, but available for local scripts.
763
764 =cut
765
766 sub ResetPassword {
767     my $self = shift;
768
769     unless ( $self->CurrentUserCanModify('Password') ) {
770         return ( 0, $self->loc("Permission Denied") );
771     }
772     my ( $status, $pass ) = $self->SetRandomPassword();
773
774     unless ($status) {
775         return ( 0, "$pass" );
776     }
777
778     my $ret = RT::Interface::Email::SendEmailUsingTemplate(
779         To        => $self->EmailAddress,
780         Template  => 'PasswordChange',
781         Arguments => {
782             NewPassword => $pass,
783         },
784     );
785
786     if ($ret) {
787         return ( 1, $self->loc('New password notification sent') );
788     } else {
789         return ( 0, $self->loc('Notification could not be sent') );
790     }
791
792 }
793
794 =head3 GenerateRandomPassword MIN_LEN and MAX_LEN
795
796 Returns a random password between MIN_LEN and MAX_LEN characters long.
797
798 =cut
799
800 sub GenerateRandomPassword {
801     my $self = shift;   # just to drop it
802     return Text::Password::Pronounceable->generate(@_);
803 }
804
805 sub SafeSetPassword {
806     my $self = shift;
807     my %args = (
808         Current      => undef,
809         New          => undef,
810         Confirmation => undef,
811         @_,
812     );
813     return (1) unless defined $args{'New'} && length $args{'New'};
814
815     my %cond = $self->CurrentUserRequireToSetPassword;
816
817     unless ( $cond{'CanSet'} ) {
818         return (0, $self->loc('You can not set password.') .' '. $cond{'Reason'} );
819     }
820
821     my $error = '';
822     if ( $cond{'RequireCurrent'} && !$self->CurrentUser->IsPassword($args{'Current'}) ) {
823         if ( defined $args{'Current'} && length $args{'Current'} ) {
824             $error = $self->loc("Please enter your current password correctly.");
825         } else {
826             $error = $self->loc("Please enter your current password.");
827         }
828     } elsif ( $args{'New'} ne $args{'Confirmation'} ) {
829         $error = $self->loc("Passwords do not match.");
830     }
831
832     if ( $error ) {
833         $error .= ' '. $self->loc('Password has not been set.');
834         return (0, $error);
835     }
836
837     return $self->SetPassword( $args{'New'} );
838 }
839
840 =head3 SetPassword
841
842 Takes a string. Checks the string's length and sets this user's password
843 to that string.
844
845 =cut
846
847 sub SetPassword {
848     my $self     = shift;
849     my $password = shift;
850
851     unless ( $self->CurrentUserCanModify('Password') ) {
852         return ( 0, $self->loc('Password: Permission Denied') );
853     }
854
855     if ( !$password ) {
856         return ( 0, $self->loc("No password set") );
857     } else {
858         my ($val, $msg) = $self->ValidatePassword($password);
859         return ($val, $msg) if !$val;
860
861         my $new = !$self->HasPassword;
862         $password = $self->_GeneratePassword($password);
863
864         ( $val, $msg ) = $self->_Set(Field => 'Password', Value => $password);
865         if ($val) {
866             return ( 1, $self->loc("Password set") ) if $new;
867             return ( 1, $self->loc("Password changed") );
868         } else {
869             return ( $val, $msg );
870         }
871     }
872
873 }
874
875 sub _GeneratePassword_bcrypt {
876     my $self = shift;
877     my ($password, @rest) = @_;
878
879     my $salt;
880     my $rounds;
881     if (@rest) {
882         # The first split is the number of rounds
883         $rounds = $rest[0];
884
885         # The salt is the first 22 characters, b64 encoded usign the
886         # special bcrypt base64.
887         $salt = Crypt::Eksblowfish::Bcrypt::de_base64( substr($rest[1], 0, 22) );
888     } else {
889         $rounds = RT->Config->Get('BcryptCost');
890
891         # Generate a random 16-octet base64 salt
892         $salt = "";
893         $salt .= pack("C", int rand(256)) for 1..16;
894     }
895
896     my $hash = Crypt::Eksblowfish::Bcrypt::bcrypt_hash({
897         key_nul => 1,
898         cost    => $rounds,
899         salt    => $salt,
900     }, Digest::SHA::sha512( encode_utf8($password) ) );
901
902     return join("!", "", "bcrypt", sprintf("%02d", $rounds),
903                 Crypt::Eksblowfish::Bcrypt::en_base64( $salt ).
904                 Crypt::Eksblowfish::Bcrypt::en_base64( $hash )
905               );
906 }
907
908 sub _GeneratePassword_sha512 {
909     my $self = shift;
910     my ($password, $salt) = @_;
911
912     # Generate a 16-character base64 salt
913     unless ($salt) {
914         $salt = "";
915         $salt .= ("a".."z", "A".."Z","0".."9", "+", "/")[rand 64]
916             for 1..16;
917     }
918
919     my $sha = Digest::SHA->new(512);
920     $sha->add($salt);
921     $sha->add(encode_utf8($password));
922     return join("!", "", "sha512", $salt, $sha->b64digest);
923 }
924
925 =head3 _GeneratePassword PASSWORD [, SALT]
926
927 Returns a string to store in the database.  This string takes the form:
928
929    !method!salt!hash
930
931 By default, the method is currently C<bcrypt>.
932
933 =cut
934
935 sub _GeneratePassword {
936     my $self = shift;
937     return $self->_GeneratePassword_bcrypt(@_);
938 }
939
940 =head3 HasPassword
941
942 Returns true if the user has a valid password, otherwise returns false.
943
944 =cut
945
946 sub HasPassword {
947     my $self = shift;
948     my $pwd = $self->__Value('Password');
949     return undef if !defined $pwd
950                     || $pwd eq ''
951                     || $pwd eq '*NO-PASSWORD*';
952     return 1;
953 }
954
955 =head3 IsPassword
956
957 Returns true if the passed in value is this user's password.
958 Returns undef otherwise.
959
960 =cut
961
962 sub IsPassword {
963     my $self  = shift;
964     my $value = shift;
965
966     #TODO there isn't any apparent way to legitimately ACL this
967
968     # RT does not allow null passwords
969     if ( ( !defined($value) ) or ( $value eq '' ) ) {
970         return (undef);
971     }
972
973    if ( $self->PrincipalObj->Disabled ) {
974         $RT::Logger->info(
975             "Disabled user " . $self->Name . " tried to log in" );
976         return (undef);
977     }
978
979     unless ($self->HasPassword) {
980         return(undef);
981      }
982
983     my $stored = $self->__Value('Password');
984     if ($stored =~ /^!/) {
985         # If it's a new-style (>= RT 4.0) password, it starts with a '!'
986         my (undef, $method, @rest) = split /!/, $stored;
987         if ($method eq "bcrypt") {
988             return 0 unless $self->_GeneratePassword_bcrypt($value, @rest) eq $stored;
989             # Upgrade to a larger number of rounds if necessary
990             return 1 unless $rest[0] < RT->Config->Get('BcryptCost');
991         } elsif ($method eq "sha512") {
992             return 0 unless $self->_GeneratePassword_sha512($value, @rest) eq $stored;
993         } else {
994             $RT::Logger->warn("Unknown hash method $method");
995             return 0;
996         }
997     } elsif (length $stored == 40) {
998         # The truncated SHA256(salt,MD5(passwd)) form from 2010/12 is 40 characters long
999         my $hash = MIME::Base64::decode_base64($stored);
1000         # Decoding yields 30 byes; first 4 are the salt, the rest are substr(SHA256,0,26)
1001         my $salt = substr($hash, 0, 4, "");
1002         return 0 unless substr(Digest::SHA::sha256($salt . Digest::MD5::md5($value)), 0, 26) eq $hash;
1003     } elsif (length $stored == 32) {
1004         # Hex nonsalted-md5
1005         return 0 unless Digest::MD5::md5_hex(encode_utf8($value)) eq $stored;
1006     } elsif (length $stored == 22) {
1007         # Base64 nonsalted-md5
1008         return 0 unless Digest::MD5::md5_base64(encode_utf8($value)) eq $stored;
1009     } elsif (length $stored == 13) {
1010         # crypt() output
1011         return 0 unless crypt(encode_utf8($value), $stored) eq $stored;
1012     } else {
1013         $RT::Logger->warning("Unknown password form");
1014         return 0;
1015     }
1016
1017     # We got here by validating successfully, but with a legacy
1018     # password form.  Update to the most recent form.
1019     my $obj = $self->isa("RT::CurrentUser") ? $self->UserObj : $self;
1020     $obj->_Set(Field => 'Password', Value =>  $self->_GeneratePassword($value) );
1021     return 1;
1022 }
1023
1024 sub CurrentUserRequireToSetPassword {
1025     my $self = shift;
1026
1027     my %res = (
1028         CanSet => 1,
1029         Reason => '',
1030         RequireCurrent => 1,
1031     );
1032
1033     if ( RT->Config->Get('WebRemoteUserAuth')
1034         && !RT->Config->Get('WebFallbackToRTLogin')
1035     ) {
1036         $res{'CanSet'} = 0;
1037         $res{'Reason'} = $self->loc("External authentication enabled.");
1038     } elsif ( !$self->CurrentUser->HasPassword ) {
1039         if ( $self->CurrentUser->id == ($self->id||0) ) {
1040             # don't require current password if user has no
1041             $res{'RequireCurrent'} = 0;
1042         } else {
1043             $res{'CanSet'} = 0;
1044             $res{'Reason'} = $self->loc("Your password is not set.");
1045         }
1046     }
1047
1048     return %res;
1049 }
1050
1051 =head3 AuthToken
1052
1053 Returns an authentication string associated with the user. This
1054 string can be used to generate passwordless URLs to integrate
1055 RT with services and programms like callendar managers, rss
1056 readers and other.
1057
1058 =cut
1059
1060 sub AuthToken {
1061     my $self = shift;
1062     my $secret = $self->_Value( AuthToken => @_ );
1063     return $secret if $secret;
1064
1065     $secret = substr(Digest::MD5::md5_hex(time . {} . rand()),0,16);
1066
1067     my $tmp = RT::User->new( RT->SystemUser );
1068     $tmp->Load( $self->id );
1069     my ($status, $msg) = $tmp->SetAuthToken( $secret );
1070     unless ( $status ) {
1071         $RT::Logger->error( "Couldn't set auth token: $msg" );
1072         return undef;
1073     }
1074     return $secret;
1075 }
1076
1077 =head3 GenerateAuthToken
1078
1079 Generate a random authentication string for the user.
1080
1081 =cut
1082
1083 sub GenerateAuthToken {
1084     my $self = shift;
1085     my $token = substr(Digest::MD5::md5_hex(time . {} . rand()),0,16);
1086     return $self->SetAuthToken( $token );
1087 }
1088
1089 =head3 GenerateAuthString
1090
1091 Takes a string and returns back a hex hash string. Later you can use
1092 this pair to make sure it's generated by this user using L</ValidateAuthString>
1093
1094 =cut
1095
1096 sub GenerateAuthString {
1097     my $self = shift;
1098     my $protect = shift;
1099
1100     my $str = $self->AuthToken . $protect;
1101     utf8::encode($str);
1102
1103     return substr(Digest::MD5::md5_hex($str),0,16);
1104 }
1105
1106 =head3 ValidateAuthString
1107
1108 Takes auth string and protected string. Returns true is protected string
1109 has been protected by user's L</AuthToken>. See also L</GenerateAuthString>.
1110
1111 =cut
1112
1113 sub ValidateAuthString {
1114     my $self = shift;
1115     my $auth_string = shift;
1116     my $protected = shift;
1117
1118     my $str = $self->AuthToken . $protected;
1119     utf8::encode( $str );
1120
1121     return $auth_string eq substr(Digest::MD5::md5_hex($str),0,16);
1122 }
1123
1124 =head2 SetDisabled
1125
1126 Toggles the user's disabled flag.
1127 If this flag is
1128 set, all password checks for this user will fail. All ACL checks for this
1129 user will fail. The user will appear in no user listings.
1130
1131 =cut
1132
1133 sub SetDisabled {
1134     my $self = shift;
1135     my $val = shift;
1136     unless ( $self->CurrentUser->HasRight(Right => 'AdminUsers', Object => $RT::System) ) {
1137         return (0, $self->loc('Permission Denied'));
1138     }
1139
1140     $RT::Handle->BeginTransaction();
1141     my ($status, $msg) = $self->PrincipalObj->SetDisabled($val);
1142     unless ($status) {
1143         $RT::Handle->Rollback();
1144         $RT::Logger->warning(sprintf("Couldn't %s user %s", ($val == 1) ? "disable" : "enable", $self->PrincipalObj->Id));
1145         return ($status, $msg);
1146     }
1147     $self->_NewTransaction( Type => ($val == 1) ? "Disabled" : "Enabled" );
1148
1149     $RT::Handle->Commit();
1150
1151     if ( $val == 1 ) {
1152         return (1, $self->loc("User disabled"));
1153     } else {
1154         return (1, $self->loc("User enabled"));
1155     }
1156
1157 }
1158
1159 =head2 Disabled
1160
1161 Returns true if user is disabled or false otherwise
1162
1163 =cut
1164
1165 sub Disabled {
1166     my $self = shift;
1167     return $self->PrincipalObj->Disabled(@_);
1168 }
1169
1170 =head2 PrincipalObj
1171
1172 Returns the principal object for this user. returns an empty RT::Principal
1173 if there's no principal object matching this user.
1174 The response is cached. PrincipalObj should never ever change.
1175
1176 =cut
1177
1178 sub PrincipalObj {
1179     my $self = shift;
1180
1181     unless ( $self->id ) {
1182         $RT::Logger->error("Couldn't get principal for an empty user");
1183         return undef;
1184     }
1185
1186     if ( !$self->{_principal_obj} ) {
1187
1188         my $obj = RT::Principal->new( $self->CurrentUser );
1189         $obj->LoadById( $self->id );
1190         if (! $obj->id ) {
1191             $RT::Logger->crit( 'No principal for user #' . $self->id );
1192             return undef;
1193         } elsif ( $obj->PrincipalType ne 'User' ) {
1194             $RT::Logger->crit(   'User #' . $self->id . ' has principal of ' . $obj->PrincipalType . ' type' );
1195             return undef;
1196         }
1197         $self->{_principal_obj} = $obj;
1198     }
1199     return $self->{_principal_obj};
1200 }
1201
1202
1203 =head2 PrincipalId
1204
1205 Returns this user's PrincipalId
1206
1207 =cut
1208
1209 sub PrincipalId {
1210     my $self = shift;
1211     return $self->Id;
1212 }
1213
1214 =head2 HasGroupRight
1215
1216 Takes a paramhash which can contain
1217 these items:
1218     GroupObj => RT::Group or Group => integer
1219     Right => 'Right'
1220
1221
1222 Returns 1 if this user has the right specified in the paramhash for the Group
1223 passed in.
1224
1225 Returns undef if they don't.
1226
1227 =cut
1228
1229 sub HasGroupRight {
1230     my $self = shift;
1231     my %args = (
1232         GroupObj    => undef,
1233         Group       => undef,
1234         Right       => undef,
1235         @_
1236     );
1237
1238
1239     if ( defined $args{'Group'} ) {
1240         $args{'GroupObj'} = RT::Group->new( $self->CurrentUser );
1241         $args{'GroupObj'}->Load( $args{'Group'} );
1242     }
1243
1244     # Validate and load up the GroupId
1245     unless ( ( defined $args{'GroupObj'} ) and ( $args{'GroupObj'}->Id ) ) {
1246         return undef;
1247     }
1248
1249     # Figure out whether a user has the right we're asking about.
1250     my $retval = $self->HasRight(
1251         Object => $args{'GroupObj'},
1252         Right     => $args{'Right'},
1253     );
1254
1255     return ($retval);
1256 }
1257
1258 =head2 OwnGroups
1259
1260 Returns a group collection object containing the groups of which this
1261 user is a member.
1262
1263 =cut
1264
1265 sub OwnGroups {
1266     my $self = shift;
1267     my $groups = RT::Groups->new($self->CurrentUser);
1268     $groups->LimitToUserDefinedGroups;
1269     $groups->WithMember(
1270         PrincipalId => $self->Id,
1271         Recursively => 1
1272     );
1273     return $groups;
1274 }
1275
1276 =head2 HasRight
1277
1278 Shim around PrincipalObj->HasRight. See L<RT::Principal>.
1279
1280 =cut
1281
1282 sub HasRight {
1283     my $self = shift;
1284     return $self->PrincipalObj->HasRight(@_);
1285 }
1286
1287 =head2 CurrentUserCanSee [FIELD]
1288
1289 Returns true if the current user can see the user, based on if it is
1290 public, ourself, or we have AdminUsers
1291
1292 =cut
1293
1294 sub CurrentUserCanSee {
1295     my $self = shift;
1296     my ($what, $txn) = @_;
1297
1298     # If it's a public property, fine
1299     return 1 if $self->_Accessible( $what, 'public' );
1300
1301     # Users can see all of their own properties
1302     return 1 if defined($self->Id) and $self->CurrentUser->Id == $self->Id;
1303
1304     # If the user has the admin users right, that's also enough
1305     return 1 if $self->CurrentUserHasRight( 'AdminUsers' );
1306
1307     # Transactions of public properties are visible to users with ShowUserHistory
1308     if ($what eq "Transaction" and $self->CurrentUserHasRight( 'ShowUserHistory' )) {
1309         my $type = $txn->__Value('Type');
1310         my $field = $txn->__Value('Field');
1311         return 1 if $type eq "Set" and $self->CurrentUserCanSee($field, $txn);
1312
1313         # RT::Transaction->CurrentUserCanSee deals with ensuring we meet
1314         # the ACLs on CFs, so allow them here
1315         return 1 if $type eq "CustomField";
1316     }
1317
1318     return 0;
1319 }
1320
1321 =head2 CurrentUserCanModify RIGHT
1322
1323 If the user has rights for this object, either because
1324 he has 'AdminUsers' or (if he's trying to edit himself and the right isn't an
1325 admin right) 'ModifySelf', return 1. otherwise, return undef.
1326
1327 =cut
1328
1329 sub CurrentUserCanModify {
1330     my $self  = shift;
1331     my $field = shift;
1332
1333     if ( $self->CurrentUser->HasRight(Right => 'AdminUsers', Object => $RT::System) ) {
1334         return (1);
1335     }
1336
1337     #If the field is marked as an "administrators only" field,
1338     # don't let the user touch it.
1339     elsif ( $self->_Accessible( $field, 'admin' ) ) {
1340         return (undef);
1341     }
1342
1343     #If the current user is trying to modify themselves
1344     elsif ( ( $self->id == $self->CurrentUser->id )
1345         and ( $self->CurrentUser->HasRight(Right => 'ModifySelf', Object => $RT::System) ) )
1346     {
1347         return (1);
1348     }
1349
1350     #If we don't have a good reason to grant them rights to modify
1351     # by now, they lose
1352     else {
1353         return (undef);
1354     }
1355
1356 }
1357
1358 =head2 CurrentUserHasRight
1359
1360 Takes a single argument. returns 1 if $Self->CurrentUser
1361 has the requested right. returns undef otherwise
1362
1363 =cut
1364
1365 sub CurrentUserHasRight {
1366     my $self  = shift;
1367     my $right = shift;
1368
1369     return ( $self->CurrentUser->HasRight(Right => $right, Object => $RT::System) );
1370 }
1371
1372 sub _PrefName {
1373     my $name = shift;
1374     if (ref $name) {
1375         $name = ref($name).'-'.$name->Id;
1376     }
1377
1378     return 'Pref-'. $name;
1379 }
1380
1381 =head2 Preferences NAME/OBJ DEFAULT
1382
1383 Obtain user preferences associated with given object or name.
1384 Returns DEFAULT if no preferences found.  If DEFAULT is a hashref,
1385 override the entries with user preferences.
1386
1387 =cut
1388
1389 our %PREFERENCES_CACHE = ();
1390
1391 sub Preferences {
1392     my $self  = shift;
1393     my $name = _PrefName(shift);
1394     my $default = shift;
1395
1396     my $content;
1397     if ( exists $PREFERENCES_CACHE{ $self->id }{ $name } ) {
1398         $content = $PREFERENCES_CACHE{ $self->id }{ $name };
1399     }
1400     else {
1401         my $attr = RT::Attribute->new( $self->CurrentUser );
1402         $attr->LoadByNameAndObject( Object => $self, Name => $name );
1403         $PREFERENCES_CACHE{ $self->id }{ $name } = $content
1404             = $attr->Id ? $attr->Content : undef;
1405     }
1406
1407     unless ( ref $content eq 'HASH' ) {
1408         return defined $content ? $content : $default;
1409     }
1410
1411     if (ref $default eq 'HASH') {
1412         for (keys %$default) {
1413             exists $content->{$_} or $content->{$_} = $default->{$_};
1414         }
1415     } elsif (defined $default) {
1416         $RT::Logger->error("Preferences $name for user #".$self->Id." is hash but default is not");
1417     }
1418     return $content;
1419 }
1420
1421 =head2 SetPreferences NAME/OBJ VALUE
1422
1423 Set user preferences associated with given object or name.
1424
1425 =cut
1426
1427 sub SetPreferences {
1428     my $self = shift;
1429     my $name = _PrefName( shift );
1430     my $value = shift;
1431
1432     return (0, $self->loc("No permission to set preferences"))
1433         unless $self->CurrentUserCanModify('Preferences');
1434
1435     # we clear cache in RT::Attribute
1436
1437     my $attr = RT::Attribute->new( $self->CurrentUser );
1438     $attr->LoadByNameAndObject( Object => $self, Name => $name );
1439     if ( $attr->Id ) {
1440         my ($ok, $msg) = $attr->SetContent( $value );
1441         return (1, "No updates made")
1442             if $msg eq "That is already the current value";
1443         return ($ok, $msg);
1444     } else {
1445         return $self->AddAttribute( Name => $name, Content => $value );
1446     }
1447 }
1448
1449 =head2 Stylesheet
1450
1451 Returns a list of valid stylesheets take from preferences.
1452
1453 =cut
1454
1455 sub Stylesheet {
1456     my $self = shift;
1457
1458     my $style = RT->Config->Get('WebDefaultStylesheet', $self->CurrentUser);
1459
1460     if (RT::Interface::Web->ComponentPathIsSafe($style)) {
1461         for my $root (RT::Interface::Web->StaticRoots) {
1462             if (-d "$root/css/$style") {
1463                 return $style
1464             }
1465         }
1466     }
1467
1468     # Fall back to the system stylesheet.
1469     return RT->Config->Get('WebDefaultStylesheet');
1470 }
1471
1472 =head2 WatchedQueues ROLE_LIST
1473
1474 Returns a RT::Queues object containing every queue watched by the user.
1475
1476 Takes a list of roles which is some subset of ('Cc', 'AdminCc').  Defaults to:
1477
1478 $user->WatchedQueues('Cc', 'AdminCc');
1479
1480 =cut
1481
1482 sub WatchedQueues {
1483
1484     my $self = shift;
1485     my @roles = @_ ? @_ : ('Cc', 'AdminCc');
1486
1487     $RT::Logger->debug('WatcheQueues got user ' . $self->Name);
1488
1489     my $watched_queues = RT::Queues->new($self->CurrentUser);
1490
1491     my $group_alias = $watched_queues->Join(
1492                                              ALIAS1 => 'main',
1493                                              FIELD1 => 'id',
1494                                              TABLE2 => 'Groups',
1495                                              FIELD2 => 'Instance',
1496                                            );
1497
1498     $watched_queues->Limit(
1499                             ALIAS => $group_alias,
1500                             FIELD => 'Domain',
1501                             VALUE => 'RT::Queue-Role',
1502                             ENTRYAGGREGATOR => 'AND',
1503                             CASESENSITIVE => 0,
1504                           );
1505     if (grep { $_ eq 'Cc' } @roles) {
1506         $watched_queues->Limit(
1507                                 SUBCLAUSE => 'LimitToWatchers',
1508                                 ALIAS => $group_alias,
1509                                 FIELD => 'Name',
1510                                 VALUE => 'Cc',
1511                                 ENTRYAGGREGATOR => 'OR',
1512                               );
1513     }
1514     if (grep { $_ eq 'AdminCc' } @roles) {
1515         $watched_queues->Limit(
1516                                 SUBCLAUSE => 'LimitToWatchers',
1517                                 ALIAS => $group_alias,
1518                                 FIELD => 'Name',
1519                                 VALUE => 'AdminCc',
1520                                 ENTRYAGGREGATOR => 'OR',
1521                               );
1522     }
1523
1524     my $queues_alias = $watched_queues->Join(
1525                                               ALIAS1 => $group_alias,
1526                                               FIELD1 => 'id',
1527                                               TABLE2 => 'CachedGroupMembers',
1528                                               FIELD2 => 'GroupId',
1529                                             );
1530     $watched_queues->Limit(
1531                             ALIAS => $queues_alias,
1532                             FIELD => 'MemberId',
1533                             VALUE => $self->PrincipalId,
1534                           );
1535     $watched_queues->Limit(
1536                             ALIAS => $queues_alias,
1537                             FIELD => 'Disabled',
1538                             VALUE => 0,
1539                           );
1540
1541
1542     $RT::Logger->debug("WatchedQueues got " . $watched_queues->Count . " queues");
1543
1544     return $watched_queues;
1545
1546 }
1547
1548 sub _Set {
1549     my $self = shift;
1550
1551     my %args = (
1552         Field => undef,
1553         Value => undef,
1554     TransactionType   => 'Set',
1555     RecordTransaction => 1,
1556         @_
1557     );
1558
1559     # Nobody is allowed to futz with RT_System or Nobody
1560
1561     if ( ($self->Id == RT->SystemUser->Id )  ||
1562          ($self->Id == RT->Nobody->Id)) {
1563         return ( 0, $self->loc("Can not modify system users") );
1564     }
1565     unless ( $self->CurrentUserCanModify( $args{'Field'} ) ) {
1566         return ( 0, $self->loc("Permission Denied") );
1567     }
1568
1569     my $Old = $self->SUPER::_Value("$args{'Field'}");
1570
1571     my ($ret, $msg) = $self->SUPER::_Set( Field => $args{'Field'},
1572                       Value => $args{'Value'} );
1573
1574     #If we can't actually set the field to the value, don't record
1575     # a transaction. instead, get out of here.
1576     if ( $ret == 0 ) { return ( 0, $msg ); }
1577
1578     if ( $args{'RecordTransaction'} == 1 ) {
1579         if ($args{'Field'} eq "Password") {
1580             $args{'Value'} = $Old = '********';
1581         }
1582         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
1583                                                Type => $args{'TransactionType'},
1584                                                Field     => $args{'Field'},
1585                                                NewValue  => $args{'Value'},
1586                                                OldValue  => $Old,
1587                                                TimeTaken => $args{'TimeTaken'},
1588         );
1589         return ( $Trans, scalar $TransObj->BriefDescription );
1590     } else {
1591         return ( $ret, $msg );
1592     }
1593 }
1594
1595 =head2 _Value
1596
1597 Takes the name of a table column.
1598 Returns its value as a string, if the user passes an ACL check
1599
1600 =cut
1601
1602 sub _Value {
1603
1604     my $self  = shift;
1605     my $field = shift;
1606
1607     # Defer to the abstraction above to know if the field can be read
1608     return $self->SUPER::_Value($field) if $self->CurrentUserCanSee($field);
1609     return undef;
1610 }
1611
1612 =head2 FriendlyName
1613
1614 Return the friendly name
1615
1616 =cut
1617
1618 sub FriendlyName {
1619     my $self = shift;
1620     return $self->RealName if defined $self->RealName and length $self->RealName;
1621     return $self->Name;
1622 }
1623
1624 =head2 Format
1625
1626 Class or object method.
1627
1628 Returns a string describing a user in the current user's preferred format.
1629
1630 May be invoked in three ways:
1631
1632     $UserObj->Format;
1633     RT::User->Format( User => $UserObj );   # same as above
1634     RT::User->Format( Address => $AddressObj, CurrentUser => $CurrentUserObj );
1635
1636 Possible arguments are:
1637
1638 =over
1639
1640 =item User
1641
1642 An L<RT::User> object representing the user to format.  Preferred to Address.
1643
1644 =item Address
1645
1646 An L<Email::Address> object representing the user address to format.  Address
1647 will be used to lookup an L<RT::User> if possible.
1648
1649 =item CurrentUser
1650
1651 Required when Format is called as a class method with an Address argument.
1652 Otherwise, this argument is ignored in preference to the CurrentUser of the
1653 involved L<RT::User> object.
1654
1655 =item Format
1656
1657 Specifies the format to use, overriding any set from the config or current
1658 user's preferences.
1659
1660 =back
1661
1662 =cut
1663
1664 sub Format {
1665     my $self = shift;
1666     my %args = (
1667         User        => undef,
1668         Address     => undef,
1669         CurrentUser => undef,
1670         Format      => undef,
1671         @_
1672     );
1673
1674     if (blessed($self) and $self->id) {
1675         @args{"User", "CurrentUser"} = ($self, $self->CurrentUser);
1676     }
1677     elsif ($args{User} and $args{User}->id) {
1678         $args{CurrentUser} = $args{User}->CurrentUser;
1679     }
1680     elsif ($args{Address} and $args{CurrentUser}) {
1681         $args{User} = RT::User->new( $args{CurrentUser} );
1682         $args{User}->LoadByEmail( $args{Address}->address );
1683         if ($args{User}->id) {
1684             delete $args{Address};
1685         } else {
1686             delete $args{User};
1687         }
1688     }
1689     else {
1690         RT->Logger->warning("Invalid arguments to RT::User->Format at @{[join '/', caller]}");
1691         return "";
1692     }
1693
1694     $args{Format} ||= RT->Config->Get("UsernameFormat", $args{CurrentUser});
1695     $args{Format} =~ s/[^A-Za-z0-9_]+//g;
1696
1697     my $method    = "_FormatUser" . ucfirst lc $args{Format};
1698     my $formatter = $self->can($method);
1699
1700     unless ($formatter) {
1701         RT->Logger->error(
1702             "Either system config or user #" . $args{CurrentUser}->id .
1703             " picked UsernameFormat $args{Format}, but RT::User->$method doesn't exist"
1704         );
1705         $formatter = $self->can("_FormatUserRole");
1706     }
1707     return $formatter->( $self, map { $_ => $args{$_} } qw(User Address) );
1708 }
1709
1710 sub _FormatUserRole {
1711     my $self = shift;
1712     my %args = @_;
1713
1714     my $user = $args{User};
1715     return $self->_FormatUserVerbose(@_)
1716         unless $user and $user->Privileged;
1717
1718     my $name = $user->Name;
1719     $name .= " (".$user->RealName.")"
1720         if $user->RealName and lc $user->RealName ne lc $user->Name;
1721     return $name;
1722 }
1723
1724 sub _FormatUserConcise {
1725     my $self = shift;
1726     my %args = @_;
1727     return $args{User} ? $args{User}->FriendlyName : $args{Address}->address;
1728 }
1729
1730 sub _FormatUserVerbose {
1731     my $self = shift;
1732     my %args = @_;
1733     my ($user, $address) = @args{"User", "Address"};
1734
1735     my $email   = '';
1736     my $phrase  = '';
1737     my $comment = '';
1738
1739     if ($user) {
1740         $email   = $user->EmailAddress || '';
1741         $phrase  = $user->RealName  if $user->RealName and lc $user->RealName ne lc $email;
1742         $comment = $user->Name      if lc $user->Name ne lc $email;
1743     } else {
1744         ($email, $phrase, $comment) = (map { $address->$_ } "address", "phrase", "comment");
1745     }
1746
1747     return join " ", grep { $_ } ($phrase || $comment || ''), ($email ? "<$email>" : "");
1748 }
1749
1750 =head2 PreferredKey
1751
1752 Returns the preferred key of the user. If none is set, then this will query
1753 GPG and set the preferred key to the maximally trusted key found (and then
1754 return it). Returns C<undef> if no preferred key can be found.
1755
1756 =cut
1757
1758 sub PreferredKey
1759 {
1760     my $self = shift;
1761     return undef unless RT->Config->Get('GnuPG')->{'Enable'};
1762
1763     if ( ($self->CurrentUser->Id != $self->Id )  &&
1764           !$self->CurrentUser->HasRight(Right =>'AdminUsers', Object => $RT::System) ) {
1765           return undef;
1766     }
1767
1768
1769
1770     my $prefkey = $self->FirstAttribute('PreferredKey');
1771     return $prefkey->Content if $prefkey;
1772
1773     # we don't have a preferred key for this user, so now we must query GPG
1774     my %res = RT::Crypt->GetKeysForEncryption($self->EmailAddress);
1775     return undef unless defined $res{'info'};
1776     my @keys = @{ $res{'info'} };
1777     return undef if @keys == 0;
1778
1779     if (@keys == 1) {
1780         $prefkey = $keys[0]->{'id'} || $keys[0]->{'Fingerprint'};
1781     } else {
1782         # prefer the maximally trusted key
1783         @keys = sort { $b->{'TrustLevel'} <=> $a->{'TrustLevel'} } @keys;
1784         $prefkey = $keys[0]->{'id'} || $keys[0]->{'Fingerprint'};
1785     }
1786
1787     $self->SetAttribute(Name => 'PreferredKey', Content => $prefkey);
1788     return $prefkey;
1789 }
1790
1791 sub PrivateKey {
1792     my $self = shift;
1793
1794
1795     #If the user wants to see their own values, let them.
1796     #If the user is an admin, let them.
1797     #Otherwwise, don't let them.
1798     #
1799     if ( ($self->CurrentUser->Id != $self->Id )  &&
1800           !$self->CurrentUser->HasRight(Right =>'AdminUsers', Object => $RT::System) ) {
1801           return undef;
1802     }
1803
1804     my $key = $self->FirstAttribute('PrivateKey') or return undef;
1805     return $key->Content;
1806 }
1807
1808 sub SetPrivateKey {
1809     my $self = shift;
1810     my $key = shift;
1811
1812     unless ($self->CurrentUserCanModify('PrivateKey')) {
1813         return (0, $self->loc("Permission Denied"));
1814     }
1815
1816     unless ( $key ) {
1817         my ($status, $msg) = $self->DeleteAttribute('PrivateKey');
1818         unless ( $status ) {
1819             $RT::Logger->error( "Couldn't delete attribute: $msg" );
1820             return ($status, $self->loc("Couldn't unset private key"));
1821         }
1822         return ($status, $self->loc("Unset private key"));
1823     }
1824
1825     # check that it's really private key
1826     {
1827         my %tmp = RT::Crypt->GetKeysForSigning( Signer => $key, Protocol => 'GnuPG' );
1828         return (0, $self->loc("No such key or it's not suitable for signing"))
1829             if $tmp{'exit_code'} || !$tmp{'info'};
1830     }
1831
1832     my ($status, $msg) = $self->SetAttribute(
1833         Name => 'PrivateKey',
1834         Content => $key,
1835     );
1836     return ($status, $self->loc("Couldn't set private key"))
1837         unless $status;
1838     return ($status, $self->loc("Set private key"));
1839 }
1840
1841 sub SetLang {
1842     my $self = shift;
1843     my ($lang) = @_;
1844
1845     unless ($self->CurrentUserCanModify('Lang')) {
1846         return (0, $self->loc("Permission Denied"));
1847     }
1848
1849     # Local hack to cause the result message to be in the _new_ language
1850     # if we're updating ourselves
1851     $self->CurrentUser->{LangHandle} = RT::I18N->get_handle( $lang )
1852         if $self->CurrentUser->id == $self->id;
1853     return $self->_Set( Field => 'Lang', Value => $lang );
1854 }
1855
1856 sub BasicColumns {
1857     (
1858     [ Name => 'Username' ],
1859     [ EmailAddress => 'Email' ],
1860     [ RealName => 'Name' ],
1861     [ Organization => 'Organization' ],
1862     );
1863 }
1864
1865 =head2 Bookmarks
1866
1867 Returns an unordered list of IDs representing the user's bookmarked tickets.
1868
1869 =cut
1870
1871 sub Bookmarks {
1872     my $self = shift;
1873     my $bookmarks = $self->FirstAttribute('Bookmarks');
1874     return if !$bookmarks;
1875
1876     $bookmarks = $bookmarks->Content;
1877     return if !$bookmarks;
1878
1879     return keys %$bookmarks;
1880 }
1881
1882 =head2 HasBookmark TICKET
1883
1884 Returns whether the provided ticket is bookmarked by the user.
1885
1886 =cut
1887
1888 sub HasBookmark {
1889     my $self   = shift;
1890     my $ticket = shift;
1891     my $id     = $ticket->id;
1892
1893     # maintain bookmarks across merges
1894     my @ids = ($id, $ticket->Merged);
1895
1896     my $bookmarks = $self->FirstAttribute('Bookmarks');
1897     $bookmarks = $bookmarks ? $bookmarks->Content : {};
1898
1899     my @bookmarked = grep { $bookmarks->{ $_ } } @ids;
1900     return @bookmarked ? 1 : 0;
1901 }
1902
1903 =head2 ToggleBookmark TICKET
1904
1905 Toggles whether the provided ticket is bookmarked by the user.
1906
1907 =cut
1908
1909 sub ToggleBookmark {
1910     my $self   = shift;
1911     my $ticket = shift;
1912     my $id     = $ticket->id;
1913
1914     # maintain bookmarks across merges
1915     my @ids = ($id, $ticket->Merged);
1916
1917     my $bookmarks = $self->FirstAttribute('Bookmarks');
1918     $bookmarks = $bookmarks ? $bookmarks->Content : {};
1919
1920     my $is_bookmarked;
1921
1922     if ( grep { $bookmarks->{ $_ } } @ids ) {
1923         delete $bookmarks->{ $_ } foreach @ids;
1924         $is_bookmarked = 0;
1925     } else {
1926         $bookmarks->{ $id } = 1;
1927         $is_bookmarked = 1;
1928     }
1929
1930     $self->SetAttribute(
1931         Name    => 'Bookmarks',
1932         Content => $bookmarks,
1933     );
1934
1935     return $is_bookmarked;
1936 }
1937
1938 =head2 Create PARAMHASH
1939
1940 Create takes a hash of values and creates a row in the database:
1941
1942   varchar(200) 'Name'.
1943   varbinary(256) 'Password'.
1944   varchar(16) 'AuthToken'.
1945   text 'Comments'.
1946   text 'Signature'.
1947   varchar(120) 'EmailAddress'.
1948   text 'FreeformContactInfo'.
1949   varchar(200) 'Organization'.
1950   varchar(120) 'RealName'.
1951   varchar(16) 'NickName'.
1952   varchar(16) 'Lang'.
1953   varchar(16) 'EmailEncoding'.
1954   varchar(16) 'WebEncoding'.
1955   varchar(100) 'ExternalContactInfoId'.
1956   varchar(30) 'ContactInfoSystem'.
1957   varchar(100) 'ExternalAuthId'.
1958   varchar(30) 'AuthSystem'.
1959   varchar(16) 'Gecos'.
1960   varchar(30) 'HomePhone'.
1961   varchar(30) 'WorkPhone'.
1962   varchar(30) 'MobilePhone'.
1963   varchar(30) 'PagerPhone'.
1964   varchar(200) 'Address1'.
1965   varchar(200) 'Address2'.
1966   varchar(100) 'City'.
1967   varchar(100) 'State'.
1968   varchar(16) 'Zip'.
1969   varchar(50) 'Country'.
1970   varchar(50) 'Timezone'.
1971   text 'PGPKey'.
1972
1973 =cut
1974
1975
1976
1977
1978 =head2 id
1979
1980 Returns the current value of id. 
1981 (In the database, id is stored as int(11).)
1982
1983
1984 =cut
1985
1986
1987 =head2 Name
1988
1989 Returns the current value of Name. 
1990 (In the database, Name is stored as varchar(200).)
1991
1992
1993
1994 =head2 SetName VALUE
1995
1996
1997 Set Name to VALUE. 
1998 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1999 (In the database, Name will be stored as a varchar(200).)
2000
2001
2002 =cut
2003
2004
2005 =head2 Password
2006
2007 Returns the current value of Password. 
2008 (In the database, Password is stored as varchar(256).)
2009
2010
2011
2012 =head2 SetPassword VALUE
2013
2014
2015 Set Password to VALUE. 
2016 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2017 (In the database, Password will be stored as a varchar(256).)
2018
2019
2020 =cut
2021
2022
2023 =head2 AuthToken
2024
2025 Returns the current value of AuthToken. 
2026 (In the database, AuthToken is stored as varchar(16).)
2027
2028
2029
2030 =head2 SetAuthToken VALUE
2031
2032
2033 Set AuthToken to VALUE. 
2034 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2035 (In the database, AuthToken will be stored as a varchar(16).)
2036
2037
2038 =cut
2039
2040
2041 =head2 Comments
2042
2043 Returns the current value of Comments. 
2044 (In the database, Comments is stored as text.)
2045
2046
2047
2048 =head2 SetComments VALUE
2049
2050
2051 Set Comments to VALUE. 
2052 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2053 (In the database, Comments will be stored as a text.)
2054
2055
2056 =cut
2057
2058
2059 =head2 Signature
2060
2061 Returns the current value of Signature. 
2062 (In the database, Signature is stored as text.)
2063
2064
2065
2066 =head2 SetSignature VALUE
2067
2068
2069 Set Signature to VALUE. 
2070 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2071 (In the database, Signature will be stored as a text.)
2072
2073
2074 =cut
2075
2076
2077 =head2 EmailAddress
2078
2079 Returns the current value of EmailAddress. 
2080 (In the database, EmailAddress is stored as varchar(120).)
2081
2082
2083
2084 =head2 SetEmailAddress VALUE
2085
2086
2087 Set EmailAddress to VALUE. 
2088 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2089 (In the database, EmailAddress will be stored as a varchar(120).)
2090
2091
2092 =cut
2093
2094
2095 =head2 FreeformContactInfo
2096
2097 Returns the current value of FreeformContactInfo. 
2098 (In the database, FreeformContactInfo is stored as text.)
2099
2100
2101
2102 =head2 SetFreeformContactInfo VALUE
2103
2104
2105 Set FreeformContactInfo to VALUE. 
2106 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2107 (In the database, FreeformContactInfo will be stored as a text.)
2108
2109
2110 =cut
2111
2112
2113 =head2 Organization
2114
2115 Returns the current value of Organization. 
2116 (In the database, Organization is stored as varchar(200).)
2117
2118
2119
2120 =head2 SetOrganization VALUE
2121
2122
2123 Set Organization to VALUE. 
2124 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2125 (In the database, Organization will be stored as a varchar(200).)
2126
2127
2128 =cut
2129
2130
2131 =head2 RealName
2132
2133 Returns the current value of RealName. 
2134 (In the database, RealName is stored as varchar(120).)
2135
2136
2137
2138 =head2 SetRealName VALUE
2139
2140
2141 Set RealName to VALUE. 
2142 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2143 (In the database, RealName will be stored as a varchar(120).)
2144
2145
2146 =cut
2147
2148
2149 =head2 NickName
2150
2151 Returns the current value of NickName. 
2152 (In the database, NickName is stored as varchar(16).)
2153
2154
2155
2156 =head2 SetNickName VALUE
2157
2158
2159 Set NickName to VALUE. 
2160 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2161 (In the database, NickName will be stored as a varchar(16).)
2162
2163
2164 =cut
2165
2166
2167 =head2 Lang
2168
2169 Returns the current value of Lang. 
2170 (In the database, Lang is stored as varchar(16).)
2171
2172
2173
2174 =head2 SetLang VALUE
2175
2176
2177 Set Lang to VALUE. 
2178 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2179 (In the database, Lang will be stored as a varchar(16).)
2180
2181
2182 =cut
2183
2184
2185 =head2 EmailEncoding
2186
2187 Returns the current value of EmailEncoding. 
2188 (In the database, EmailEncoding is stored as varchar(16).)
2189
2190
2191
2192 =head2 SetEmailEncoding VALUE
2193
2194
2195 Set EmailEncoding to VALUE. 
2196 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2197 (In the database, EmailEncoding will be stored as a varchar(16).)
2198
2199
2200 =cut
2201
2202
2203 =head2 WebEncoding
2204
2205 Returns the current value of WebEncoding. 
2206 (In the database, WebEncoding is stored as varchar(16).)
2207
2208
2209
2210 =head2 SetWebEncoding VALUE
2211
2212
2213 Set WebEncoding to VALUE. 
2214 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2215 (In the database, WebEncoding will be stored as a varchar(16).)
2216
2217
2218 =cut
2219
2220
2221 =head2 ExternalContactInfoId
2222
2223 Returns the current value of ExternalContactInfoId. 
2224 (In the database, ExternalContactInfoId is stored as varchar(100).)
2225
2226
2227
2228 =head2 SetExternalContactInfoId VALUE
2229
2230
2231 Set ExternalContactInfoId to VALUE. 
2232 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2233 (In the database, ExternalContactInfoId will be stored as a varchar(100).)
2234
2235
2236 =cut
2237
2238
2239 =head2 ContactInfoSystem
2240
2241 Returns the current value of ContactInfoSystem. 
2242 (In the database, ContactInfoSystem is stored as varchar(30).)
2243
2244
2245
2246 =head2 SetContactInfoSystem VALUE
2247
2248
2249 Set ContactInfoSystem to VALUE. 
2250 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2251 (In the database, ContactInfoSystem will be stored as a varchar(30).)
2252
2253
2254 =cut
2255
2256
2257 =head2 ExternalAuthId
2258
2259 Returns the current value of ExternalAuthId. 
2260 (In the database, ExternalAuthId is stored as varchar(100).)
2261
2262
2263
2264 =head2 SetExternalAuthId VALUE
2265
2266
2267 Set ExternalAuthId to VALUE. 
2268 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2269 (In the database, ExternalAuthId will be stored as a varchar(100).)
2270
2271
2272 =cut
2273
2274
2275 =head2 AuthSystem
2276
2277 Returns the current value of AuthSystem. 
2278 (In the database, AuthSystem is stored as varchar(30).)
2279
2280
2281
2282 =head2 SetAuthSystem VALUE
2283
2284
2285 Set AuthSystem to VALUE. 
2286 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2287 (In the database, AuthSystem will be stored as a varchar(30).)
2288
2289
2290 =cut
2291
2292
2293 =head2 Gecos
2294
2295 Returns the current value of Gecos. 
2296 (In the database, Gecos is stored as varchar(16).)
2297
2298
2299
2300 =head2 SetGecos VALUE
2301
2302
2303 Set Gecos to VALUE. 
2304 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2305 (In the database, Gecos will be stored as a varchar(16).)
2306
2307
2308 =cut
2309
2310
2311 =head2 HomePhone
2312
2313 Returns the current value of HomePhone. 
2314 (In the database, HomePhone is stored as varchar(30).)
2315
2316
2317
2318 =head2 SetHomePhone VALUE
2319
2320
2321 Set HomePhone to VALUE. 
2322 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2323 (In the database, HomePhone will be stored as a varchar(30).)
2324
2325
2326 =cut
2327
2328
2329 =head2 WorkPhone
2330
2331 Returns the current value of WorkPhone. 
2332 (In the database, WorkPhone is stored as varchar(30).)
2333
2334
2335
2336 =head2 SetWorkPhone VALUE
2337
2338
2339 Set WorkPhone to VALUE. 
2340 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2341 (In the database, WorkPhone will be stored as a varchar(30).)
2342
2343
2344 =cut
2345
2346
2347 =head2 MobilePhone
2348
2349 Returns the current value of MobilePhone. 
2350 (In the database, MobilePhone is stored as varchar(30).)
2351
2352
2353
2354 =head2 SetMobilePhone VALUE
2355
2356
2357 Set MobilePhone to VALUE. 
2358 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2359 (In the database, MobilePhone will be stored as a varchar(30).)
2360
2361
2362 =cut
2363
2364
2365 =head2 PagerPhone
2366
2367 Returns the current value of PagerPhone. 
2368 (In the database, PagerPhone is stored as varchar(30).)
2369
2370
2371
2372 =head2 SetPagerPhone VALUE
2373
2374
2375 Set PagerPhone to VALUE. 
2376 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2377 (In the database, PagerPhone will be stored as a varchar(30).)
2378
2379
2380 =cut
2381
2382
2383 =head2 Address1
2384
2385 Returns the current value of Address1. 
2386 (In the database, Address1 is stored as varchar(200).)
2387
2388
2389
2390 =head2 SetAddress1 VALUE
2391
2392
2393 Set Address1 to VALUE. 
2394 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2395 (In the database, Address1 will be stored as a varchar(200).)
2396
2397
2398 =cut
2399
2400
2401 =head2 Address2
2402
2403 Returns the current value of Address2. 
2404 (In the database, Address2 is stored as varchar(200).)
2405
2406
2407
2408 =head2 SetAddress2 VALUE
2409
2410
2411 Set Address2 to VALUE. 
2412 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2413 (In the database, Address2 will be stored as a varchar(200).)
2414
2415
2416 =cut
2417
2418
2419 =head2 City
2420
2421 Returns the current value of City. 
2422 (In the database, City is stored as varchar(100).)
2423
2424
2425
2426 =head2 SetCity VALUE
2427
2428
2429 Set City to VALUE. 
2430 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2431 (In the database, City will be stored as a varchar(100).)
2432
2433
2434 =cut
2435
2436
2437 =head2 State
2438
2439 Returns the current value of State. 
2440 (In the database, State is stored as varchar(100).)
2441
2442
2443
2444 =head2 SetState VALUE
2445
2446
2447 Set State to VALUE. 
2448 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2449 (In the database, State will be stored as a varchar(100).)
2450
2451
2452 =cut
2453
2454
2455 =head2 Zip
2456
2457 Returns the current value of Zip. 
2458 (In the database, Zip is stored as varchar(16).)
2459
2460
2461
2462 =head2 SetZip VALUE
2463
2464
2465 Set Zip to VALUE. 
2466 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2467 (In the database, Zip will be stored as a varchar(16).)
2468
2469
2470 =cut
2471
2472
2473 =head2 Country
2474
2475 Returns the current value of Country. 
2476 (In the database, Country is stored as varchar(50).)
2477
2478
2479
2480 =head2 SetCountry VALUE
2481
2482
2483 Set Country to VALUE. 
2484 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2485 (In the database, Country will be stored as a varchar(50).)
2486
2487
2488 =cut
2489
2490
2491 =head2 Timezone
2492
2493 Returns the current value of Timezone. 
2494 (In the database, Timezone is stored as varchar(50).)
2495
2496
2497
2498 =head2 SetTimezone VALUE
2499
2500
2501 Set Timezone to VALUE. 
2502 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2503 (In the database, Timezone will be stored as a varchar(50).)
2504
2505
2506 =cut
2507
2508
2509 =head2 PGPKey
2510
2511 Returns the current value of PGPKey. 
2512 (In the database, PGPKey is stored as text.)
2513
2514
2515
2516 =head2 SetPGPKey VALUE
2517
2518
2519 Set PGPKey to VALUE. 
2520 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2521 (In the database, PGPKey will be stored as a text.)
2522
2523
2524 =cut
2525
2526
2527 =head2 SMIMECertificate
2528
2529 Returns the current value of SMIMECertificate. 
2530 (In the database, SMIMECertificate is stored as text.)
2531
2532
2533
2534 =head2 SetSMIMECertificate VALUE
2535
2536
2537 Set SMIMECertificate to VALUE. 
2538 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2539 (In the database, SMIMECertificate will be stored as a text.)
2540
2541
2542 =cut
2543
2544
2545 =head2 Creator
2546
2547 Returns the current value of Creator. 
2548 (In the database, Creator is stored as int(11).)
2549
2550
2551 =cut
2552
2553
2554 =head2 Created
2555
2556 Returns the current value of Created. 
2557 (In the database, Created is stored as datetime.)
2558
2559
2560 =cut
2561
2562
2563 =head2 LastUpdatedBy
2564
2565 Returns the current value of LastUpdatedBy. 
2566 (In the database, LastUpdatedBy is stored as int(11).)
2567
2568
2569 =cut
2570
2571
2572 =head2 LastUpdated
2573
2574 Returns the current value of LastUpdated. 
2575 (In the database, LastUpdated is stored as datetime.)
2576
2577
2578 =cut
2579
2580
2581
2582 sub _CoreAccessible {
2583     {
2584      
2585         id =>
2586         {read => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
2587         Name => 
2588         {read => 1, write => 1, sql_type => 12, length => 200,  is_blob => 0,  is_numeric => 0,  type => 'varchar(200)', default => ''},
2589         Password => 
2590         {read => 1, write => 1, sql_type => 12, length => 256,  is_blob => 0,  is_numeric => 0,  type => 'varchar(256)', default => ''},
2591         AuthToken => 
2592         {read => 1, write => 1, sql_type => 12, length => 16,  is_blob => 0,  is_numeric => 0,  type => 'varchar(16)', default => ''},
2593         Comments => 
2594         {read => 1, write => 1, sql_type => -4, length => 0,  is_blob => 1,  is_numeric => 0,  type => 'text', default => ''},
2595         Signature => 
2596         {read => 1, write => 1, sql_type => -4, length => 0,  is_blob => 1,  is_numeric => 0,  type => 'text', default => ''},
2597         EmailAddress => 
2598         {read => 1, write => 1, sql_type => 12, length => 120,  is_blob => 0,  is_numeric => 0,  type => 'varchar(120)', default => ''},
2599         FreeformContactInfo => 
2600         {read => 1, write => 1, sql_type => -4, length => 0,  is_blob => 1,  is_numeric => 0,  type => 'text', default => ''},
2601         Organization => 
2602         {read => 1, write => 1, sql_type => 12, length => 200,  is_blob => 0,  is_numeric => 0,  type => 'varchar(200)', default => ''},
2603         RealName => 
2604         {read => 1, write => 1, sql_type => 12, length => 120,  is_blob => 0,  is_numeric => 0,  type => 'varchar(120)', default => ''},
2605         NickName => 
2606         {read => 1, write => 1, sql_type => 12, length => 16,  is_blob => 0,  is_numeric => 0,  type => 'varchar(16)', default => ''},
2607         Lang => 
2608         {read => 1, write => 1, sql_type => 12, length => 16,  is_blob => 0,  is_numeric => 0,  type => 'varchar(16)', default => ''},
2609         EmailEncoding => 
2610         {read => 1, write => 1, sql_type => 12, length => 16,  is_blob => 0,  is_numeric => 0,  type => 'varchar(16)', default => ''},
2611         WebEncoding => 
2612         {read => 1, write => 1, sql_type => 12, length => 16,  is_blob => 0,  is_numeric => 0,  type => 'varchar(16)', default => ''},
2613         ExternalContactInfoId => 
2614         {read => 1, write => 1, sql_type => 12, length => 100,  is_blob => 0,  is_numeric => 0,  type => 'varchar(100)', default => ''},
2615         ContactInfoSystem => 
2616         {read => 1, write => 1, sql_type => 12, length => 30,  is_blob => 0,  is_numeric => 0,  type => 'varchar(30)', default => ''},
2617         ExternalAuthId => 
2618         {read => 1, write => 1, sql_type => 12, length => 100,  is_blob => 0,  is_numeric => 0,  type => 'varchar(100)', default => ''},
2619         AuthSystem => 
2620         {read => 1, write => 1, sql_type => 12, length => 30,  is_blob => 0,  is_numeric => 0,  type => 'varchar(30)', default => ''},
2621         Gecos => 
2622         {read => 1, write => 1, sql_type => 12, length => 16,  is_blob => 0,  is_numeric => 0,  type => 'varchar(16)', default => ''},
2623         HomePhone => 
2624         {read => 1, write => 1, sql_type => 12, length => 30,  is_blob => 0,  is_numeric => 0,  type => 'varchar(30)', default => ''},
2625         WorkPhone => 
2626         {read => 1, write => 1, sql_type => 12, length => 30,  is_blob => 0,  is_numeric => 0,  type => 'varchar(30)', default => ''},
2627         MobilePhone => 
2628         {read => 1, write => 1, sql_type => 12, length => 30,  is_blob => 0,  is_numeric => 0,  type => 'varchar(30)', default => ''},
2629         PagerPhone => 
2630         {read => 1, write => 1, sql_type => 12, length => 30,  is_blob => 0,  is_numeric => 0,  type => 'varchar(30)', default => ''},
2631         Address1 => 
2632         {read => 1, write => 1, sql_type => 12, length => 200,  is_blob => 0,  is_numeric => 0,  type => 'varchar(200)', default => ''},
2633         Address2 => 
2634         {read => 1, write => 1, sql_type => 12, length => 200,  is_blob => 0,  is_numeric => 0,  type => 'varchar(200)', default => ''},
2635         City => 
2636         {read => 1, write => 1, sql_type => 12, length => 100,  is_blob => 0,  is_numeric => 0,  type => 'varchar(100)', default => ''},
2637         State => 
2638         {read => 1, write => 1, sql_type => 12, length => 100,  is_blob => 0,  is_numeric => 0,  type => 'varchar(100)', default => ''},
2639         Zip => 
2640         {read => 1, write => 1, sql_type => 12, length => 16,  is_blob => 0,  is_numeric => 0,  type => 'varchar(16)', default => ''},
2641         Country => 
2642         {read => 1, write => 1, sql_type => 12, length => 50,  is_blob => 0,  is_numeric => 0,  type => 'varchar(50)', default => ''},
2643         Timezone => 
2644         {read => 1, write => 1, sql_type => 12, length => 50,  is_blob => 0,  is_numeric => 0,  type => 'varchar(50)', default => ''},
2645         PGPKey => 
2646         {read => 1, write => 1, sql_type => -4, length => 0,  is_blob => 1,  is_numeric => 0,  type => 'text', default => ''},
2647         SMIMECertificate =>
2648         {read => 1, write => 1, sql_type => -4, length => 0,  is_blob => 1,  is_numeric => 0,  type => 'text', default => ''},
2649         Creator => 
2650         {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
2651         Created => 
2652         {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
2653         LastUpdatedBy => 
2654         {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
2655         LastUpdated => 
2656         {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
2657
2658  }
2659 };
2660
2661 sub UID {
2662     my $self = shift;
2663     return undef unless defined $self->Name;
2664     return "@{[ref $self]}-@{[$self->Name]}";
2665 }
2666
2667 sub FindDependencies {
2668     my $self = shift;
2669     my ($walker, $deps) = @_;
2670
2671     $self->SUPER::FindDependencies($walker, $deps);
2672
2673     # ACL equivalence group
2674     my $objs = RT::Groups->new( $self->CurrentUser );
2675     $objs->Limit( FIELD => 'Domain', VALUE => 'ACLEquivalence', CASESENSITIVE => 0 );
2676     $objs->Limit( FIELD => 'Instance', VALUE => $self->Id );
2677     $deps->Add( in => $objs );
2678
2679     # Memberships in SystemInternal groups
2680     $objs = RT::GroupMembers->new( $self->CurrentUser );
2681     $objs->Limit( FIELD => 'MemberId', VALUE => $self->Id );
2682     my $principals = $objs->Join(
2683         ALIAS1 => 'main',
2684         FIELD1 => 'GroupId',
2685         TABLE2 => 'Principals',
2686         FIELD2 => 'id',
2687     );
2688     my $groups = $objs->Join(
2689         ALIAS1 => $principals,
2690         FIELD1 => 'ObjectId',
2691         TABLE2 => 'Groups',
2692         FIELD2 => 'Id',
2693     );
2694     $objs->Limit(
2695         ALIAS => $groups,
2696         FIELD => 'Domain',
2697         VALUE => 'SystemInternal',
2698         CASESENSITIVE => 0
2699     );
2700     $deps->Add( in => $objs );
2701
2702     # XXX: This ignores the myriad of "in" references from the Creator
2703     # and LastUpdatedBy columns.
2704 }
2705
2706 sub Serialize {
2707     my $self = shift;
2708     return (
2709         Disabled => $self->PrincipalObj->Disabled,
2710         Principal => $self->PrincipalObj->UID,
2711         PrincipalId => $self->PrincipalObj->Id,
2712         $self->SUPER::Serialize(@_),
2713     );
2714 }
2715
2716 sub PreInflate {
2717     my $class = shift;
2718     my ($importer, $uid, $data) = @_;
2719
2720     my $principal_uid = delete $data->{Principal};
2721     my $principal_id  = delete $data->{PrincipalId};
2722     my $disabled      = delete $data->{Disabled};
2723
2724     my $obj = RT::User->new( RT->SystemUser );
2725     $obj->LoadByCols( Name => $data->{Name} );
2726     $obj->LoadByEmail( $data->{EmailAddress} ) unless $obj->Id;
2727     if ($obj->Id) {
2728         # User already exists -- merge
2729
2730         # XXX: We might be merging a privileged user into an unpriv one,
2731         # in which case we should probably promote the unpriv user to
2732         # being privileged.  Of course, we don't know if the user being
2733         # imported is privileged yet, as its group memberships show up
2734         # later in the stream...
2735         $importer->MergeValues($obj, $data);
2736         $importer->SkipTransactions( $uid );
2737
2738         # Mark both the principal and the user object as resolved
2739         $importer->Resolve(
2740             $principal_uid,
2741             ref($obj->PrincipalObj),
2742             $obj->PrincipalObj->Id
2743         );
2744         $importer->Resolve( $uid => ref($obj) => $obj->Id );
2745         return;
2746     }
2747
2748     # Create a principal first, so we know what ID to use
2749     my $principal = RT::Principal->new( RT->SystemUser );
2750     my ($id) = $principal->Create(
2751         PrincipalType => 'User',
2752         Disabled => $disabled,
2753         ObjectId => 0,
2754     );
2755
2756     # Now we have a principal id, set the id for the user record
2757     $data->{id} = $id;
2758
2759     $importer->Resolve( $principal_uid => ref($principal), $id );
2760
2761     $importer->Postpone(
2762         for => $uid,
2763         uid => $principal_uid,
2764         column => "ObjectId",
2765     );
2766
2767     return $class->SUPER::PreInflate( $importer, $uid, $data );
2768 }
2769
2770 sub PostInflate {
2771     my $self = shift;
2772     RT->InitSystemObjects if $self->Name eq "RT_System";
2773 }
2774
2775 RT::Base->_ImportOverlays();
2776
2777
2778 1;