This commit was generated by cvs2svn to compensate for changes in r2526,
[freeside.git] / rt / lib / RT / User.pm
index cbc10f5..4e85540 100755 (executable)
-# BEGIN LICENSE BLOCK
-# 
-# Copyright (c) 1996-2003 Jesse Vincent <jesse@bestpractical.com>
-# 
-# (Except where explictly superceded by other copyright notices)
-# 
-# This work is made available to you under the terms of Version 2 of
-# the GNU General Public License. A copy of that license should have
-# been provided with this software, but in any event can be snarfed
-# from www.gnu.org.
-# 
-# This work is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-# General Public License for more details.
-# 
-# Unless otherwise specified, all modifications, corrections or
-# extensions to this work which alter its source code become the
-# property of Best Practical Solutions, LLC when submitted for
-# inclusion in the work.
-# 
-# 
-# END LICENSE BLOCK
-# Autogenerated by DBIx::SearchBuilder factory (by <jesse@bestpractical.com>)
-# WARNING: THIS FILE IS AUTOGENERATED. ALL CHANGES TO THIS FILE WILL BE LOST.  
-# 
-# !! DO NOT EDIT THIS FILE !!
-#
-
-use strict;
-
+# $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/User.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+# (c) 1996-2000 Jesse Vincent <jesse@fsck.com>
+# This software is redistributable under the terms of the GNU GPL
 
 =head1 NAME
 
-RT::User
-
+  RT::User - RT User object
 
 =head1 SYNOPSIS
 
-=head1 DESCRIPTION
-
-=head1 METHODS
-
-=cut
-
-package RT::User;
-use RT::Record; 
-
+  use RT::User;
 
-use vars qw( @ISA );
-@ISA= qw( RT::Record );
-
-sub _Init {
-  my $self = shift; 
+=head1 DESCRIPTION
 
-  $self->Table('Users');
-  $self->SUPER::_Init(@_);
-}
 
+=head1 METHODS
 
+=begin testing
 
+ok(require RT::TestHarness);
+ok(require RT::User);
 
+=end testing
 
-=item Create PARAMHASH
-
-Create takes a hash of values and creates a row in the database:
-
-  varchar(200) 'Name'.
-  varchar(40) 'Password'.
-  blob 'Comments'.
-  blob 'Signature'.
-  varchar(120) 'EmailAddress'.
-  blob 'FreeformContactInfo'.
-  varchar(200) 'Organization'.
-  varchar(120) 'RealName'.
-  varchar(16) 'NickName'.
-  varchar(16) 'Lang'.
-  varchar(16) 'EmailEncoding'.
-  varchar(16) 'WebEncoding'.
-  varchar(100) 'ExternalContactInfoId'.
-  varchar(30) 'ContactInfoSystem'.
-  varchar(100) 'ExternalAuthId'.
-  varchar(30) 'AuthSystem'.
-  varchar(16) 'Gecos'.
-  varchar(30) 'HomePhone'.
-  varchar(30) 'WorkPhone'.
-  varchar(30) 'MobilePhone'.
-  varchar(30) 'PagerPhone'.
-  varchar(200) 'Address1'.
-  varchar(200) 'Address2'.
-  varchar(100) 'City'.
-  varchar(100) 'State'.
-  varchar(16) 'Zip'.
-  varchar(50) 'Country'.
-  varchar(50) 'Timezone'.
-  text 'PGPKey'.
 
 =cut
 
 
+package RT::User;
+use RT::Record;
+@ISA= qw(RT::Record);
 
-
-sub Create {
+# {{{ sub _Init
+sub _Init  {
     my $self = shift;
-    my %args = ( 
-                Name => '',
-                Password => '',
-                Comments => '',
-                Signature => '',
-                EmailAddress => '',
-                FreeformContactInfo => '',
-                Organization => '',
-                RealName => '',
-                NickName => '',
-                Lang => '',
-                EmailEncoding => '',
-                WebEncoding => '',
-                ExternalContactInfoId => '',
-                ContactInfoSystem => '',
-                ExternalAuthId => '',
-                AuthSystem => '',
-                Gecos => '',
-                HomePhone => '',
-                WorkPhone => '',
-                MobilePhone => '',
-                PagerPhone => '',
-                Address1 => '',
-                Address2 => '',
-                City => '',
-                State => '',
-                Zip => '',
-                Country => '',
-                Timezone => '',
-                PGPKey => '',
-
-                 @_);
-    $self->SUPER::Create(
-                         Name => $args{'Name'},
-                         Password => $args{'Password'},
-                         Comments => $args{'Comments'},
-                         Signature => $args{'Signature'},
-                         EmailAddress => $args{'EmailAddress'},
-                         FreeformContactInfo => $args{'FreeformContactInfo'},
-                         Organization => $args{'Organization'},
-                         RealName => $args{'RealName'},
-                         NickName => $args{'NickName'},
-                         Lang => $args{'Lang'},
-                         EmailEncoding => $args{'EmailEncoding'},
-                         WebEncoding => $args{'WebEncoding'},
-                         ExternalContactInfoId => $args{'ExternalContactInfoId'},
-                         ContactInfoSystem => $args{'ContactInfoSystem'},
-                         ExternalAuthId => $args{'ExternalAuthId'},
-                         AuthSystem => $args{'AuthSystem'},
-                         Gecos => $args{'Gecos'},
-                         HomePhone => $args{'HomePhone'},
-                         WorkPhone => $args{'WorkPhone'},
-                         MobilePhone => $args{'MobilePhone'},
-                         PagerPhone => $args{'PagerPhone'},
-                         Address1 => $args{'Address1'},
-                         Address2 => $args{'Address2'},
-                         City => $args{'City'},
-                         State => $args{'State'},
-                         Zip => $args{'Zip'},
-                         Country => $args{'Country'},
-                         Timezone => $args{'Timezone'},
-                         PGPKey => $args{'PGPKey'},
-);
-
+    $self->{'table'} = "Users";
+    return($self->SUPER::_Init(@_));
+}
+# }}}
+
+# {{{ sub _Accessible 
+
+sub _Accessible  {
+  my $self = shift;
+  my %Cols = (
+             # {{{ Core RT info
+             Name => 'public/read/write/admin',
+             Password => 'write',
+             Comments => 'read/write/admin',
+             Signature => 'read/write',
+             EmailAddress => 'public/read/write',
+             PagerEmailAddress => 'read/write',
+             FreeformContactInfo => 'read/write',
+             Organization => 'public/read/write/admin',
+             Disabled => 'public/read/write/admin', #To modify this attribute, we have helper
+             #methods
+             Privileged => 'read/write/admin', # 0=no 1=user 2=system
+
+             # }}}
+             
+             # {{{ Names
+             
+             RealName => 'public/read/write',
+             NickName => 'public/read/write',
+             # }}}
+                     
+             # {{{ Localization and Internationalization
+             Lang => 'public/read/write',
+             EmailEncoding => 'public/read/write',
+             WebEncoding => 'public/read/write',
+             # }}}
+             
+             # {{{ External ContactInfo Linkage
+             ExternalContactInfoId => 'public/read/write/admin',
+             ContactInfoSystem => 'public/read/write/admin',
+             # }}}
+             
+             # {{{ User Authentication identifier
+             ExternalAuthId => 'public/read/write/admin',
+             #Authentication system used for user 
+             AuthSystem => 'public/read/write/admin',
+             Gecos => 'public/read/write/admin', #Gecos is the name of the fields in a 
+             # unix passwd file. In this case, it refers to "Unix Username"
+             # }}}
+             
+             # {{{ Telephone numbers
+             HomePhone =>  'read/write',
+             WorkPhone => 'read/write',
+             MobilePhone => 'read/write',
+             PagerPhone => 'read/write',
+
+             # }}}
+             
+             # {{{ Paper Address
+             Address1 => 'read/write',
+             Address2 => 'read/write',
+             City => 'read/write',
+             State => 'read/write',
+             Zip => 'read/write',
+             Country => 'read/write',
+             # }}}
+             
+             # {{{ Core DBIx::Record Attributes
+             Creator => 'read/auto',
+             Created => 'read/auto',
+             LastUpdatedBy => 'read/auto',
+             LastUpdated => 'read/auto'
+
+             # }}}
+            );
+  return($self->SUPER::_Accessible(@_, %Cols));
 }
 
+# }}}
 
+# {{{ sub Create 
 
-=item id
-
-Returns the current value of id. 
-(In the database, id is stored as int(11).)
-
-
-=cut
-
-
-=item Name
-
-Returns the current value of Name. 
-(In the database, Name is stored as varchar(200).)
-
-
-
-=item SetName VALUE
-
-
-Set Name to VALUE. 
-Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
-(In the database, Name will be stored as a varchar(200).)
-
-
-=cut
-
-
-=item Password
-
-Returns the current value of Password. 
-(In the database, Password is stored as varchar(40).)
-
-
-
-=item SetPassword VALUE
-
-
-Set Password to VALUE. 
-Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
-(In the database, Password will be stored as a varchar(40).)
-
-
-=cut
-
-
-=item Comments
-
-Returns the current value of Comments. 
-(In the database, Comments is stored as blob.)
-
-
-
-=item SetComments VALUE
-
-
-Set Comments to VALUE. 
-Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
-(In the database, Comments will be stored as a blob.)
-
-
-=cut
-
-
-=item Signature
-
-Returns the current value of Signature. 
-(In the database, Signature is stored as blob.)
-
-
-
-=item SetSignature VALUE
-
-
-Set Signature to VALUE. 
-Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
-(In the database, Signature will be stored as a blob.)
-
-
-=cut
-
-
-=item EmailAddress
-
-Returns the current value of EmailAddress. 
-(In the database, EmailAddress is stored as varchar(120).)
-
-
-
-=item SetEmailAddress VALUE
-
-
-Set EmailAddress to VALUE. 
-Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
-(In the database, EmailAddress will be stored as a varchar(120).)
-
-
-=cut
-
-
-=item FreeformContactInfo
-
-Returns the current value of FreeformContactInfo. 
-(In the database, FreeformContactInfo is stored as blob.)
-
-
-
-=item SetFreeformContactInfo VALUE
-
-
-Set FreeformContactInfo to VALUE. 
-Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
-(In the database, FreeformContactInfo will be stored as a blob.)
-
-
-=cut
-
-
-=item Organization
-
-Returns the current value of Organization. 
-(In the database, Organization is stored as varchar(200).)
-
-
-
-=item SetOrganization VALUE
-
-
-Set Organization to VALUE. 
-Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
-(In the database, Organization will be stored as a varchar(200).)
-
-
-=cut
-
-
-=item RealName
-
-Returns the current value of RealName. 
-(In the database, RealName is stored as varchar(120).)
-
-
-
-=item SetRealName VALUE
-
-
-Set RealName to VALUE. 
-Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
-(In the database, RealName will be stored as a varchar(120).)
-
-
-=cut
-
-
-=item NickName
-
-Returns the current value of NickName. 
-(In the database, NickName is stored as varchar(16).)
-
-
-
-=item SetNickName VALUE
-
-
-Set NickName to VALUE. 
-Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
-(In the database, NickName will be stored as a varchar(16).)
-
-
-=cut
-
-
-=item Lang
-
-Returns the current value of Lang. 
-(In the database, Lang is stored as varchar(16).)
-
-
-
-=item SetLang VALUE
-
-
-Set Lang to VALUE. 
-Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
-(In the database, Lang will be stored as a varchar(16).)
-
-
-=cut
-
-
-=item EmailEncoding
-
-Returns the current value of EmailEncoding. 
-(In the database, EmailEncoding is stored as varchar(16).)
-
-
+sub Create  {
+    my $self = shift;
+    my %args = (Privileged => 0,
+               @_ # get the real argumentlist
+              );
+    
+    #Check the ACL
+    unless ($self->CurrentUserHasRight('AdminUsers')) {
+       return (0, 'No permission to create users');
+    }
+    
+    if (! $args{'Password'})  {
+       $args{'Password'} = '*NO-PASSWORD*';
+    }
+    elsif (length($args{'Password'}) < $RT::MinimumPasswordLength) {
+        return(0,"Password too short");
+    }
+    else {
+        my $salt = join '', ('.','/',0..9,'A'..'Z','a'..'z')[rand 64, rand 64];
+        $args{'Password'} = crypt($args{'Password'}, $salt);     
+    }   
+        
+    
+    #TODO Specify some sensible defaults.
+    
+    unless (defined ($args{'Name'})) {
+       return(0, "Must specify 'Name' attribute");
+    }  
+    
+    
+    #SANITY CHECK THE NAME AND ABORT IF IT'S TAKEN
+    if ($RT::SystemUser) { #This only works if RT::SystemUser has been defined
+               my $TempUser = RT::User->new($RT::SystemUser);
+               $TempUser->Load($args{'Name'});
+               return (0, 'Name in use') if ($TempUser->Id);
+       
+               return(0, 'Email address in use') 
+                       unless ($self->ValidateEmailAddress($args{'EmailAddress'}));
+    }
+    else {
+               $RT::Logger->warning("$self couldn't check for pre-existing ".
+                            " users on create. This will happen".
+                            " on installation\n");
+    }
+    
+    my $id = $self->SUPER::Create(%args);
+    
+    #If the create failed.
+    unless ($id) {
+               return (0, 'Could not create user');
+    }
+      
+    
+    #TODO post 2.0
+    #if ($args{'SendWelcomeMessage'}) {
+    #  #TODO: Check if the email exists and looks valid
+    #  #TODO: Send the user a "welcome message" 
+    #}
+    
+    return ($id, 'User created');
+}
 
-=item SetEmailEncoding VALUE
+# }}}
 
+# {{{ sub _BootstrapCreate 
 
-Set EmailEncoding to VALUE. 
-Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
-(In the database, EmailEncoding will be stored as a varchar(16).)
+#create a user without validating _any_ data.
 
+#To be used only on database init.
 
-=cut
+sub _BootstrapCreate {
+    my $self = shift;
+    my %args = (@_);
 
+    $args{'Password'} = "*NO-PASSWORD*";
+    my $id = $self->SUPER::Create(%args);
+    
+    #If the create failed.
+    return (0, 'Could not create user') 
+      unless ($id);
 
-=item WebEncoding
+    return ($id, 'User created');
+}
 
-Returns the current value of WebEncoding. 
-(In the database, WebEncoding is stored as varchar(16).)
+# }}}
 
+# {{{ sub Delete 
 
+sub Delete  {
+    my $self = shift;
+    
+    return(0, 'Deleting this object would violate referential integrity');
+    
+}
 
-=item SetWebEncoding VALUE
+# }}}
 
+# {{{ sub Load 
 
-Set WebEncoding to VALUE. 
-Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
-(In the database, WebEncoding will be stored as a varchar(16).)
+=head2 Load
 
+Load a user object from the database. Takes a single argument.
+If the argument is numerical, load by the column 'id'. Otherwise, load by
+the "Name" column which is the user's textual username.
 
 =cut
 
+sub Load  {
+    my $self = shift;
+    my $identifier = shift || return undef;
+    
+    #if it's an int, load by id. otherwise, load by name.
+    if ($identifier !~ /\D/) {
+       $self->SUPER::LoadById($identifier);
+    }
+    else {
+       $self->LoadByCol("Name",$identifier);
+    }
+}
 
-=item ExternalContactInfoId
-
-Returns the current value of ExternalContactInfoId. 
-(In the database, ExternalContactInfoId is stored as varchar(100).)
-
+# }}}
 
 
-=item SetExternalContactInfoId VALUE
+# {{{ sub LoadByEmail
 
+=head2 LoadByEmail
 
-Set ExternalContactInfoId to VALUE. 
-Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
-(In the database, ExternalContactInfoId will be stored as a varchar(100).)
+Tries to load this user object from the database by the user's email address.
 
 
 =cut
 
+sub LoadByEmail {
+    my $self=shift;
+    my $address = shift;
 
-=item ContactInfoSystem
-
-Returns the current value of ContactInfoSystem. 
-(In the database, ContactInfoSystem is stored as varchar(30).)
-
+    # Never load an empty address as an email address.
+    unless ($address) {
+       return(undef);
+    }
 
+    $address = RT::CanonicalizeAddress($address);
+    #$RT::Logger->debug("Trying to load an email address: $address\n");
+    return $self->LoadByCol("EmailAddress", $address);
+}
+# }}}
 
-=item SetContactInfoSystem VALUE
 
+# {{{ sub ValidateEmailAddress
 
-Set ContactInfoSystem to VALUE. 
-Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
-(In the database, ContactInfoSystem will be stored as a varchar(30).)
+=head2 ValidateEmailAddress ADDRESS
 
+Returns true if the email address entered is not in use by another user or is 
+undef or ''. Returns false if it's in use. 
 
 =cut
 
+sub ValidateEmailAddress {
+       my $self = shift;
+       my $Value = shift;
 
-=item ExternalAuthId
-
-Returns the current value of ExternalAuthId. 
-(In the database, ExternalAuthId is stored as varchar(100).)
-
+       # if the email address is null, it's always valid
+       return (1) if(!$Value || $Value eq "");
 
+       my $TempUser = RT::User->new($RT::SystemUser);
+       $TempUser->LoadByEmail($Value);
 
-=item SetExternalAuthId VALUE
-
-
-Set ExternalAuthId to VALUE. 
-Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
-(In the database, ExternalAuthId will be stored as a varchar(100).)
-
-
-=cut
-
-
-=item AuthSystem
+       if( $TempUser->id && 
+          ($TempUser->id != $self->id)) { # if we found a user with that address 
+                                       # it's invalid to set this user's address to it
+               return(undef);
+       }
+       else { #it's a valid email address
+               return(1);
+       }
+}
 
-Returns the current value of AuthSystem. 
-(In the database, AuthSystem is stored as varchar(30).)
+# }}}
 
 
 
-=item SetAuthSystem VALUE
 
+# {{{ sub SetRandomPassword
 
-Set AuthSystem to VALUE. 
-Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
-(In the database, AuthSystem will be stored as a varchar(30).)
+=head2 SetRandomPassword
 
+Takes no arguments. Returns a status code and a new password or an error message.
+If the status is 1, the second value returned is the new password.
+If the status is anything else, the new value returned is the error code.
 
 =cut
 
-
-=item Gecos
-
-Returns the current value of Gecos. 
-(In the database, Gecos is stored as varchar(16).)
-
-
-
-=item SetGecos VALUE
-
-
-Set Gecos to VALUE. 
-Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
-(In the database, Gecos will be stored as a varchar(16).)
-
-
-=cut
+sub SetRandomPassword  {
+    my $self = shift;
 
 
-=item HomePhone
+    unless ($self->CurrentUserCanModify('Password')) {
+       return (0, "Permission Denied");
+    }
+    
+    my $pass = $self->GenerateRandomPassword(6,8);
 
-Returns the current value of HomePhone. 
-(In the database, HomePhone is stored as varchar(30).)
+    # If we have "notify user on 
 
+    my ($val, $msg) = $self->SetPassword($pass);
+    
+    #If we got an error return the error.
+    return (0, $msg) unless ($val);
+    
+    #Otherwise, we changed the password, lets return it.
+    return (1, $pass);
+    
+}
 
+# }}}
 
-=item SetHomePhone VALUE
 
+# {{{ sub ResetPassword
 
-Set HomePhone to VALUE. 
-Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
-(In the database, HomePhone will be stored as a varchar(30).)
+=head2 ResetPassword
 
+Returns status, [ERROR or new password].  Resets this user\'s password to
+a randomly generated pronouncable password and emails them, using a 
+global template called "RT_PasswordChange", which can be overridden
+with global templates "RT_PasswordChange_Privileged" or "RT_PasswordChange_NonPrivileged" 
+for privileged and Non-privileged users respectively.
 
 =cut
 
-
-=item WorkPhone
-
-Returns the current value of WorkPhone. 
-(In the database, WorkPhone is stored as varchar(30).)
-
+sub ResetPassword {
+    my $self = shift;
+    
+    unless ($self->CurrentUserCanModify('Password')) {
+       return (0, "Permission Denied");
+    }
+    my ($status, $pass) = $self->SetRandomPassword();
+
+    unless ($status) {
+       return (0, "$pass");
+    }
+    
+    my $template = RT::Template->new($self->CurrentUser);
+
+
+    if ($self->IsPrivileged) {
+       $template->LoadGlobalTemplate('RT_PasswordChange_Privileged');
+    } 
+    else {
+       $template->LoadGlobalTemplate('RT_PasswordChange_Privileged');
+    }  
+    
+    unless ($template->Id) {
+       $template->LoadGlobalTemplate('RT_PasswordChange');
+    }  
+    
+    unless ($template->Id) {
+       $RT::Logger->crit("$self tried to send ".$self->Name." a password reminder ".
+                         "but couldn't find a password change template");
+    }  
+
+    my $notification =  RT::Action::SendPasswordEmail->new(TemplateObj => $template,
+                                                          Argument => $pass);
+    
+    $notification->SetTo($self->EmailAddress);
+
+    my ($ret);
+    $ret = $notification->Prepare();
+    if ($ret) {
+       $ret = $notification->Commit();
+    }
+    
+    if ($ret) {
+       return(1, 'New password notification sent');
+    }  else {
+       return (0, 'Notification could not be sent');
+    }  
+    
+}
 
 
-=item SetWorkPhone VALUE
+# }}}
 
+# {{{ sub GenerateRandomPassword
 
-Set WorkPhone to VALUE. 
-Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
-(In the database, WorkPhone will be stored as a varchar(30).)
+=head2 GenerateRandomPassword MIN_LEN and MAX_LEN
 
+Returns a random password between MIN_LEN and MAX_LEN characters long.
 
 =cut
 
-
-=item MobilePhone
-
-Returns the current value of MobilePhone. 
-(In the database, MobilePhone is stored as varchar(30).)
+sub GenerateRandomPassword {
+    my $self = shift;
+    my $min_length = shift;
+    my $max_length = shift;
+    
+    #This code derived from mpw.pl, a bit of code with a sordid history
+    # Its notes: 
+    
+    # Perl cleaned up a bit by Jesse Vincent 1/14/2001.
+    # Converted to perl from C by Marc Horowitz, 1/20/2000.
+    # Converted to C from Multics PL/I by Bill Sommerfeld, 4/21/86.
+    # Original PL/I version provided by Jerry Saltzer.
+
+    
+    my ($frequency, $start_freq, $total_sum, $row_sums);
+
+    #When munging characters, we need to know where to start counting letters from
+    my $a = ord('a');
+
+    # frequency of English digraphs (from D Edwards 1/27/66) 
+    $frequency =
+      [ [ 4, 20, 28, 52, 2, 11, 28, 4, 32, 4, 6, 62, 23,
+         167, 2, 14, 0, 83, 76, 127, 7, 25, 8, 1, 9, 1 ], # aa - az
+       [ 13, 0, 0, 0, 55, 0, 0, 0, 8, 2, 0, 22, 0,
+         0, 11, 0, 0, 15, 4, 2, 13, 0, 0, 0, 15, 0 ], # ba - bz
+       [ 32, 0, 7, 1, 69, 0, 0, 33, 17, 0, 10, 9, 1,
+         0, 50, 3, 0, 10, 0, 28, 11, 0, 0, 0, 3, 0 ], # ca - cz
+       [ 40, 16, 9, 5, 65, 18, 3, 9, 56, 0, 1, 4, 15,
+         6, 16, 4, 0, 21, 18, 53, 19, 5, 15, 0, 3, 0 ], # da - dz
+       [ 84, 20, 55, 125, 51, 40, 19, 16, 50, 1, 4, 55, 54,
+         146, 35, 37, 6, 191, 149, 65, 9, 26, 21, 12, 5, 0 ], # ea - ez
+       [ 19, 3, 5, 1, 19, 21, 1, 3, 30, 2, 0, 11, 1,
+         0, 51, 0, 0, 26, 8, 47, 6, 3, 3, 0, 2, 0 ], # fa - fz
+       [ 20, 4, 3, 2, 35, 1, 3, 15, 18, 0, 0, 5, 1,
+         4, 21, 1, 1, 20, 9, 21, 9, 0, 5, 0, 1, 0 ], # ga - gz
+       [ 101, 1, 3, 0, 270, 5, 1, 6, 57, 0, 0, 0, 3,
+         2, 44, 1, 0, 3, 10, 18, 6, 0, 5, 0, 3, 0 ], # ha - hz
+       [ 40, 7, 51, 23, 25, 9, 11, 3, 0, 0, 2, 38, 25,
+         202, 56, 12, 1, 46, 79, 117, 1, 22, 0, 4, 0, 3 ], # ia - iz
+       [ 3, 0, 0, 0, 5, 0, 0, 0, 1, 0, 0, 0, 0,
+         0, 4, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0 ], # ja - jz
+       [ 1, 0, 0, 0, 11, 0, 0, 0, 13, 0, 0, 0, 0,
+         2, 0, 0, 0, 0, 6, 2, 1, 0, 2, 0, 1, 0 ], # ka - kz
+       [ 44, 2, 5, 12, 62, 7, 5, 2, 42, 1, 1, 53, 2,
+         2, 25, 1, 1, 2, 16, 23, 9, 0, 1, 0, 33, 0 ], # la - lz
+       [ 52, 14, 1, 0, 64, 0, 0, 3, 37, 0, 0, 0, 7,
+         1, 17, 18, 1, 2, 12, 3, 8, 0, 1, 0, 2, 0 ], # ma - mz
+       [ 42, 10, 47, 122, 63, 19, 106, 12, 30, 1, 6, 6, 9,
+         7, 54, 7, 1, 7, 44, 124, 6, 1, 15, 0, 12, 0 ], # na - nz
+       [ 7, 12, 14, 17, 5, 95, 3, 5, 14, 0, 0, 19, 41,
+         134, 13, 23, 0, 91, 23, 42, 55, 16, 28, 0, 4, 1 ], # oa - oz
+       [ 19, 1, 0, 0, 37, 0, 0, 4, 8, 0, 0, 15, 1,
+         0, 27, 9, 0, 33, 14, 7, 6, 0, 0, 0, 0, 0 ], # pa - pz
+       [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+         0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 0, 0 ], # qa - qz
+       [ 83, 8, 16, 23, 169, 4, 8, 8, 77, 1, 10, 5, 26,
+         16, 60, 4, 0, 24, 37, 55, 6, 11, 4, 0, 28, 0 ], # ra - rz
+       [ 65, 9, 17, 9, 73, 13, 1, 47, 75, 3, 0, 7, 11,
+         12, 56, 17, 6, 9, 48, 116, 35, 1, 28, 0, 4, 0 ], # sa - sz
+       [ 57, 22, 3, 1, 76, 5, 2, 330, 126, 1, 0, 14, 10,
+         6, 79, 7, 0, 49, 50, 56, 21, 2, 27, 0, 24, 0 ], # ta - tz
+       [ 11, 5, 9, 6, 9, 1, 6, 0, 9, 0, 1, 19, 5,
+         31, 1, 15, 0, 47, 39, 31, 0, 3, 0, 0, 0, 0 ], # ua - uz
+       [ 7, 0, 0, 0, 72, 0, 0, 0, 28, 0, 0, 0, 0,
+         0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0 ], # va - vz
+       [ 36, 1, 1, 0, 38, 0, 0, 33, 36, 0, 0, 4, 1,
+         8, 15, 0, 0, 0, 4, 2, 0, 0, 1, 0, 0, 0 ], # wa - wz
+       [ 1, 0, 2, 0, 0, 1, 0, 0, 3, 0, 0, 0, 0,
+         0, 1, 5, 0, 0, 0, 3, 0, 0, 1, 0, 0, 0 ], # xa - xz
+       [ 14, 5, 4, 2, 7, 12, 12, 6, 10, 0, 0, 3, 7,
+         5, 17, 3, 0, 4, 16, 30, 0, 0, 5, 0, 0, 0 ], # ya - yz
+       [ 1, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0,
+         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] ]; # za - zz
+
+    #We need to know the totals for each row 
+    $row_sums =
+      [ map { my $sum = 0; map { $sum += $_ } @$_; $sum } @$frequency ];
+    
+
+    #Frequency with which a given letter starts a word.
+    $start_freq =
+      [ 1299, 425, 725, 271, 375, 470, 93, 223, 1009, 24, 20, 355, 379,
+       319, 823, 618, 21, 317, 962, 1991, 271, 104, 516, 6, 16, 14 ];
+    
+    $total_sum = 0; map { $total_sum += $_ } @$start_freq;
+    
+    
+    my $length = $min_length + int(rand($max_length-$min_length));
+    
+    my $char = $self->GenerateRandomNextChar($total_sum, $start_freq);
+    my @word = ($char+$a);
+    for (2..$length) {
+       $char = $self->_GenerateRandomNextChar($row_sums->[$char], $frequency->[$char]);
+       push(@word, $char+$a);
+    }
+    
+    #Return the password
+    return pack("C*",@word);
+    
+}
 
 
+#A private helper function for RandomPassword
+# Takes a row summary and a frequency chart for the next character to be searched
+sub _GenerateRandomNextChar {
+    my $self = shift;
+    my($all, $freq) = @_;
+    my($pos, $i);
+    
+    for ($pos = int(rand($all)), $i=0;
+        $pos >= $freq->[$i];
+        $pos -= $freq->[$i], $i++) {};
+    
+    return($i);
+}
 
-=item SetMobilePhone VALUE
+# }}}
 
+# {{{ sub SetPassword
 
-Set MobilePhone to VALUE. 
-Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
-(In the database, MobilePhone will be stored as a varchar(30).)
+=head2 SetPassword
 
+Takes a string. Checks the string's length and sets this user's password 
+to that string.
 
 =cut
 
+sub SetPassword {
+    my $self = shift;
+    my $password = shift;
+    
+    unless ($self->CurrentUserCanModify('Password')) {
+       return(0, 'Permission Denied');
+    }
+    
+    if (! $password)  {
+        return(0, "No password set");
+    }
+    elsif (length($password) < $RT::MinimumPasswordLength) {
+        return(0,"Password too short");
+    }
+    else {
+        my $salt = join '', ('.','/',0..9,'A'..'Z','a'..'z')[rand 64, rand 64];
+        return ( $self->SUPER::SetPassword(crypt($password, $salt)) );
+    }   
+    
+}
 
-=item PagerPhone
-
-Returns the current value of PagerPhone. 
-(In the database, PagerPhone is stored as varchar(30).)
-
-
-
-=item SetPagerPhone VALUE
+# }}}
 
+# {{{ sub IsPassword 
 
-Set PagerPhone to VALUE. 
-Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
-(In the database, PagerPhone will be stored as a varchar(30).)
+=head2 IsPassword
 
+Returns true if the passed in value is this user's password.
+Returns undef otherwise.
 
 =cut
 
+sub IsPassword { 
+    my $self = shift;
+    my $value = shift;
+
+    #TODO there isn't any apparent way to legitimately ACL this
+
+    # RT does not allow null passwords 
+    if ((!defined ($value)) or ($value eq '')) {
+       return(undef);
+    } 
+    if ($self->Disabled) {
+       $RT::Logger->info("Disabled user ".$self->Name." tried to log in");
+       return(undef);
+    }
+
+    if ( ($self->__Value('Password') eq '') || 
+         ($self->__Value('Password') eq undef) )  {
+        return(undef);
+     }
+    if ($self->__Value('Password') eq crypt($value, $self->__Value('Password'))) {
+       return (1);
+    }
+    else {
+       return (undef);
+    }
+}
 
-=item Address1
-
-Returns the current value of Address1. 
-(In the database, Address1 is stored as varchar(200).)
-
-
-
-=item SetAddress1 VALUE
-
-
-Set Address1 to VALUE. 
-Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
-(In the database, Address1 will be stored as a varchar(200).)
-
-
-=cut
+# }}}
 
+# {{{ sub SetDisabled
 
-=item Address2
+=head2 Sub SetDisabled
 
-Returns the current value of Address2. 
-(In the database, Address2 is stored as varchar(200).)
+Toggles the user's disabled flag.
+If this flag is
+set, all password checks for this user will fail. All ACL checks for this
+user will fail. The user will appear in no user listings.
 
+=cut 
 
+# }}}
 
-=item SetAddress2 VALUE
+# {{{ ACL Related routines
 
+# {{{ GrantQueueRight
 
-Set Address2 to VALUE. 
-Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
-(In the database, Address2 will be stored as a varchar(200).)
+=head2 GrantQueueRight
 
+Grant a queue right to this user.  Takes a paramhash of which the elements
+RightAppliesTo and RightName are important.
 
 =cut
 
+sub GrantQueueRight {
+    
+    my $self = shift;
+    my %args = ( RightScope => 'Queue',
+                RightName => undef,
+                RightAppliesTo => undef,
+                PrincipalType => 'User',
+                PrincipalId => $self->Id,
+                @_);
+   
+    #ACL check handled in ACE.pm
+
+    require RT::ACE;
+
+#    $RT::Logger->debug("$self ->GrantQueueRight right:". $args{'RightName'} .
+#                     " applies to queue ".$args{'RightAppliesTo'}."\n");
+    
+    my $ace = new RT::ACE($self->CurrentUser);
+    
+    return ($ace->Create(%args));
+}
 
-=item City
-
-Returns the current value of City. 
-(In the database, City is stored as varchar(100).)
-
-
-
-=item SetCity VALUE
+# }}}
 
+# {{{ GrantSystemRight
 
-Set City to VALUE. 
-Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
-(In the database, City will be stored as a varchar(100).)
+=head2 GrantSystemRight
 
+Grant a system right to this user. 
+The only element that's important to set is RightName.
 
 =cut
+sub GrantSystemRight {
+    
+    my $self = shift;
+    my %args = ( RightScope => 'System',
+                RightName => undef,
+                RightAppliesTo => 0,
+                PrincipalType => 'User',
+                PrincipalId => $self->Id,
+                @_);
+   
+
+    #ACL check handled in ACE.pm
+
+    require RT::ACE;    
+    my $ace = new RT::ACE($self->CurrentUser);
+    
+    return ($ace->Create(%args));
+}
 
 
-=item State
-
-Returns the current value of State. 
-(In the database, State is stored as varchar(100).)
+# }}}
 
+# {{{ sub HasQueueRight
 
+=head2 HasQueueRight
 
-=item SetState VALUE
+Takes a paramhash which can contain
+these items:
+    TicketObj => RT::Ticket or QueueObj => RT::Queue or Queue => integer
+    IsRequestor => undef, (for bootstrapping create)
+    Right => 'Right' 
 
 
-Set State to VALUE. 
-Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
-(In the database, State will be stored as a varchar(100).)
+Returns 1 if this user has the right specified in the paramhash. for the queue
+passed in.
 
+Returns undef if they don't
 
 =cut
 
+sub HasQueueRight {
+    my $self = shift;
+    my %args = ( TicketObj => undef,
+                 QueueObj => undef,
+                Queue => undef,
+                IsRequestor => undef,
+                Right => undef,
+                @_);
+    
+    my ($IsRequestor, $IsCc, $IsAdminCc, $IsOwner);
+    
+    if (defined $args{'Queue'}) {
+       $args{'QueueObj'} = new RT::Queue($self->CurrentUser);
+       $args{'QueueObj'}->Load($args{'Queue'});
+    }
+    
+    if (defined $args{'TicketObj'}) {
+       $args{'QueueObj'} = $args{'TicketObj'}->QueueObj();
+    }
+
+    # {{{ Validate and load up the QueueId
+    unless ((defined $args{'QueueObj'}) and ($args{'QueueObj'}->Id)) {
+       require Carp;
+       $RT::Logger->debug(Carp::cluck ("$self->HasQueueRight Couldn't find a queue id"));
+       return undef;
+    }
+
+    # }}}
+
+        
+    # Figure out whether a user has the right we're asking about.
+    # first see if they have the right personally for the queue in question. 
+    my $retval = $self->_HasRight(Scope => 'Queue',
+                                 AppliesTo => $args{'QueueObj'}->Id,
+                                 Right => $args{'Right'},
+                                 IsOwner => $IsOwner);
+
+    return ($retval) if (defined $retval);
+    
+    # then we see whether they have the right personally globally. 
+    $retval = $self->HasSystemRight( $args{'Right'});
+
+    return ($retval) if (defined $retval);
+    
+    # now that we know they don't have the right personally,
+    
+    # {{{ Find out about whether the current user is a Requestor, Cc, AdminCc or Owner
+
+    if (defined $args{'TicketObj'}) {
+       if ($args{'TicketObj'}->IsRequestor($self)) {#user is requestor
+           $IsRequestor = 1;
+       }       
+
+       if ($args{'TicketObj'}->IsCc($self)) { #If user is a cc
+           $IsCc = 1;
+       }
+
+       if ($args{'TicketObj'}->IsAdminCc($self)) { #If user is an admin cc
+           $IsAdminCc = 1;
+       }       
+       
+       if ($args{'TicketObj'}->IsOwner($self)) { #If user is an owner
+           $IsOwner = 1;
+       }
+    }
+    
+    if (defined $args{'QueueObj'}) {
+       if ($args{'QueueObj'}->IsCc($self)) { #If user is a cc
+           $IsCc = 1;
+       }
+       if ($args{'QueueObj'}->IsAdminCc($self)) { #If user is an admin cc
+           $IsAdminCc = 1;
+       }
+       
+    } 
+    # }}}
+    
+    # then see whether they have the right for the queue as a member of a metagroup 
+
+    $retval = $self->_HasRight(Scope => 'Queue',
+                                 AppliesTo => $args{'QueueObj'}->Id,
+                                 Right => $args{'Right'},
+                                 IsOwner => $IsOwner,
+                                 IsCc => $IsCc,
+                                 IsAdminCc => $IsAdminCc,
+                                 IsRequestor => $IsRequestor
+                                );
+
+    return ($retval) if (defined $retval);
+
+    #   then we see whether they have the right globally as a member of a metagroup
+    $retval = $self->HasSystemRight( $args{'Right'},
+                                    (IsOwner => $IsOwner,
+                                     IsCc => $IsCc,
+                                     IsAdminCc => $IsAdminCc,
+                                     IsRequestor => $IsRequestor
+                                    ) );
+
+    #If they haven't gotten it by now, they just lose.
+    return ($retval);
+    
+}
 
-=item Zip
-
-Returns the current value of Zip. 
-(In the database, Zip is stored as varchar(16).)
-
-
-
-=item SetZip VALUE
+# }}}
+  
+# {{{ sub HasSystemRight
 
+=head2 HasSystemRight
 
-Set Zip to VALUE. 
-Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
-(In the database, Zip will be stored as a varchar(16).)
+takes an array of a single value and a paramhash.
+The single argument is the right being passed in.
+the param hash is some additional data. (IsCc, IsOwner, IsAdminCc and IsRequestor)
 
+Returns 1 if this user has the listed 'right'. Returns undef if this user doesn't.
 
 =cut
 
+sub HasSystemRight {
+    my $self = shift;
+    my $right = shift;
+
+    my %args = ( IsOwner => undef,
+                IsCc => undef,
+                IsAdminCc => undef,
+                IsRequestor => undef,
+                @_);
+    
+    unless (defined $right) {
+
+       $RT::Logger->debug("$self RT::User::HasSystemRight was passed in no right.");
+       return(undef);
+    }  
+    return ( $self->_HasRight ( Scope => 'System',
+                               AppliesTo => '0',
+                               Right => $right,
+                               IsOwner => $args{'IsOwner'},
+                               IsCc => $args{'IsCc'},
+                               IsAdminCc => $args{'IsAdminCc'},
+                               IsRequestor => $args{'IsRequestor'},
+                               
+                             )
+          );
+    
+}
 
-=item Country
-
-Returns the current value of Country. 
-(In the database, Country is stored as varchar(50).)
-
-
-
-=item SetCountry VALUE
-
-
-Set Country to VALUE. 
-Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
-(In the database, Country will be stored as a varchar(50).)
-
+# }}}
 
-=cut
+# {{{ sub _HasRight
 
+=head2 sub _HasRight (Right => 'right', Scope => 'scope',  AppliesTo => int, ExtendedPrincipals => SQL)
 
-=item Timezone
+_HasRight is a private helper method for checking a user's rights. It takes
+several options:
 
-Returns the current value of Timezone. 
-(In the database, Timezone is stored as varchar(50).)
+=item Right is a textual right name
 
+=item Scope is a textual scope name. (As of July these were Queue, Ticket and System
 
+=item AppliesTo is the numerical Id of the object identified in the scope. For tickets, this is the queue #. for queues, this is the queue #
 
-=item SetTimezone VALUE
+=item ExtendedPrincipals is an  SQL select clause which assumes that the only
+table in play is ACL.  It's used by HasQueueRight to pass in which 
+metaprincipals apply. Actually, it's probably obsolete. TODO: remove it.
 
+Returns 1 if a matching ACE was found.
 
-Set Timezone to VALUE. 
-Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
-(In the database, Timezone will be stored as a varchar(50).)
-
+Returns undef if no ACE was found.
 
 =cut
 
 
-=item PGPKey
-
-Returns the current value of PGPKey. 
-(In the database, PGPKey is stored as text.)
-
-
+sub _HasRight {
+    
+    my $self = shift;
+    my %args = ( Right => undef,
+                Scope => undef,
+                AppliesTo => undef,
+                IsRequestor => undef,
+                IsCc => undef,
+                IsAdminCc => undef,
+                IsOwner => undef,
+                ExtendedPrincipals => undef,
+                @_);
+    
+    if ($self->Disabled) {
+       $RT::Logger->debug ("Disabled User:  ".$self->Name.
+                           " failed access check for ".$args{'Right'}.
+                           " to object ".$args{'Scope'}."/".
+                           $args{'AppliesTo'}."\n");
+       return (undef);
+    }
+    
+    if (!defined $args{'Right'}) {
+       $RT::Logger->debug("_HasRight called without a right\n");
+       return(undef);
+    }
+    elsif (!defined $args{'Scope'}) {
+       $RT::Logger->debug("_HasRight called without a scope\n");
+       return(undef);
+    }
+    elsif (!defined $args{'AppliesTo'}) {
+       $RT::Logger->debug("_HasRight called without an AppliesTo object\n");
+       return(undef);
+    }
+    
+    #If we've cached a win or loss for this lookup say so
+    
+    #TODO Security +++ check to make sure this is complete and right
+    
+    #Construct a hashkey to cache decisions in
+    my ($hashkey);
+    { #it's ugly, but we need to turn off warning, cuz we're joining nulls.
+       local $^W=0;
+       $hashkey =$self->Id .":". join(':',%args);
+    }  
+    
+  # $RT::Logger->debug($hashkey."\n");
+    
+    #Anything older than 10 seconds needs to be rechecked
+    my $cache_timeout = (time - 10);
+    
+    
+    if ((defined $self->{'rights'}{"$hashkey"}) &&
+           ($self->{'rights'}{"$hashkey"} == 1 ) &&
+        (defined $self->{'rights'}{"$hashkey"}{'set'} ) &&
+           ($self->{'rights'}{"$hashkey"}{'set'} > $cache_timeout)) {
+#        $RT::Logger->debug("Cached ACL win for ". 
+#                           $args{'Right'}.$args{'Scope'}.
+#                           $args{'AppliesTo'}."\n");      
+       return ($self->{'rights'}{"$hashkey"});
+    }
+    elsif ((defined $self->{'rights'}{"$hashkey"}) &&
+              ($self->{'rights'}{"$hashkey"} == -1)  &&
+           (defined $self->{'rights'}{"$hashkey"}{'set'}) &&
+              ($self->{'rights'}{"$hashkey"}{'set'} > $cache_timeout)) {
+       
+#      $RT::Logger->debug("Cached ACL loss decision for ". 
+#                         $args{'Right'}.$args{'Scope'}.
+#                         $args{'AppliesTo'}."\n");        
+       
+       return(undef);
+    }
+    
+    
+    my $RightClause = "(RightName = '$args{'Right'}')";
+    my $ScopeClause = "(RightScope = '$args{'Scope'}')";
+    
+    #If an AppliesTo was passed in, we should pay attention to it.
+    #otherwise, none is needed
+    
+    $ScopeClause = "($ScopeClause AND (RightAppliesTo = $args{'AppliesTo'}))"
+      if ($args{'AppliesTo'});
+    
+    
+    # The generic principals clause looks for users with my id
+    # and Rights that apply to _everyone_
+    my $PrincipalsClause = "((PrincipalType = 'User') AND (PrincipalId = ".$self->Id."))";
+    
+    
+    # If the user is the superuser, grant them the damn right ;)
+    my $SuperUserClause = 
+      "(RightName = 'SuperUser') AND (RightScope = 'System') AND (RightAppliesTo = 0)";
+    
+    # If we've been passed in an extended principals clause, we should lump it
+    # on to the existing principals clause. it'll make life easier
+    if ($args{'ExtendedPrincipals'}) {
+       $PrincipalsClause = "(($PrincipalsClause) OR ".
+         "($args{'ExtendedPrincipalsClause'}))";
+    }
+    
+    my $GroupPrincipalsClause = "((ACL.PrincipalType = 'Group') ".
+      "AND (ACL.PrincipalId = Groups.Id) AND (GroupMembers.GroupId = Groups.Id) ".
+     " AND (GroupMembers.UserId = ".$self->Id."))";
+    
+    
+
+
+    # {{{ A bunch of magic statements that make the metagroups listed
+    # work. basically, we if the user falls into the right group,
+    # we add the type of ACL check needed
+    my (@MetaPrincipalsSubClauses, $MetaPrincipalsClause);
+    
+    #The user is always part of the 'Everyone' Group
+    push (@MetaPrincipalsSubClauses,  "((Groups.Name = 'Everyone') AND 
+                                       (PrincipalType = 'Group') AND 
+                                       (Groups.Id = PrincipalId))");
+
+    if ($args{'IsAdminCc'}) {
+       push (@MetaPrincipalsSubClauses,  "((Groups.Name = 'AdminCc') AND 
+                                       (PrincipalType = 'Group') AND 
+                                       (Groups.Id = PrincipalId))");
+    }
+    if ($args{'IsCc'}) {
+       push (@MetaPrincipalsSubClauses, " ((Groups.Name = 'Cc') AND 
+                                       (PrincipalType = 'Group') AND 
+                                       (Groups.Id = PrincipalId))");
+    }
+    if ($args{'IsRequestor'}) {
+       push (@MetaPrincipalsSubClauses,  " ((Groups.Name = 'Requestor') AND 
+                                       (PrincipalType = 'Group') AND 
+                                       (Groups.Id = PrincipalId))");
+    }
+    if ($args{'IsOwner'}) {
+       
+       push (@MetaPrincipalsSubClauses, " ((Groups.Name = 'Owner') AND 
+                                       (PrincipalType = 'Group') AND 
+                                       (Groups.Id = PrincipalId))");
+    }
+
+    # }}}
+    
+    my ($GroupRightsQuery, $MetaGroupRightsQuery, $IndividualRightsQuery, $hitcount);
+    
+    # {{{ If there are any metaprincipals to be checked
+    if (@MetaPrincipalsSubClauses) {
+       #chop off the leading or
+       #TODO redo this with an array and a join
+       $MetaPrincipalsClause = join (" OR ", @MetaPrincipalsSubClauses);
+       
+       $MetaGroupRightsQuery =  "SELECT COUNT(ACL.id) FROM ACL, Groups".
+         " WHERE " .
+           " ($ScopeClause) AND ($RightClause) AND ($MetaPrincipalsClause)";
+       
+       # {{{ deal with checking if the user has a right as a member of a metagroup
+
+#      $RT::Logger->debug("Now Trying $MetaGroupRightsQuery\n");       
+       $hitcount = $self->_Handle->FetchResult($MetaGroupRightsQuery);
+       
+       #if there's a match, the right is granted
+       if ($hitcount) {
+           $self->{'rights'}{"$hashkey"}{'set'} = time;
+           $self->{'rights'}{"$hashkey"} = 1;
+           return (1);
+       }
+       
+#      $RT::Logger->debug("No ACL matched MetaGroups query: $MetaGroupRightsQuery\n"); 
+
+       # }}}    
+       
+    }
+    # }}}
+
+    # {{{ deal with checking if the user has a right as a member of a group
+    # This query checks to se whether the user has the right as a member of a
+    # group
+    $GroupRightsQuery = "SELECT COUNT(ACL.id) FROM ACL, GroupMembers, Groups".
+      " WHERE " .
+       " (((($ScopeClause) AND ($RightClause)) OR ($SuperUserClause)) ".
+         " AND ($GroupPrincipalsClause))";    
+    
+    #  $RT::Logger->debug("Now Trying $GroupRightsQuery\n");   
+    $hitcount = $self->_Handle->FetchResult($GroupRightsQuery);
+    
+    #if there's a match, the right is granted
+    if ($hitcount) {
+       $self->{'rights'}{"$hashkey"}{'set'} = time;
+       $self->{'rights'}{"$hashkey"} = 1;
+       return (1);
+    }
+    
+#    $RT::Logger->debug("No ACL matched $GroupRightsQuery\n"); 
+    
+    # }}}
+
+    # {{{ Check to see whether the user has a right as an individual
+    
+    # This query checks to see whether the current user has the right directly
+    $IndividualRightsQuery = "SELECT COUNT(ACL.id) FROM ACL WHERE ".
+      " ((($ScopeClause) AND ($RightClause)) OR ($SuperUserClause)) " .
+       " AND ($PrincipalsClause)";
+
+    
+    $hitcount = $self->_Handle->FetchResult($IndividualRightsQuery);
+    
+    if ($hitcount) {
+       $self->{'rights'}{"$hashkey"}{'set'} = time;
+       $self->{'rights'}{"$hashkey"} = 1;
+       return (1);
+    }
+    # }}}
+
+    else { #If the user just doesn't have the right
+       
+#      $RT::Logger->debug("No ACL matched $IndividualRightsQuery\n");
+       
+       #If nothing matched, return 0.
+       $self->{'rights'}{"$hashkey"}{'set'} = time;
+       $self->{'rights'}{"$hashkey"} = -1;
+
+       
+       return (undef);
+    }
+}
 
-=item SetPGPKey VALUE
+# }}}
 
+# {{{ sub CurrentUserCanModify
 
-Set PGPKey to VALUE. 
-Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
-(In the database, PGPKey will be stored as a text.)
+=head2 CurrentUserCanModify RIGHT
 
+If the user has rights for this object, either because
+he has 'AdminUsers' or (if he\'s trying to edit himself and the right isn\'t an 
+admin right) 'ModifySelf', return 1. otherwise, return undef.
 
 =cut
 
+sub CurrentUserCanModify {
+    my $self = shift;
+    my $right = shift;
+
+    if ($self->CurrentUserHasRight('AdminUsers')) {
+       return (1);
+    }
+    #If the field is marked as an "administrators only" field, 
+    # don\'t let the user touch it.
+    elsif ($self->_Accessible($right, 'admin')) {
+       return(undef);
+    }
+    
+    #If the current user is trying to modify themselves
+    elsif ( ($self->id == $self->CurrentUser->id)  and
+           ($self->CurrentUserHasRight('ModifySelf'))) {
+       return(1);
+    }
+    #If we don\'t have a good reason to grant them rights to modify
+    # by now, they lose
+    else {
+       return(undef);
+    }
+    
+}
 
-=item Creator
+# }}}
 
-Returns the current value of Creator. 
-(In the database, Creator is stored as int(11).)
+# {{{ sub CurrentUserHasRight
 
+=head2 CurrentUserHasRight
+  
+  Takes a single argument. returns 1 if $Self->CurrentUser
+  has the requested right. returns undef otherwise
 
 =cut
 
+sub CurrentUserHasRight {
+    my $self = shift;
+    my $right = shift;
+    
+    return ($self->CurrentUser->HasSystemRight($right));
+}
 
-=item Created
+# }}}
 
-Returns the current value of Created. 
-(In the database, Created is stored as datetime.)
 
+# {{{ sub _Set
 
-=cut
+sub _Set {
+  my $self = shift;
+  
+  my %args = (Field => undef,
+             Value => undef,
+             @_
+            );
 
+  # Nobody is allowed to futz with RT_System or Nobody unless they
+  # want to change an email address. For 2.2, neither should have an email address
 
-=item LastUpdatedBy
+  if ($self->Privileged == 2) {
+    return (0, "Can not modify system users"); 
+  }
+  unless ($self->CurrentUserCanModify($args{'Field'})) {
+      return (0, "Permission Denied");
+  }
 
-Returns the current value of LastUpdatedBy. 
-(In the database, LastUpdatedBy is stored as int(11).)
 
+  
+  #Set the new value
+  my ($ret, $msg)=$self->SUPER::_Set(Field => $args{'Field'}, 
+                                    Value=> $args{'Value'});
+  
+    return ($ret, $msg);
+}
 
-=cut
-
+# }}}
 
-=item LastUpdated
+# {{{ sub _Value 
 
-Returns the current value of LastUpdated. 
-(In the database, LastUpdated is stored as datetime.)
+=head2 _Value
 
+Takes the name of a table column.
+Returns its value as a string, if the user passes an ACL check
 
 =cut
 
+sub _Value  {
+
+  my $self = shift;
+  my $field = shift;
+  
+  #If the current user doesn't have ACLs, don't let em at it.  
+  
+  my @PublicFields = qw( Name EmailAddress Organization Disabled
+                        RealName NickName Gecos ExternalAuthId 
+                        AuthSystem ExternalContactInfoId 
+                        ContactInfoSystem );
+
+  #if the field is public, return it.
+  if ($self->_Accessible($field, 'public')) {
+      return($self->SUPER::_Value($field));
+      
+  }
+  #If the user wants to see their own values, let them
+  elsif ($self->CurrentUser->Id == $self->Id) {        
+      return($self->SUPER::_Value($field));
+  } 
+  #If the user has the admin users right, return the field
+  elsif ($self->CurrentUserHasRight('AdminUsers')) {
+      return($self->SUPER::_Value($field));
+  }
+  else {
+      return(undef);
+  }    
 
+}
 
-sub _ClassAccessible {
-    {
-     
-        id =>
-               {read => 1, type => 'int(11)', default => ''},
-        Name => 
-               {read => 1, write => 1, type => 'varchar(200)', default => ''},
-        Password => 
-               {read => 1, write => 1, type => 'varchar(40)', default => ''},
-        Comments => 
-               {read => 1, write => 1, type => 'blob', default => ''},
-        Signature => 
-               {read => 1, write => 1, type => 'blob', default => ''},
-        EmailAddress => 
-               {read => 1, write => 1, type => 'varchar(120)', default => ''},
-        FreeformContactInfo => 
-               {read => 1, write => 1, type => 'blob', default => ''},
-        Organization => 
-               {read => 1, write => 1, type => 'varchar(200)', default => ''},
-        RealName => 
-               {read => 1, write => 1, type => 'varchar(120)', default => ''},
-        NickName => 
-               {read => 1, write => 1, type => 'varchar(16)', default => ''},
-        Lang => 
-               {read => 1, write => 1, type => 'varchar(16)', default => ''},
-        EmailEncoding => 
-               {read => 1, write => 1, type => 'varchar(16)', default => ''},
-        WebEncoding => 
-               {read => 1, write => 1, type => 'varchar(16)', default => ''},
-        ExternalContactInfoId => 
-               {read => 1, write => 1, type => 'varchar(100)', default => ''},
-        ContactInfoSystem => 
-               {read => 1, write => 1, type => 'varchar(30)', default => ''},
-        ExternalAuthId => 
-               {read => 1, write => 1, type => 'varchar(100)', default => ''},
-        AuthSystem => 
-               {read => 1, write => 1, type => 'varchar(30)', default => ''},
-        Gecos => 
-               {read => 1, write => 1, type => 'varchar(16)', default => ''},
-        HomePhone => 
-               {read => 1, write => 1, type => 'varchar(30)', default => ''},
-        WorkPhone => 
-               {read => 1, write => 1, type => 'varchar(30)', default => ''},
-        MobilePhone => 
-               {read => 1, write => 1, type => 'varchar(30)', default => ''},
-        PagerPhone => 
-               {read => 1, write => 1, type => 'varchar(30)', default => ''},
-        Address1 => 
-               {read => 1, write => 1, type => 'varchar(200)', default => ''},
-        Address2 => 
-               {read => 1, write => 1, type => 'varchar(200)', default => ''},
-        City => 
-               {read => 1, write => 1, type => 'varchar(100)', default => ''},
-        State => 
-               {read => 1, write => 1, type => 'varchar(100)', default => ''},
-        Zip => 
-               {read => 1, write => 1, type => 'varchar(16)', default => ''},
-        Country => 
-               {read => 1, write => 1, type => 'varchar(50)', default => ''},
-        Timezone => 
-               {read => 1, write => 1, type => 'varchar(50)', default => ''},
-        PGPKey => 
-               {read => 1, write => 1, type => 'text', default => ''},
-        Creator => 
-               {read => 1, auto => 1, type => 'int(11)', default => '0'},
-        Created => 
-               {read => 1, auto => 1, type => 'datetime', default => ''},
-        LastUpdatedBy => 
-               {read => 1, auto => 1, type => 'int(11)', default => '0'},
-        LastUpdated => 
-               {read => 1, auto => 1, type => 'datetime', default => ''},
-
- }
-};
-
-
-        eval "require RT::User_Overlay";
-        if ($@ && $@ !~ qr{^Can't locate RT/User_Overlay.pm}) {
-            die $@;
-        };
-
-        eval "require RT::User_Vendor";
-        if ($@ && $@ !~ qr{^Can't locate RT/User_Vendor.pm}) {
-            die $@;
-        };
-
-        eval "require RT::User_Local";
-        if ($@ && $@ !~ qr{^Can't locate RT/User_Local.pm}) {
-            die $@;
-        };
-
-
-
-
-=head1 SEE ALSO
-
-This class allows "overlay" methods to be placed
-into the following files _Overlay is for a System overlay by the original author,
-_Vendor is for 3rd-party vendor add-ons, while _Local is for site-local customizations.  
-
-These overlay files can contain new subs or subs to replace existing subs in this module.
-
-If you'll be working with perl 5.6.0 or greater, each of these files should begin with the line 
-
-   no warnings qw(redefine);
-
-so that perl does not kick and scream when you redefine a subroutine or variable in your overlay.
-
-RT::User_Overlay, RT::User_Vendor, RT::User_Local
-
-=cut
-
+# }}}
 
+# }}}
 1;