+sub IsPassword {
+ my $self = shift;
+ my $value = shift;
+
+ #TODO there isn't any apparent way to legitimately ACL this
+
+ # RT does not allow null passwords
+ if ( ( !defined($value) ) or ( $value eq '' ) ) {
+ return (undef);
+ }
+
+ if ( $self->PrincipalObj->Disabled ) {
+ $RT::Logger->info(
+ "Disabled user " . $self->Name . " tried to log in" );
+ return (undef);
+ }
+
+ unless ($self->HasPassword) {
+ return(undef);
+ }
+
+ my $stored = $self->__Value('Password');
+ if ($stored =~ /^!/) {
+ # If it's a new-style (>= RT 4.0) password, it starts with a '!'
+ my (undef, $method, $salt, undef) = split /!/, $stored;
+ if ($method eq "sha512") {
+ return $self->_GeneratePassword_sha512($value, $salt) eq $stored;
+ } else {
+ $RT::Logger->warn("Unknown hash method $method");
+ return 0;
+ }
+ } elsif (length $stored == 40) {
+ # The truncated SHA256(salt,MD5(passwd)) form from 2010/12 is 40 characters long
+ my $hash = MIME::Base64::decode_base64($stored);
+ # Decoding yields 30 byes; first 4 are the salt, the rest are substr(SHA256,0,26)
+ my $salt = substr($hash, 0, 4, "");
+ return 0 unless substr(Digest::SHA::sha256($salt . Digest::MD5::md5($value)), 0, 26) eq $hash;
+ } elsif (length $stored == 32) {
+ # Hex nonsalted-md5
+ return 0 unless Digest::MD5::md5_hex(encode_utf8($value)) eq $stored;
+ } elsif (length $stored == 22) {
+ # Base64 nonsalted-md5
+ return 0 unless Digest::MD5::md5_base64(encode_utf8($value)) eq $stored;
+ } elsif (length $stored == 13) {
+ # crypt() output
+ return 0 unless crypt(encode_utf8($value), $stored) eq $stored;
+ } else {
+ $RT::Logger->warning("Unknown password form");
+ return 0;
+ }
+
+ # We got here by validating successfully, but with a legacy
+ # password form. Update to the most recent form.
+ my $obj = $self->isa("RT::CurrentUser") ? $self->UserObj : $self;
+ $obj->_Set(Field => 'Password', Value => $self->_GeneratePassword($value) );
+ return 1;
+}
+
+sub CurrentUserRequireToSetPassword {
+ my $self = shift;
+
+ my %res = (
+ CanSet => 1,
+ Reason => '',
+ RequireCurrent => 1,
+ );
+
+ if ( RT->Config->Get('WebExternalAuth')
+ && !RT->Config->Get('WebFallbackToInternalAuth')
+ ) {
+ $res{'CanSet'} = 0;
+ $res{'Reason'} = $self->loc("External authentication enabled.");
+ } elsif ( !$self->CurrentUser->HasPassword ) {
+ if ( $self->CurrentUser->id == ($self->id||0) ) {
+ # don't require current password if user has no
+ $res{'RequireCurrent'} = 0;
+ } else {
+ $res{'CanSet'} = 0;
+ $res{'Reason'} = $self->loc("Your password is not set.");
+ }
+ }
+
+ return %res;
+}
+
+=head3 AuthToken
+
+Returns an authentication string associated with the user. This
+string can be used to generate passwordless URLs to integrate
+RT with services and programms like callendar managers, rss
+readers and other.
+
+=cut
+
+sub AuthToken {
+ my $self = shift;
+ my $secret = $self->_Value( AuthToken => @_ );
+ return $secret if $secret;
+
+ $secret = substr(Digest::MD5::md5_hex(time . {} . rand()),0,16);
+
+ my $tmp = RT::User->new( RT->SystemUser );
+ $tmp->Load( $self->id );
+ my ($status, $msg) = $tmp->SetAuthToken( $secret );
+ unless ( $status ) {
+ $RT::Logger->error( "Couldn't set auth token: $msg" );
+ return undef;
+ }
+ return $secret;
+}
+
+=head3 GenerateAuthToken
+
+Generate a random authentication string for the user.
+
+=cut
+
+sub GenerateAuthToken {
+ my $self = shift;
+ my $token = substr(Digest::MD5::md5_hex(time . {} . rand()),0,16);
+ return $self->SetAuthToken( $token );
+}
+
+=head3 GenerateAuthString
+
+Takes a string and returns back a hex hash string. Later you can use
+this pair to make sure it's generated by this user using L</ValidateAuthString>
+
+=cut
+
+sub GenerateAuthString {
+ my $self = shift;
+ my $protect = shift;
+
+ my $str = $self->AuthToken . $protect;
+ utf8::encode($str);
+
+ return substr(Digest::MD5::md5_hex($str),0,16);
+}
+
+=head3 ValidateAuthString
+
+Takes auth string and protected string. Returns true is protected string
+has been protected by user's L</AuthToken>. See also L</GenerateAuthString>.
+
+=cut
+
+sub ValidateAuthString {
+ my $self = shift;
+ my $auth_string = shift;
+ my $protected = shift;
+
+ my $str = $self->AuthToken . $protected;
+ utf8::encode( $str );
+
+ return $auth_string eq substr(Digest::MD5::md5_hex($str),0,16);
+}
+
+=head2 SetDisabled
+
+Toggles the user's disabled flag.
+If this flag is
+set, all password checks for this user will fail. All ACL checks for this
+user will fail. The user will appear in no user listings.
+
+=cut
+
+sub SetDisabled {
+ my $self = shift;
+ my $val = shift;
+ unless ( $self->CurrentUser->HasRight(Right => 'AdminUsers', Object => $RT::System) ) {
+ return (0, $self->loc('Permission Denied'));
+ }
+
+ $RT::Handle->BeginTransaction();
+ my $set_err = $self->PrincipalObj->SetDisabled($val);
+ unless ($set_err) {
+ $RT::Handle->Rollback();
+ $RT::Logger->warning(sprintf("Couldn't %s user %s", ($val == 1) ? "disable" : "enable", $self->PrincipalObj->Id));
+ return (undef);
+ }
+ $self->_NewTransaction( Type => ($val == 1) ? "Disabled" : "Enabled" );
+
+ $RT::Handle->Commit();
+
+ if ( $val == 1 ) {
+ return (1, $self->loc("User disabled"));
+ } else {
+ return (1, $self->loc("User enabled"));
+ }
+
+}
+
+=head2 Disabled
+
+Returns true if user is disabled or false otherwise
+
+=cut
+
+sub Disabled {
+ my $self = shift;
+ return $self->PrincipalObj->Disabled(@_);
+}
+
+=head2 PrincipalObj
+
+Returns the principal object for this user. returns an empty RT::Principal
+if there's no principal object matching this user.
+The response is cached. PrincipalObj should never ever change.
+
+=cut
+
+sub PrincipalObj {
+ my $self = shift;
+
+ unless ( $self->id ) {
+ $RT::Logger->error("Couldn't get principal for an empty user");
+ return undef;
+ }
+
+ if ( !$self->{_principal_obj} ) {
+
+ my $obj = RT::Principal->new( $self->CurrentUser );
+ $obj->LoadById( $self->id );
+ if (! $obj->id ) {
+ $RT::Logger->crit( 'No principal for user #' . $self->id );
+ return undef;
+ } elsif ( $obj->PrincipalType ne 'User' ) {
+ $RT::Logger->crit( 'User #' . $self->id . ' has principal of ' . $obj->PrincipalType . ' type' );
+ return undef;
+ }
+ $self->{_principal_obj} = $obj;
+ }
+ return $self->{_principal_obj};
+}
+
+
+=head2 PrincipalId
+
+Returns this user's PrincipalId
+
+=cut
+
+sub PrincipalId {
+ my $self = shift;
+ return $self->Id;
+}
+
+=head2 HasGroupRight
+
+Takes a paramhash which can contain
+these items:
+ GroupObj => RT::Group or Group => integer
+ Right => 'Right'
+
+
+Returns 1 if this user has the right specified in the paramhash for the Group
+passed in.
+
+Returns undef if they don't.
+
+=cut
+
+sub HasGroupRight {
+ my $self = shift;
+ my %args = (
+ GroupObj => undef,
+ Group => undef,
+ Right => undef,
+ @_
+ );
+
+
+ if ( defined $args{'Group'} ) {
+ $args{'GroupObj'} = RT::Group->new( $self->CurrentUser );
+ $args{'GroupObj'}->Load( $args{'Group'} );
+ }
+
+ # Validate and load up the GroupId
+ unless ( ( defined $args{'GroupObj'} ) and ( $args{'GroupObj'}->Id ) ) {
+ return undef;
+ }
+
+ # Figure out whether a user has the right we're asking about.
+ my $retval = $self->HasRight(
+ Object => $args{'GroupObj'},
+ Right => $args{'Right'},
+ );
+
+ return ($retval);
+}
+
+=head2 OwnGroups
+
+Returns a group collection object containing the groups of which this
+user is a member.
+
+=cut
+
+sub OwnGroups {
+ my $self = shift;
+ my $groups = RT::Groups->new($self->CurrentUser);
+ $groups->LimitToUserDefinedGroups;
+ $groups->WithMember(
+ PrincipalId => $self->Id,
+ Recursively => 1
+ );
+ return $groups;
+}
+
+=head2 HasRight
+
+Shim around PrincipalObj->HasRight. See L<RT::Principal>.
+
+=cut
+
+sub HasRight {
+ my $self = shift;
+ return $self->PrincipalObj->HasRight(@_);
+}
+
+=head2 CurrentUserCanSee [FIELD]
+
+Returns true if the current user can see the user, based on if it is
+public, ourself, or we have AdminUsers
+
+=cut
+
+sub CurrentUserCanSee {
+ my $self = shift;
+ my ($what) = @_;
+
+ # 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;
+ }
+
+ # Users can see their own properties
+ elsif ( defined($self->Id) && $self->CurrentUser->Id == $self->Id ) {
+ return 1;
+ }
+
+ # 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;
+ }
+}
+
+=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 $field = shift;
+
+ if ( $self->CurrentUser->HasRight(Right => 'AdminUsers', Object => $RT::System) ) {
+ return (1);
+ }
+
+ #If the field is marked as an "administrators only" field,
+ # don't let the user touch it.
+ elsif ( $self->_Accessible( $field, 'admin' ) ) {
+ return (undef);
+ }
+
+ #If the current user is trying to modify themselves
+ elsif ( ( $self->id == $self->CurrentUser->id )
+ and ( $self->CurrentUser->HasRight(Right => 'ModifySelf', Object => $RT::System) ) )
+ {
+ return (1);
+ }
+
+ #If we don't have a good reason to grant them rights to modify
+ # by now, they lose
+ else {
+ return (undef);
+ }
+
+}
+
+=head2 CurrentUserHasRight
+
+Takes a single argument. returns 1 if $Self->CurrentUser
+has the requested right. returns undef otherwise
+
+=cut
+
+sub CurrentUserHasRight {
+ my $self = shift;
+ my $right = shift;
+
+ return ( $self->CurrentUser->HasRight(Right => $right, Object => $RT::System) );
+}
+
+sub _PrefName {
+ my $name = shift;
+ if (ref $name) {
+ $name = ref($name).'-'.$name->Id;
+ }
+
+ return 'Pref-'.$name;
+}
+
+=head2 Preferences NAME/OBJ DEFAULT
+
+Obtain user preferences associated with given object or name.
+Returns DEFAULT if no preferences found. If DEFAULT is a hashref,
+override the entries with user preferences.
+
+=cut
+
+sub Preferences {
+ my $self = shift;
+ my $name = _PrefName (shift);
+ my $default = shift;
+
+ my $attr = RT::Attribute->new( $self->CurrentUser );
+ $attr->LoadByNameAndObject( Object => $self, Name => $name );
+
+ my $content = $attr->Id ? $attr->Content : undef;
+ unless ( ref $content eq 'HASH' ) {
+ return defined $content ? $content : $default;
+ }
+
+ if (ref $default eq 'HASH') {
+ for (keys %$default) {
+ exists $content->{$_} or $content->{$_} = $default->{$_};
+ }
+ } elsif (defined $default) {
+ $RT::Logger->error("Preferences $name for user".$self->Id." is hash but default is not");
+ }
+ return $content;
+}
+
+=head2 SetPreferences NAME/OBJ VALUE
+
+Set user preferences associated with given object or name.
+
+=cut
+
+sub SetPreferences {
+ my $self = shift;
+ my $name = _PrefName( shift );
+ my $value = shift;
+
+ return (0, $self->loc("No permission to set preferences"))
+ unless $self->CurrentUserCanModify('Preferences');
+
+ my $attr = RT::Attribute->new( $self->CurrentUser );
+ $attr->LoadByNameAndObject( Object => $self, Name => $name );
+ if ( $attr->Id ) {
+ my ($ok, $msg) = $attr->SetContent( $value );
+ return (1, "No updates made")
+ if $msg eq "That is already the current value";
+ return ($ok, $msg);
+ } else {
+ return $self->AddAttribute( Name => $name, Content => $value );
+ }
+}
+
+=head2 Stylesheet
+
+Returns a list of valid stylesheets take from preferences.
+
+=cut
+
+sub Stylesheet {
+ my $self = shift;
+
+ my $style = RT->Config->Get('WebDefaultStylesheet', $self->CurrentUser);
+
+ if (RT::Interface::Web->ComponentPathIsSafe($style)) {
+ my @css_paths = map { $_ . '/NoAuth/css' } RT::Interface::Web->ComponentRoots;
+
+ for my $css_path (@css_paths) {
+ if (-d "$css_path/$style") {
+ return $style
+ }
+ }
+ }
+
+ # Fall back to the system stylesheet.
+ return RT->Config->Get('WebDefaultStylesheet');
+}
+
+=head2 WatchedQueues ROLE_LIST
+
+Returns a RT::Queues object containing every queue watched by the user.
+
+Takes a list of roles which is some subset of ('Cc', 'AdminCc'). Defaults to:
+
+$user->WatchedQueues('Cc', 'AdminCc');
+
+=cut
+
+sub WatchedQueues {
+
+ my $self = shift;
+ my @roles = @_ || ('Cc', 'AdminCc');
+
+ $RT::Logger->debug('WatcheQueues got user ' . $self->Name);
+
+ my $watched_queues = RT::Queues->new($self->CurrentUser);
+
+ my $group_alias = $watched_queues->Join(
+ ALIAS1 => 'main',
+ FIELD1 => 'id',
+ TABLE2 => 'Groups',
+ FIELD2 => 'Instance',
+ );
+
+ $watched_queues->Limit(
+ ALIAS => $group_alias,
+ FIELD => 'Domain',
+ VALUE => 'RT::Queue-Role',
+ ENTRYAGGREGATOR => 'AND',
+ );
+ if (grep { $_ eq 'Cc' } @roles) {
+ $watched_queues->Limit(
+ SUBCLAUSE => 'LimitToWatchers',
+ ALIAS => $group_alias,
+ FIELD => 'Type',
+ VALUE => 'Cc',
+ ENTRYAGGREGATOR => 'OR',
+ );
+ }
+ if (grep { $_ eq 'AdminCc' } @roles) {
+ $watched_queues->Limit(
+ SUBCLAUSE => 'LimitToWatchers',
+ ALIAS => $group_alias,
+ FIELD => 'Type',
+ VALUE => 'AdminCc',
+ ENTRYAGGREGATOR => 'OR',
+ );
+ }
+
+ my $queues_alias = $watched_queues->Join(
+ ALIAS1 => $group_alias,
+ FIELD1 => 'id',
+ TABLE2 => 'CachedGroupMembers',
+ FIELD2 => 'GroupId',
+ );
+ $watched_queues->Limit(
+ ALIAS => $queues_alias,
+ FIELD => 'MemberId',
+ VALUE => $self->PrincipalId,
+ );
+ $watched_queues->Limit(
+ ALIAS => $queues_alias,
+ FIELD => 'Disabled',
+ VALUE => 0,
+ );
+
+
+ $RT::Logger->debug("WatchedQueues got " . $watched_queues->Count . " queues");
+
+ return $watched_queues;
+
+}
+
+sub _Set {
+ my $self = shift;
+
+ my %args = (
+ Field => undef,
+ Value => undef,
+ TransactionType => 'Set',
+ RecordTransaction => 1,
+ @_
+ );
+
+ # Nobody is allowed to futz with RT_System or Nobody
+
+ if ( ($self->Id == RT->SystemUser->Id ) ||
+ ($self->Id == RT->Nobody->Id)) {
+ return ( 0, $self->loc("Can not modify system users") );
+ }
+ unless ( $self->CurrentUserCanModify( $args{'Field'} ) ) {
+ return ( 0, $self->loc("Permission Denied") );
+ }
+
+ my $Old = $self->SUPER::_Value("$args{'Field'}");
+
+ my ($ret, $msg) = $self->SUPER::_Set( Field => $args{'Field'},
+ Value => $args{'Value'} );
+
+ #If we can't actually set the field to the value, don't record
+ # a transaction. instead, get out of here.
+ if ( $ret == 0 ) { return ( 0, $msg ); }
+
+ if ( $args{'RecordTransaction'} == 1 ) {
+ if ($args{'Field'} eq "Password") {
+ $args{'Value'} = $Old = '********';
+ }
+ my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
+ Type => $args{'TransactionType'},
+ Field => $args{'Field'},
+ NewValue => $args{'Value'},
+ OldValue => $Old,
+ TimeTaken => $args{'TimeTaken'},
+ );
+ return ( $Trans, scalar $TransObj->BriefDescription );
+ } else {
+ return ( $ret, $msg );
+ }
+}
+
+=head2 _Value
+
+Takes the name of a table column.
+Returns its value as a string, if the user passes an ACL check
+
+=cut
+
+sub _Value {
+
+ my $self = shift;
+ my $field = shift;
+
+ # Defer to the abstraction above to know if the field can be read
+ return $self->SUPER::_Value($field) if $self->CurrentUserCanSee($field);
+ return undef;
+}
+
+=head2 FriendlyName
+
+Return the friendly name
+
+=cut
+
+sub FriendlyName {
+ my $self = shift;
+ return $self->RealName if defined($self->RealName);
+ return $self->Name if defined($self->Name);
+ return "";
+}
+
+=head2 PreferredKey
+
+Returns the preferred key of the user. If none is set, then this will query
+GPG and set the preferred key to the maximally trusted key found (and then
+return it). Returns C<undef> if no preferred key can be found.
+
+=cut
+
+sub PreferredKey
+{
+ my $self = shift;
+ return undef unless RT->Config->Get('GnuPG')->{'Enable'};
+
+ if ( ($self->CurrentUser->Id != $self->Id ) &&
+ !$self->CurrentUser->HasRight(Right =>'AdminUsers', Object => $RT::System) ) {
+ return undef;
+ }
+
+
+
+ my $prefkey = $self->FirstAttribute('PreferredKey');
+ return $prefkey->Content if $prefkey;
+
+ # we don't have a preferred key for this user, so now we must query GPG
+ require RT::Crypt::GnuPG;
+ my %res = RT::Crypt::GnuPG::GetKeysForEncryption($self->EmailAddress);
+ return undef unless defined $res{'info'};
+ my @keys = @{ $res{'info'} };
+ return undef if @keys == 0;
+
+ if (@keys == 1) {
+ $prefkey = $keys[0]->{'Fingerprint'};
+ } else {
+ # prefer the maximally trusted key
+ @keys = sort { $b->{'TrustLevel'} <=> $a->{'TrustLevel'} } @keys;
+ $prefkey = $keys[0]->{'Fingerprint'};
+ }
+
+ $self->SetAttribute(Name => 'PreferredKey', Content => $prefkey);
+ return $prefkey;
+}
+
+sub PrivateKey {
+ my $self = shift;
+
+
+ #If the user wants to see their own values, let them.
+ #If the user is an admin, let them.
+ #Otherwwise, don't let them.
+ #
+ if ( ($self->CurrentUser->Id != $self->Id ) &&
+ !$self->CurrentUser->HasRight(Right =>'AdminUsers', Object => $RT::System) ) {
+ return undef;
+ }
+
+ my $key = $self->FirstAttribute('PrivateKey') or return undef;
+ return $key->Content;
+}
+
+sub SetPrivateKey {
+ my $self = shift;
+ my $key = shift;
+
+ unless ($self->CurrentUserCanModify('PrivateKey')) {
+ return (0, $self->loc("Permission Denied"));
+ }
+
+ unless ( $key ) {
+ my ($status, $msg) = $self->DeleteAttribute('PrivateKey');
+ unless ( $status ) {
+ $RT::Logger->error( "Couldn't delete attribute: $msg" );
+ return ($status, $self->loc("Couldn't unset private key"));
+ }
+ return ($status, $self->loc("Unset private key"));
+ }
+
+ # check that it's really private key
+ {
+ my %tmp = RT::Crypt::GnuPG::GetKeysForSigning( $key );
+ return (0, $self->loc("No such key or it's not suitable for signing"))
+ if $tmp{'exit_code'} || !$tmp{'info'};
+ }
+
+ my ($status, $msg) = $self->SetAttribute(
+ Name => 'PrivateKey',
+ Content => $key,
+ );
+ return ($status, $self->loc("Couldn't set private key"))
+ unless $status;
+ return ($status, $self->loc("Set private key"));
+}
+
+sub BasicColumns {
+ (
+ [ Name => 'Username' ],
+ [ EmailAddress => 'Email' ],
+ [ RealName => 'Name' ],
+ [ Organization => 'Organization' ],
+ );
+}
+
+=head2 Create PARAMHASH
+
+Create takes a hash of values and creates a row in the database:
+
+ varchar(200) 'Name'.
+ varbinary(256) 'Password'.
+ varchar(16) 'AuthToken'.
+ text 'Comments'.
+ text 'Signature'.
+ varchar(120) 'EmailAddress'.
+ text '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
+
+
+
+
+=head2 id
+
+Returns the current value of id.
+(In the database, id is stored as int(11).)
+
+
+=cut
+
+
+=head2 Name
+
+Returns the current value of Name.
+(In the database, Name is stored as varchar(200).)
+
+
+
+=head2 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
+
+
+=head2 Password
+
+Returns the current value of Password.
+(In the database, Password is stored as varchar(256).)
+
+
+
+=head2 SetPassword VALUE
+
+
+Set Password to VALUE.
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, Password will be stored as a varchar(256).)
+
+
+=cut
+
+
+=head2 AuthToken
+
+Returns the current value of AuthToken.
+(In the database, AuthToken is stored as varchar(16).)
+
+
+
+=head2 SetAuthToken VALUE
+
+
+Set AuthToken to VALUE.
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, AuthToken will be stored as a varchar(16).)
+
+
+=cut
+
+
+=head2 Comments
+
+Returns the current value of Comments.
+(In the database, Comments is stored as text.)
+
+
+
+=head2 SetComments VALUE
+
+
+Set Comments to VALUE.
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, Comments will be stored as a text.)
+
+
+=cut
+
+
+=head2 Signature
+
+Returns the current value of Signature.
+(In the database, Signature is stored as text.)
+
+
+
+=head2 SetSignature VALUE
+
+
+Set Signature to VALUE.
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, Signature will be stored as a text.)
+
+
+=cut
+
+
+=head2 EmailAddress
+
+Returns the current value of EmailAddress.
+(In the database, EmailAddress is stored as varchar(120).)
+
+
+
+=head2 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
+
+
+=head2 FreeformContactInfo
+
+Returns the current value of FreeformContactInfo.
+(In the database, FreeformContactInfo is stored as text.)
+
+
+
+=head2 SetFreeformContactInfo VALUE
+
+
+Set FreeformContactInfo to VALUE.
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, FreeformContactInfo will be stored as a text.)
+
+
+=cut
+
+
+=head2 Organization
+
+Returns the current value of Organization.
+(In the database, Organization is stored as varchar(200).)
+
+
+
+=head2 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
+
+
+=head2 RealName
+
+Returns the current value of RealName.
+(In the database, RealName is stored as varchar(120).)
+
+
+
+=head2 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
+
+
+=head2 NickName
+
+Returns the current value of NickName.
+(In the database, NickName is stored as varchar(16).)
+
+
+
+=head2 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
+
+
+=head2 Lang
+
+Returns the current value of Lang.
+(In the database, Lang is stored as varchar(16).)
+
+
+
+=head2 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
+
+
+=head2 EmailEncoding
+
+Returns the current value of EmailEncoding.
+(In the database, EmailEncoding is stored as varchar(16).)
+
+
+
+=head2 SetEmailEncoding VALUE
+
+
+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).)
+
+
+=cut
+
+
+=head2 WebEncoding
+
+Returns the current value of WebEncoding.
+(In the database, WebEncoding is stored as varchar(16).)
+
+
+
+=head2 SetWebEncoding VALUE
+
+
+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).)
+
+
+=cut
+
+
+=head2 ExternalContactInfoId
+
+Returns the current value of ExternalContactInfoId.
+(In the database, ExternalContactInfoId is stored as varchar(100).)
+
+
+
+=head2 SetExternalContactInfoId VALUE
+
+
+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).)
+
+
+=cut
+
+
+=head2 ContactInfoSystem
+
+Returns the current value of ContactInfoSystem.
+(In the database, ContactInfoSystem is stored as varchar(30).)
+
+
+
+=head2 SetContactInfoSystem VALUE
+
+
+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).)
+
+
+=cut
+
+
+=head2 ExternalAuthId
+
+Returns the current value of ExternalAuthId.
+(In the database, ExternalAuthId is stored as varchar(100).)
+
+
+
+=head2 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
+
+
+=head2 AuthSystem
+
+Returns the current value of AuthSystem.
+(In the database, AuthSystem is stored as varchar(30).)
+
+
+
+=head2 SetAuthSystem VALUE
+
+
+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).)
+
+
+=cut
+
+
+=head2 Gecos
+
+Returns the current value of Gecos.
+(In the database, Gecos is stored as varchar(16).)
+
+
+
+=head2 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
+
+
+=head2 HomePhone
+
+Returns the current value of HomePhone.
+(In the database, HomePhone is stored as varchar(30).)
+
+
+
+=head2 SetHomePhone VALUE
+
+
+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).)
+
+
+=cut
+
+
+=head2 WorkPhone
+
+Returns the current value of WorkPhone.
+(In the database, WorkPhone is stored as varchar(30).)
+
+
+
+=head2 SetWorkPhone VALUE
+
+
+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).)
+
+
+=cut
+
+
+=head2 MobilePhone
+
+Returns the current value of MobilePhone.
+(In the database, MobilePhone is stored as varchar(30).)
+
+
+
+=head2 SetMobilePhone VALUE
+
+
+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).)
+
+
+=cut
+
+
+=head2 PagerPhone
+
+Returns the current value of PagerPhone.
+(In the database, PagerPhone is stored as varchar(30).)
+
+
+
+=head2 SetPagerPhone VALUE
+
+
+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).)
+
+
+=cut
+
+
+=head2 Address1
+
+Returns the current value of Address1.
+(In the database, Address1 is stored as varchar(200).)
+
+
+
+=head2 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
+
+
+=head2 Address2
+
+Returns the current value of Address2.
+(In the database, Address2 is stored as varchar(200).)
+
+
+
+=head2 SetAddress2 VALUE
+
+
+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).)
+
+
+=cut
+
+
+=head2 City
+
+Returns the current value of City.
+(In the database, City is stored as varchar(100).)
+
+
+
+=head2 SetCity VALUE
+
+
+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).)
+
+
+=cut
+
+
+=head2 State
+
+Returns the current value of State.
+(In the database, State is stored as varchar(100).)
+
+
+
+=head2 SetState VALUE
+
+
+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).)
+
+
+=cut
+
+
+=head2 Zip
+
+Returns the current value of Zip.
+(In the database, Zip is stored as varchar(16).)
+
+
+
+=head2 SetZip VALUE
+
+
+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).)
+
+
+=cut
+
+
+=head2 Country
+
+Returns the current value of Country.
+(In the database, Country is stored as varchar(50).)
+
+
+
+=head2 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
+
+
+=head2 Timezone
+
+Returns the current value of Timezone.
+(In the database, Timezone is stored as varchar(50).)
+
+
+
+=head2 SetTimezone VALUE
+
+
+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).)
+
+
+=cut
+
+
+=head2 PGPKey
+
+Returns the current value of PGPKey.
+(In the database, PGPKey is stored as text.)
+
+
+
+=head2 SetPGPKey VALUE
+
+
+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.)
+
+
+=cut
+
+
+=head2 Creator
+
+Returns the current value of Creator.
+(In the database, Creator is stored as int(11).)
+
+
+=cut
+
+
+=head2 Created
+
+Returns the current value of Created.
+(In the database, Created is stored as datetime.)
+
+
+=cut
+
+
+=head2 LastUpdatedBy
+
+Returns the current value of LastUpdatedBy.
+(In the database, LastUpdatedBy is stored as int(11).)
+
+
+=cut
+
+
+=head2 LastUpdated
+
+Returns the current value of LastUpdated.
+(In the database, LastUpdated is stored as datetime.)
+
+
+=cut
+
+
+# much false laziness w/Ticket.pm. now with RT 4!
+our %LINKDIRMAP = (
+ MemberOf => { Base => 'MemberOf',
+ Target => 'HasMember', },
+ RefersTo => { Base => 'RefersTo',
+ Target => 'ReferredToBy', },
+ DependsOn => { Base => 'DependsOn',
+ Target => 'DependedOnBy', },
+ MergedInto => { Base => 'MergedInto',
+ Target => 'MergedInto', },
+
+);
+
+sub LINKDIRMAP { return \%LINKDIRMAP }
+
+
+=head2 DeleteLink
+
+Delete a link. takes a paramhash of Base, Target and Type.
+Either Base or Target must be null. The null value will
+be replaced with this ticket\'s id
+
+=cut
+
+sub DeleteLink {
+ my $self = shift;
+ my %args = (
+ Base => undef,
+ Target => undef,
+ Type => undef,
+ @_
+ );
+
+ unless ( $args{'Target'} || $args{'Base'} ) {
+ $RT::Logger->error("Base or Target must be specified\n");
+ return ( 0, $self->loc('Either base or target must be specified') );
+ }
+
+ #check acls
+ my $right = 0;
+ $right++ if $self->CurrentUserHasRight('AdminUsers');
+ if ( !$right && $RT::StrictLinkACL ) {
+ return ( 0, $self->loc("Permission Denied") );
+ }
+
+# # If the other URI is an RT::Ticket, we want to make sure the user
+# # can modify it too...
+# my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
+# return (0, $msg) unless $status;
+# if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
+# $right++;
+# }
+# if ( ( !$RT::StrictLinkACL && $right == 0 ) ||
+# ( $RT::StrictLinkACL && $right < 2 ) )
+# {
+# return ( 0, $self->loc("Permission Denied") );
+# }
+
+ my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
+
+ if ( !$val ) {
+ $RT::Logger->debug("Couldn't find that link\n");
+ return ( 0, $Msg );
+ }
+
+ my ($direction, $remote_link);
+
+ if ( $args{'Base'} ) {
+ $remote_link = $args{'Base'};
+ $direction = 'Target';
+ }
+ elsif ( $args{'Target'} ) {
+ $remote_link = $args{'Target'};
+ $direction='Base';
+ }
+
+ if ( $args{'Silent'} ) {
+ return ( $val, $Msg );
+ }
+ else {
+ my $remote_uri = RT::URI->new( $self->CurrentUser );
+ $remote_uri->FromURI( $remote_link );
+
+ my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
+ Type => 'DeleteLink',
+ Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
+ OldValue => $remote_uri->URI || $remote_link,
+ TimeTaken => 0
+ );
+
+ if ( $remote_uri->IsLocal ) {
+
+ my $OtherObj = $remote_uri->Object;
+ my ( $val, $Msg ) = $OtherObj->_NewTransaction(Type => 'DeleteLink',
+ Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
+ : $LINKDIRMAP{$args{'Type'}}->{Target},
+ OldValue => $self->URI,
+ ActivateScrips => ! $RT::LinkTransactionsRun1Scrip,
+ TimeTaken => 0 );
+ }
+
+ return ( $Trans, $Msg );
+ }
+}
+
+sub AddLink {
+ my $self = shift;
+ my %args = ( Target => '',
+ Base => '',
+ Type => '',
+ Silent => undef,
+ @_ );
+
+ unless ( $args{'Target'} || $args{'Base'} ) {
+ $RT::Logger->error("Base or Target must be specified\n");
+ return ( 0, $self->loc('Either base or target must be specified') );
+ }
+
+ my $right = 0;
+ $right++ if $self->CurrentUserHasRight('AdminUsers');
+ if ( !$right && $RT::StrictLinkACL ) {
+ return ( 0, $self->loc("Permission Denied") );
+ }
+
+# # If the other URI is an RT::Ticket, we want to make sure the user
+# # can modify it too...
+# my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
+# return (0, $msg) unless $status;
+# if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
+# $right++;
+# }
+# if ( ( !$RT::StrictLinkACL && $right == 0 ) ||
+# ( $RT::StrictLinkACL && $right < 2 ) )
+# {
+# return ( 0, $self->loc("Permission Denied") );
+# }
+
+ return $self->_AddLink(%args);
+}
+
+=head2 _AddLink
+
+Private non-acled variant of AddLink so that links can be added during create.
+
+=cut
+
+sub _AddLink {
+ my $self = shift;
+ my %args = ( Target => '',
+ Base => '',
+ Type => '',
+ Silent => undef,
+ @_ );
+
+ my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
+ return ($val, $msg) if !$val || $exist;
+
+ my ($direction, $remote_link);
+ if ( $args{'Target'} ) {
+ $remote_link = $args{'Target'};
+ $direction = 'Base';
+ } elsif ( $args{'Base'} ) {
+ $remote_link = $args{'Base'};
+ $direction = 'Target';
+ }
+
+ # Don't write the transaction if we're doing this on create
+ if ( $args{'Silent'} ) {
+ return ( $val, $msg );
+ }
+ else {
+ my $remote_uri = RT::URI->new( $self->CurrentUser );
+ $remote_uri->FromURI( $remote_link );
+
+ #Write the transaction
+ my ( $Trans, $Msg, $TransObj ) =
+ $self->_NewTransaction(Type => 'AddLink',
+ Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
+ NewValue => $remote_uri->URI || $remote_link,
+ TimeTaken => 0 );
+
+ if ( $remote_uri->IsLocal ) {
+
+ my $OtherObj = $remote_uri->Object;
+ my ( $val, $Msg ) = $OtherObj->_NewTransaction(Type => 'AddLink',
+ Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
+ : $LINKDIRMAP{$args{'Type'}}->{Target},
+ NewValue => $self->URI,
+ ActivateScrips => ! $RT::LinkTransactionsRun1Scrip,
+ TimeTaken => 0 );
+ }
+ return ( $val, $Msg );
+ }