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