X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=rt%2Flib%2FRT%2FUser.pm;h=b8a51f064a97d3aed95d0f34b92e4e2f0bfc24e3;hp=1859d3f5c97034befc28e006b04a48992d616ab4;hb=9aee669886202be7035e6c6049fc71bc99dd3013;hpb=919e930aa9279b3c5cd12b593889cd6de79d67bf diff --git a/rt/lib/RT/User.pm b/rt/lib/RT/User.pm index 1859d3f5c..b8a51f064 100755 --- a/rt/lib/RT/User.pm +++ b/rt/lib/RT/User.pm @@ -66,6 +66,7 @@ package RT::User; use strict; use warnings; +use Scalar::Util qw(blessed); use base 'RT::Record'; @@ -78,6 +79,7 @@ sub Table {'Users'} use Digest::SHA; use Digest::MD5; +use Crypt::Eksblowfish::Bcrypt qw(); use RT::Principals; use RT::ACE; use RT::Interface::Email; @@ -86,22 +88,25 @@ use Text::Password::Pronounceable; sub _OverlayAccessible { { - Name => { public => 1, admin => 1 }, + Name => { public => 1, admin => 1 }, # loc_left_pair Password => { read => 0 }, - EmailAddress => { public => 1 }, - Organization => { public => 1, admin => 1 }, - RealName => { public => 1 }, - NickName => { public => 1 }, - Lang => { public => 1 }, + EmailAddress => { public => 1 }, # loc_left_pair + Organization => { public => 1, admin => 1 }, # loc_left_pair + RealName => { public => 1 }, # loc_left_pair + NickName => { public => 1 }, # loc_left_pair + Lang => { public => 1 }, # loc_left_pair EmailEncoding => { public => 1 }, WebEncoding => { public => 1 }, ExternalContactInfoId => { public => 1, admin => 1 }, ContactInfoSystem => { public => 1, admin => 1 }, ExternalAuthId => { public => 1, admin => 1 }, AuthSystem => { public => 1, admin => 1 }, - Gecos => { public => 1, admin => 1 }, - PGPKey => { public => 1, admin => 1 }, - + Gecos => { public => 1, admin => 1 }, # loc_left_pair + PGPKey => { public => 1, admin => 1 }, # loc_left_pair + SMIMECertificate => { public => 1, admin => 1 }, # loc_left_pair + City => { public => 1 }, # loc_left_pair + Country => { public => 1 }, # loc_left_pair + Timezone => { public => 1 }, # loc_left_pair } } @@ -296,7 +301,7 @@ sub ValidatePassword { my $password = shift; if ( length($password) < RT->Config->Get('MinimumPasswordLength') ) { - return ( 0, $self->loc("Password needs to be at least [_1] characters long", RT->Config->Get('MinimumPasswordLength')) ); + return ( 0, $self->loc("Password needs to be at least [quant,_1,character,characters] long", RT->Config->Get('MinimumPasswordLength')) ); } return 1; @@ -549,8 +554,8 @@ sub LoadOrCreateByEmail { } } } - return (0, $message) unless $self->id; - return ($self->Id, $message); + return wantarray ? (0, $message) : 0 unless $self->id; + return wantarray ? ($self->Id, $message) : $self->Id; } =head2 ValidateEmailAddress ADDRESS @@ -628,25 +633,13 @@ sub SetEmailAddress { =head2 EmailFrequency -Takes optional Ticket argument in paramhash. Returns 'no email', -'squelched', 'daily', 'weekly' or empty string depending on -user preferences. - -=over 4 - -=item 'no email' - user has no email, so can not recieve notifications. - -=item 'squelched' - returned only when Ticket argument is provided and -notifications to the user has been supressed for this ticket. - -=item 'daily' - retruned when user recieve daily messages digest instead -of immediate delivery. - -=item 'weekly' - previous, but weekly. +Takes optional Ticket argument in paramhash. Returns a string, suitable +for localization, describing any notable properties about email delivery +to the user. This includes lack of email address, ticket-level +squelching (if C is provided in the paramhash), or user email +delivery preferences. -=item empty string returned otherwise. - -=back +Returns the empty string if there are no notable properties. =cut @@ -658,12 +651,18 @@ sub EmailFrequency { ); return '' unless $self->id && $self->id != RT->Nobody->id && $self->id != RT->SystemUser->id; - return 'no email address' unless my $email = $self->EmailAddress; - return 'email disabled for ticket' if $args{'Ticket'} && - grep lc $email eq lc $_->Content, $args{'Ticket'}->SquelchMailTo; + return 'no email address set' # loc + unless my $email = $self->EmailAddress; + return 'email disabled for ticket' # loc + if $args{'Ticket'} && + grep lc $email eq lc $_->Content, $args{'Ticket'}->SquelchMailTo; my $frequency = RT->Config->Get( 'EmailFrequency', $self ) || ''; - return 'daily' if $frequency =~ /daily/i; - return 'weekly' if $frequency =~ /weekly/i; + return 'receives daily digests' # loc + if $frequency =~ /daily/i; + return 'receives weekly digests' # loc + if $frequency =~ /weekly/i; + return 'email delivery suspended' # loc + if $frequency =~ /suspend/i; return ''; } @@ -865,6 +864,39 @@ sub SetPassword { } +sub _GeneratePassword_bcrypt { + my $self = shift; + my ($password, @rest) = @_; + + my $salt; + my $rounds; + if (@rest) { + # The first split is the number of rounds + $rounds = $rest[0]; + + # The salt is the first 22 characters, b64 encoded usign the + # special bcrypt base64. + $salt = Crypt::Eksblowfish::Bcrypt::de_base64( substr($rest[1], 0, 22) ); + } else { + $rounds = RT->Config->Get('BcryptCost'); + + # Generate a random 16-octet base64 salt + $salt = ""; + $salt .= pack("C", int rand(256)) for 1..16; + } + + my $hash = Crypt::Eksblowfish::Bcrypt::bcrypt_hash({ + key_nul => 1, + cost => $rounds, + salt => $salt, + }, Digest::SHA::sha512( Encode::encode( 'UTF-8', $password) ) ); + + return join("!", "", "bcrypt", sprintf("%02d", $rounds), + Crypt::Eksblowfish::Bcrypt::en_base64( $salt ). + Crypt::Eksblowfish::Bcrypt::en_base64( $hash ) + ); +} + sub _GeneratePassword_sha512 { my $self = shift; my ($password, $salt) = @_; @@ -888,13 +920,13 @@ Returns a string to store in the database. This string takes the form: !method!salt!hash -By default, the method is currently C. +By default, the method is currently C. =cut sub _GeneratePassword { my $self = shift; - return $self->_GeneratePassword_sha512(@_); + return $self->_GeneratePassword_bcrypt(@_); } =head3 HasPassword @@ -943,9 +975,13 @@ sub IsPassword { my $stored = $self->__Value('Password'); if ($stored =~ /^!/) { # If it's a new-style (>= RT 4.0) password, it starts with a '!' - my (undef, $method, $salt, undef) = split /!/, $stored; - if ($method eq "sha512") { - return $self->_GeneratePassword_sha512($value, $salt) eq $stored; + my (undef, $method, @rest) = split /!/, $stored; + if ($method eq "bcrypt") { + return 0 unless $self->_GeneratePassword_bcrypt($value, @rest) eq $stored; + # Upgrade to a larger number of rounds if necessary + return 1 unless $rest[0] < RT->Config->Get('BcryptCost'); + } elsif ($method eq "sha512") { + return 0 unless $self->_GeneratePassword_sha512($value, @rest) eq $stored; } else { $RT::Logger->warn("Unknown hash method $method"); return 0; @@ -986,8 +1022,8 @@ sub CurrentUserRequireToSetPassword { RequireCurrent => 1, ); - if ( RT->Config->Get('WebExternalAuth') - && !RT->Config->Get('WebFallbackToInternalAuth') + if ( RT->Config->Get('WebRemoteUserAuth') + && !RT->Config->Get('WebFallbackToRTLogin') ) { $res{'CanSet'} = 0; $res{'Reason'} = $self->loc("External authentication enabled."); @@ -1092,11 +1128,11 @@ sub SetDisabled { } $RT::Handle->BeginTransaction(); - my $set_err = $self->PrincipalObj->SetDisabled($val); - unless ($set_err) { + my ($status, $msg) = $self->PrincipalObj->SetDisabled($val); + unless ($status) { $RT::Handle->Rollback(); $RT::Logger->warning(sprintf("Couldn't %s user %s", ($val == 1) ? "disable" : "enable", $self->PrincipalObj->Id)); - return (undef); + return ($status, $msg); } $self->_NewTransaction( Type => ($val == 1) ? "Disabled" : "Enabled" ); @@ -1247,26 +1283,29 @@ public, ourself, or we have AdminUsers sub CurrentUserCanSee { my $self = shift; - my ($what) = @_; + my ($what, $txn) = @_; - # If it's public, fine. Note that $what may be "transaction", which - # doesn't have an Accessible value, and thus falls through below. - if ( $self->_Accessible( $what, 'public' ) ) { - return 1; - } + # If it's a public property, fine + return 1 if $self->_Accessible( $what, 'public' ); - # Users can see their own properties - elsif ( defined($self->Id) && $self->CurrentUser->Id == $self->Id ) { - return 1; - } + # Users can see all of their own properties + return 1 if defined($self->Id) and $self->CurrentUser->Id == $self->Id; # If the user has the admin users right, that's also enough - elsif ( $self->CurrentUser->HasRight( Right => 'AdminUsers', Object => $RT::System) ) { - return 1; - } - else { - return 0; + return 1 if $self->CurrentUserHasRight( 'AdminUsers' ); + + # Transactions of public properties are visible to users with ShowUserHistory + if ($what eq "Transaction" and $self->CurrentUserHasRight( 'ShowUserHistory' )) { + my $type = $txn->__Value('Type'); + my $field = $txn->__Value('Field'); + return 1 if $type eq "Set" and $self->CurrentUserCanSee($field, $txn); + + # RT::Transaction->CurrentUserCanSee deals with ensuring we meet + # the ACLs on CFs, so allow them here + return 1 if $type eq "CustomField"; } + + return 0; } =head2 CurrentUserCanModify RIGHT @@ -1326,7 +1365,7 @@ sub _PrefName { $name = ref($name).'-'.$name->Id; } - return 'Pref-'.$name; + return 'Pref-'. $name; } =head2 Preferences NAME/OBJ DEFAULT @@ -1339,7 +1378,7 @@ override the entries with user preferences. sub Preferences { my $self = shift; - my $name = _PrefName (shift); + my $name = _PrefName(shift); my $default = shift; my ($attr) = $self->Attributes->Named( $name ); @@ -1353,7 +1392,7 @@ sub Preferences { exists $content->{$_} or $content->{$_} = $default->{$_}; } } elsif (defined $default) { - $RT::Logger->error("Preferences $name for user".$self->Id." is hash but default is not"); + $RT::Logger->error("Preferences $name for user #".$self->Id." is hash but default is not"); } return $content; } @@ -1415,10 +1454,8 @@ sub Stylesheet { my $style = RT->Config->Get('WebDefaultStylesheet', $self->CurrentUser); if (RT::Interface::Web->ComponentPathIsSafe($style)) { - my @css_paths = map { $_ . '/NoAuth/css' } RT::Interface::Web->ComponentRoots; - - for my $css_path (@css_paths) { - if (-d "$css_path/$style") { + for my $root (RT::Interface::Web->StaticRoots) { + if (-d "$root/css/$style") { return $style } } @@ -1459,12 +1496,13 @@ sub WatchedQueues { FIELD => 'Domain', VALUE => 'RT::Queue-Role', ENTRYAGGREGATOR => 'AND', + CASESENSITIVE => 0, ); if (grep { $_ eq 'Cc' } @roles) { $watched_queues->Limit( SUBCLAUSE => 'LimitToWatchers', ALIAS => $group_alias, - FIELD => 'Type', + FIELD => 'Name', VALUE => 'Cc', ENTRYAGGREGATOR => 'OR', ); @@ -1473,7 +1511,7 @@ sub WatchedQueues { $watched_queues->Limit( SUBCLAUSE => 'LimitToWatchers', ALIAS => $group_alias, - FIELD => 'Type', + FIELD => 'Name', VALUE => 'AdminCc', ENTRYAGGREGATOR => 'OR', ); @@ -1575,9 +1613,134 @@ Return the friendly name sub FriendlyName { my $self = shift; - return $self->RealName if defined($self->RealName); - return $self->Name if defined($self->Name); - return ""; + return $self->RealName if defined $self->RealName and length $self->RealName; + return $self->Name; +} + +=head2 Format + +Class or object method. + +Returns a string describing a user in the current user's preferred format. + +May be invoked in three ways: + + $UserObj->Format; + RT::User->Format( User => $UserObj ); # same as above + RT::User->Format( Address => $AddressObj, CurrentUser => $CurrentUserObj ); + +Possible arguments are: + +=over + +=item User + +An L object representing the user to format. Preferred to Address. + +=item Address + +An L object representing the user address to format. Address +will be used to lookup an L if possible. + +=item CurrentUser + +Required when Format is called as a class method with an Address argument. +Otherwise, this argument is ignored in preference to the CurrentUser of the +involved L object. + +=item Format + +Specifies the format to use, overriding any set from the config or current +user's preferences. + +=back + +=cut + +sub Format { + my $self = shift; + my %args = ( + User => undef, + Address => undef, + CurrentUser => undef, + Format => undef, + @_ + ); + + if (blessed($self) and $self->id) { + @args{"User", "CurrentUser"} = ($self, $self->CurrentUser); + } + elsif ($args{User} and $args{User}->id) { + $args{CurrentUser} = $args{User}->CurrentUser; + } + elsif ($args{Address} and $args{CurrentUser}) { + $args{User} = RT::User->new( $args{CurrentUser} ); + $args{User}->LoadByEmail( $args{Address}->address ); + if ($args{User}->id) { + delete $args{Address}; + } else { + delete $args{User}; + } + } + else { + RT->Logger->warning("Invalid arguments to RT::User->Format at @{[join '/', caller]}"); + return ""; + } + + $args{Format} ||= RT->Config->Get("UsernameFormat", $args{CurrentUser}); + $args{Format} =~ s/[^A-Za-z0-9_]+//g; + + my $method = "_FormatUser" . ucfirst lc $args{Format}; + my $formatter = $self->can($method); + + unless ($formatter) { + RT->Logger->error( + "Either system config or user #" . $args{CurrentUser}->id . + " picked UsernameFormat $args{Format}, but RT::User->$method doesn't exist" + ); + $formatter = $self->can("_FormatUserRole"); + } + return $formatter->( $self, map { $_ => $args{$_} } qw(User Address) ); +} + +sub _FormatUserRole { + my $self = shift; + my %args = @_; + + my $user = $args{User}; + return $self->_FormatUserVerbose(@_) + unless $user and $user->Privileged; + + my $name = $user->Name; + $name .= " (".$user->RealName.")" + if $user->RealName and lc $user->RealName ne lc $user->Name; + return $name; +} + +sub _FormatUserConcise { + my $self = shift; + my %args = @_; + return $args{User} ? $args{User}->FriendlyName : $args{Address}->address; +} + +sub _FormatUserVerbose { + my $self = shift; + my %args = @_; + my ($user, $address) = @args{"User", "Address"}; + + my $email = ''; + my $phrase = ''; + my $comment = ''; + + if ($user) { + $email = $user->EmailAddress || ''; + $phrase = $user->RealName if $user->RealName and lc $user->RealName ne lc $email; + $comment = $user->Name if lc $user->Name ne lc $email; + } else { + ($email, $phrase, $comment) = (map { $address->$_ } "address", "phrase", "comment"); + } + + return join " ", grep { $_ } ($phrase || $comment || ''), ($email ? "<$email>" : ""); } =head2 PreferredKey @@ -1604,8 +1767,7 @@ sub PreferredKey return $prefkey->Content if $prefkey; # we don't have a preferred key for this user, so now we must query GPG - require RT::Crypt::GnuPG; - my %res = RT::Crypt::GnuPG::GetKeysForEncryption($self->EmailAddress); + my %res = RT::Crypt->GetKeysForEncryption($self->EmailAddress); return undef unless defined $res{'info'}; my @keys = @{ $res{'info'} }; return undef if @keys == 0; @@ -1659,7 +1821,7 @@ sub SetPrivateKey { # check that it's really private key { - my %tmp = RT::Crypt::GnuPG::GetKeysForSigning( $key ); + my %tmp = RT::Crypt->GetKeysForSigning( Signer => $key, Protocol => 'GnuPG' ); return (0, $self->loc("No such key or it's not suitable for signing")) if $tmp{'exit_code'} || !$tmp{'info'}; } @@ -1673,6 +1835,21 @@ sub SetPrivateKey { return ($status, $self->loc("Set private key")); } +sub SetLang { + my $self = shift; + my ($lang) = @_; + + unless ($self->CurrentUserCanModify('Lang')) { + return (0, $self->loc("Permission Denied")); + } + + # Local hack to cause the result message to be in the _new_ language + # if we're updating ourselves + $self->CurrentUser->{LangHandle} = RT::I18N->get_handle( $lang ) + if $self->CurrentUser->id == $self->id; + return $self->_Set( Field => 'Lang', Value => $lang ); +} + sub BasicColumns { ( [ Name => 'Username' ], @@ -1682,6 +1859,79 @@ sub BasicColumns { ); } +=head2 Bookmarks + +Returns an unordered list of IDs representing the user's bookmarked tickets. + +=cut + +sub Bookmarks { + my $self = shift; + my $bookmarks = $self->FirstAttribute('Bookmarks'); + return if !$bookmarks; + + $bookmarks = $bookmarks->Content; + return if !$bookmarks; + + return keys %$bookmarks; +} + +=head2 HasBookmark TICKET + +Returns whether the provided ticket is bookmarked by the user. + +=cut + +sub HasBookmark { + my $self = shift; + my $ticket = shift; + my $id = $ticket->id; + + # maintain bookmarks across merges + my @ids = ($id, $ticket->Merged); + + my $bookmarks = $self->FirstAttribute('Bookmarks'); + $bookmarks = $bookmarks ? $bookmarks->Content : {}; + + my @bookmarked = grep { $bookmarks->{ $_ } } @ids; + return @bookmarked ? 1 : 0; +} + +=head2 ToggleBookmark TICKET + +Toggles whether the provided ticket is bookmarked by the user. + +=cut + +sub ToggleBookmark { + my $self = shift; + my $ticket = shift; + my $id = $ticket->id; + + # maintain bookmarks across merges + my @ids = ($id, $ticket->Merged); + + my $bookmarks = $self->FirstAttribute('Bookmarks'); + $bookmarks = $bookmarks ? $bookmarks->Content : {}; + + my $is_bookmarked; + + if ( grep { $bookmarks->{ $_ } } @ids ) { + delete $bookmarks->{ $_ } foreach @ids; + $is_bookmarked = 0; + } else { + $bookmarks->{ $id } = 1; + $is_bookmarked = 1; + } + + $self->SetAttribute( + Name => 'Bookmarks', + Content => $bookmarks, + ); + + return $is_bookmarked; +} + =head2 Create PARAMHASH Create takes a hash of values and creates a row in the database: @@ -2271,6 +2521,24 @@ Returns (1, 'Status message') on success and (0, 'Error Message') on failure. =cut +=head2 SMIMECertificate + +Returns the current value of SMIMECertificate. +(In the database, SMIMECertificate is stored as text.) + + + +=head2 SetSMIMECertificate VALUE + + +Set SMIMECertificate to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, SMIMECertificate will be stored as a text.) + + +=cut + + =head2 Creator Returns the current value of Creator. @@ -2569,6 +2837,8 @@ sub _CoreAccessible { {read => 1, write => 1, sql_type => 12, length => 50, is_blob => 0, is_numeric => 0, type => 'varchar(50)', default => ''}, PGPKey => {read => 1, write => 1, sql_type => -4, length => 0, is_blob => 1, is_numeric => 0, type => 'text', default => ''}, + SMIMECertificate => + {read => 1, write => 1, sql_type => -4, length => 0, is_blob => 1, is_numeric => 0, type => 'text', default => ''}, Creator => {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'}, Created => @@ -2581,6 +2851,213 @@ sub _CoreAccessible { } }; +sub UID { + my $self = shift; + return undef unless defined $self->Name; + return "@{[ref $self]}-@{[$self->Name]}"; +} + +sub FindDependencies { + my $self = shift; + my ($walker, $deps) = @_; + + $self->SUPER::FindDependencies($walker, $deps); + + # ACL equivalence group + my $objs = RT::Groups->new( $self->CurrentUser ); + $objs->Limit( FIELD => 'Domain', VALUE => 'ACLEquivalence', CASESENSITIVE => 0 ); + $objs->Limit( FIELD => 'Instance', VALUE => $self->Id ); + $deps->Add( in => $objs ); + + # Memberships in SystemInternal groups + $objs = RT::GroupMembers->new( $self->CurrentUser ); + $objs->Limit( FIELD => 'MemberId', VALUE => $self->Id ); + my $principals = $objs->Join( + ALIAS1 => 'main', + FIELD1 => 'GroupId', + TABLE2 => 'Principals', + FIELD2 => 'id', + ); + my $groups = $objs->Join( + ALIAS1 => $principals, + FIELD1 => 'ObjectId', + TABLE2 => 'Groups', + FIELD2 => 'Id', + ); + $objs->Limit( + ALIAS => $groups, + FIELD => 'Domain', + VALUE => 'SystemInternal', + CASESENSITIVE => 0 + ); + $deps->Add( in => $objs ); + + # XXX: This ignores the myriad of "in" references from the Creator + # and LastUpdatedBy columns. +} + +sub __DependsOn { + my $self = shift; + my %args = ( + Shredder => undef, + Dependencies => undef, + @_, + ); + my $deps = $args{'Dependencies'}; + my $list = []; + +# Principal + $deps->_PushDependency( + BaseObject => $self, + Flags => RT::Shredder::Constants::DEPENDS_ON | RT::Shredder::Constants::WIPE_AFTER, + TargetObject => $self->PrincipalObj, + Shredder => $args{'Shredder'} + ); + +# ACL equivalence group +# don't use LoadACLEquivalenceGroup cause it may not exists any more + my $objs = RT::Groups->new( $self->CurrentUser ); + $objs->Limit( FIELD => 'Domain', VALUE => 'ACLEquivalence', CASESENSITIVE => 0 ); + $objs->Limit( FIELD => 'Instance', VALUE => $self->Id ); + push( @$list, $objs ); + +# Cleanup user's membership + $objs = RT::GroupMembers->new( $self->CurrentUser ); + $objs->Limit( FIELD => 'MemberId', VALUE => $self->Id ); + push( @$list, $objs ); + + $deps->_PushDependencies( + BaseObject => $self, + Flags => RT::Shredder::Constants::DEPENDS_ON, + TargetObjects => $list, + Shredder => $args{'Shredder'} + ); + +# TODO: Almost all objects has Creator, LastUpdatedBy and etc. fields +# which are references on users(Principal actualy) + my @OBJECTS = qw( + ACL + Articles + Attachments + Attributes + CachedGroupMembers + Classes + CustomFieldValues + CustomFields + GroupMembers + Groups + Links + ObjectClasses + ObjectCustomFieldValues + ObjectCustomFields + ObjectScrips + Principals + Queues + ScripActions + ScripConditions + Scrips + Templates + Tickets + Transactions + Users + ); + my @var_objs; + foreach( @OBJECTS ) { + my $class = "RT::$_"; + foreach my $method ( qw(Creator LastUpdatedBy) ) { + my $objs = $class->new( $self->CurrentUser ); + next unless $objs->RecordClass->_Accessible( $method => 'read' ); + $objs->Limit( FIELD => $method, VALUE => $self->id ); + push @var_objs, $objs; + } + } + $deps->_PushDependencies( + BaseObject => $self, + Flags => RT::Shredder::Constants::DEPENDS_ON | RT::Shredder::Constants::VARIABLE, + TargetObjects => \@var_objs, + Shredder => $args{'Shredder'} + ); + + return $self->SUPER::__DependsOn( %args ); +} + +sub BeforeWipeout { + my $self = shift; + if( $self->Name =~ /^(RT_System|Nobody)$/ ) { + RT::Shredder::Exception::Info->throw('SystemObject'); + } + return $self->SUPER::BeforeWipeout( @_ ); +} + +sub Serialize { + my $self = shift; + return ( + Disabled => $self->PrincipalObj->Disabled, + Principal => $self->PrincipalObj->UID, + PrincipalId => $self->PrincipalObj->Id, + $self->SUPER::Serialize(@_), + ); +} + +sub PreInflate { + my $class = shift; + my ($importer, $uid, $data) = @_; + + my $principal_uid = delete $data->{Principal}; + my $principal_id = delete $data->{PrincipalId}; + my $disabled = delete $data->{Disabled}; + + my $obj = RT::User->new( RT->SystemUser ); + $obj->LoadByCols( Name => $data->{Name} ); + $obj->LoadByEmail( $data->{EmailAddress} ) unless $obj->Id; + if ($obj->Id) { + # User already exists -- merge + + # XXX: We might be merging a privileged user into an unpriv one, + # in which case we should probably promote the unpriv user to + # being privileged. Of course, we don't know if the user being + # imported is privileged yet, as its group memberships show up + # later in the stream... + $importer->MergeValues($obj, $data); + $importer->SkipTransactions( $uid ); + + # Mark both the principal and the user object as resolved + $importer->Resolve( + $principal_uid, + ref($obj->PrincipalObj), + $obj->PrincipalObj->Id + ); + $importer->Resolve( $uid => ref($obj) => $obj->Id ); + return; + } + + # Create a principal first, so we know what ID to use + my $principal = RT::Principal->new( RT->SystemUser ); + my ($id) = $principal->Create( + PrincipalType => 'User', + Disabled => $disabled, + ObjectId => 0, + ); + + # Now we have a principal id, set the id for the user record + $data->{id} = $id; + + $importer->Resolve( $principal_uid => ref($principal), $id ); + + $importer->Postpone( + for => $uid, + uid => $principal_uid, + column => "ObjectId", + ); + + return $class->SUPER::PreInflate( $importer, $uid, $data ); +} + +sub PostInflate { + my $self = shift; + RT->InitSystemObjects if $self->Name eq "RT_System"; +} + RT::Base->_ImportOverlays();