[Rt-commit] rt branch, 4.0-trunk, updated. rt-4.0.6-125-g2931b56

Alex Vandiver alexmv at bestpractical.com
Tue May 22 13:30:53 EDT 2012


The branch, 4.0-trunk has been updated
       via  2931b56e73c55412fbd5c90f3b6a527e1a3b9c93 (commit)
       via  8c6406d12958a0c6db67676e00522ccfd9f7824b (commit)
       via  488f351cb105ef21f6952b14fb8ec1a1aa630967 (commit)
       via  42503976eced9fa0d71fe56e792758f876d6e491 (commit)
       via  14b8b16c3036ae9a46725cefece768e929a15a4e (commit)
       via  a5346d0ba4122e989d771a093de79e8b3bdd3024 (commit)
       via  65d63b0a9b36b81044d7b164606a415f65876c9d (commit)
       via  1286ac125218937c2fe1ea50e874aaa384090774 (commit)
       via  cd32279d153f4d61a677562f7c96b208157d7a7d (commit)
       via  809ea27ee626d2cd00c0635a73a5e4a55f01e423 (commit)
       via  fec1b72e821c2c9d28996eaec0ca21a7be9cf4e7 (commit)
       via  997d5b0fc029ff0b209ec7e47f253958be334d8b (commit)
       via  001014997bd8a5c21cbb36eb50505bea14456f1b (commit)
       via  ff3a3e187d64deac5930877fee8527d32f59406a (commit)
       via  eada947f53da77f93e91aa27dd0fd30c144a3c5e (commit)
       via  dd11af8915ce12942ffd7c08607a847d1967be47 (commit)
       via  912e2385a38cce818244bbe4197897b190217d1b (commit)
       via  07890edf334ce7a238fb65af0ab4689566bff027 (commit)
       via  542b80d1fff77dd14e65fbb494eba5118cbb26a6 (commit)
       via  6b4e33882d0eac0c8ea5b416b4edd692bdd69e71 (commit)
       via  52159bcadff35afb38fbf0ed749f32f213cf537d (commit)
       via  ad3ab788779fb1f8047bfe190d1d7c439d615c01 (commit)
       via  5daf828da793b49df4aa06cce03df9ee9fbcffca (commit)
       via  96cde5748cd553256ba6def8a8353d5a6baf054b (commit)
       via  5ad63908f7e589534d41d93bf68fd64c3817f156 (commit)
       via  3f5531887c5934995688434cfdff752c27573c23 (commit)
       via  1d3432b2a14434c775a2bf637e7ec2bde4448bd5 (commit)
       via  eb44f1060ac3e78a5063aa6982c033d7bbf783aa (commit)
       via  02325246190c18f11b1f4056d7c6e7c3fa1f6a9b (commit)
       via  9b6e230856538dc8f3801a21f2261fe93a4f493b (commit)
       via  bf574b144f70e287d5750dc70735f93a4bfb69e5 (commit)
       via  b6a06ebea3dac5915979055f0a0508a846829033 (commit)
       via  f4513aee9e19bef089b5aa0586b033e291b8c509 (commit)
       via  7040ff301c762d7b30335f3808b06b4ebfe3523b (commit)
       via  c01f5852cc56e056198a9bb6110842d7553856a8 (commit)
       via  0597e05c4e1ea5d954c4cd9ec60c909464571380 (commit)
       via  013ee73c2444201435755e924f195fdcfdbb8249 (commit)
       via  9bf2265ce80bdb979e5c2b0c90263792fd302d42 (commit)
       via  b3d3b2f30b574a961b9a9fcccae66c34da4a5eb5 (commit)
       via  3a705a092526cd2106bb88fce134e06f855e52ca (commit)
       via  e915622b841a522d5595e53e2d38ff404e8e17e8 (commit)
       via  5aa4f5ceab5ef0bf5263e8b3a7bcd9b0e86c27c7 (commit)
       via  f8770f8538f28113989d067fdf62b08b0b121727 (commit)
       via  be375067d28f2fa10f112da4a51f8b87b787f07c (commit)
       via  0d977a01e9524922c58ac31e345c2696e91efc26 (commit)
       via  aa6923451b824192828867b388e49dd46971c13d (commit)
       via  baa7f1e1bf952194fd39ea95884184a756039c23 (commit)
       via  e5399ceb1ae24cea0e18800a004ac1ac8d3539f8 (commit)
       via  290b46b20d2d2fb84fea2a707e51cc049617469a (commit)
       via  56f24489b5f7a43015c528dd305f775e49911e79 (commit)
       via  05a6e45a448b0f2712a2356829ef78a1e7385d60 (commit)
       via  cd180c1f57602555614ef0d57e128f1cad544e87 (commit)
       via  87ac5328d5a13f8e99e2ca7783e28d97c15912cc (commit)
       via  82c9189f529dc65c1874a15e5379f5f9d11593f7 (commit)
       via  6a9a41d6dc2908c34c80333bb507457aec058e7d (commit)
       via  1ef24b15d3fc42131bf29f888279de55a9fd01a0 (commit)
       via  9feb75b8ee903273c6f708e6d52ab10ae3774b64 (commit)
       via  02adabdd6ca2c5df6ee3e13e742b38934cc89447 (commit)
       via  ee8717368f42f083cfd900170201f7a3d73e2f35 (commit)
       via  8fbe5b518a3898229de7f7717231d175d4d33e6f (commit)
       via  6928fac63094935a68438f0d6608cbc351c37cb4 (commit)
       via  c83b3488e33eba887ae20a6f192f2c5dc4311d01 (commit)
       via  64c6ecd431388d7c81c5f94ee4f0c526325ba9c0 (commit)
       via  19369ba8f67eec572a992f4bdb22d756872ccc37 (commit)
       via  ad5d6ed2d2b80fe2426c36d40b70dd6cf2264a6b (commit)
       via  cdcc2b65b2b361b362bc0fa86e9dc6f60fd65784 (commit)
       via  e2233e032012c4286d4afecfd0f4d84da497f97b (commit)
       via  0b5f3d82e2ee5721208685fe6d2ede4e0ebdaf29 (commit)
       via  80b14f90de3eed0a64f9318850c515fb855d2261 (commit)
       via  a325ac0d049ef4a0e58c8744ae6c61fa193c800f (commit)
       via  ad312089ab65778fafe6b625f2b796a5b79da843 (commit)
       via  3570e453da31c9cc29ef32aff4c10df5987eeb27 (commit)
       via  f923dbce924c5f3bfc1fc27560fcffe924f07b1c (commit)
       via  7c9cd7c92f7672bcf6b100aa2913d6d0e0e33753 (commit)
       via  4c5657837b3b5972e0a85da0607ff20bfb72892b (commit)
       via  189b322aa22fa68d45a52504fa6f32ab0e1a2b57 (commit)
       via  951add5ab10a12b0d40cd3a7edf812b524db6ff9 (commit)
       via  d17991d80002d65b2d9e98366a550d49ad5232c7 (commit)
       via  cfd4d893e92e4fe23615d4cd4724803c0a0804cf (commit)
       via  01cecdeeca3375402ee29e92683100f6b24e139d (commit)
       via  5170d9057c4060a8a9be422f947ad450d5db100e (commit)
       via  b784bcb5779ca6315717a5bbb9c554f0a28ecb6b (commit)
       via  00593b893332714d7288ab683e270003471e35a8 (commit)
       via  cccfb9c04fa271ff05128749ce99713e50b2da23 (commit)
       via  d0a37b0ee1bc38f7eda0ae0f155c52fef5996f73 (commit)
       via  90650ebd9e5316b3a8f6b6b8992a1b810f8db09b (commit)
       via  1f71f5df36140e2239ae82bebfc7237eab34dc0f (commit)
       via  bb917e0b5a3a5797cc0929211db808e6d9303f9a (commit)
       via  f96ce669d98ce016f2340fd2286fd14dd6edc80b (commit)
       via  8283903fdb1984c9d04722a8e0f9539e00ebf53f (commit)
       via  d9c47864b8068e8f524118a8a698f23eb0523c8d (commit)
       via  40be851ec9b9ecdb48cc9bf250a2832de8ddf1d0 (commit)
       via  9243676f7ffbfe9d6b2c614ca604a515893a8e54 (commit)
       via  d2d451591a7622e96cf3052e591029cdaa890419 (commit)
       via  26a2816316b4883529f9d22175b8be2cd58271ff (commit)
       via  e8268e46e5de3529370c4bf23256ee3331595485 (commit)
       via  adc0df31fa1427d596294ab61f6b81e8f7d9033f (commit)
       via  efd243054470431d4e6297630b712e01a02bbef0 (commit)
       via  1a7fa43df96202be810ff0d1fa05fca6ac53f648 (commit)
       via  52c4d4c0fe723869a94c3a3292c17238ed83c14f (commit)
       via  488cdbd10f68e4d8b5c52934268de3c65d7e0a57 (commit)
       via  eaa7ba63829b129d2ddae95983ff81883d149bb9 (commit)
       via  329e14ec5f581c7e9a490ca3a1b4b1f204cdd419 (commit)
       via  02ff6818763e3c5c1ccc7d1fe3854a25cda74a50 (commit)
       via  71007612cf3ab9409ab250f0e53b21be86c75780 (commit)
       via  864c8193f45fb2733f3e6a148ec1aae7d95d155f (commit)
       via  31d1728a9034e1cbcd394449a292a248cad0126e (commit)
       via  fa582274cdd3619063e383fbb712783605b7ed59 (commit)
       via  5acf18bf53605676b61a031ea4b4320146f00c42 (commit)
       via  4b065939ff0737b37930c1fdee225658a47e92fc (commit)
       via  ef35fe55305d97233f74dd75720f15d385a3432f (commit)
       via  147c38f3740ef4be7c37a74982aaa1505145e59e (commit)
       via  3ddacb63683ed572ff2f1d369974bd4b3fb8d6c6 (commit)
       via  0abb5479c11e85c5cbc9e4046a6b678bcce723ab (commit)
       via  18ef6a5a2d7f993d1902d65bf72ed0a04e984c2b (commit)
       via  1fb42a2cbdb2b461ebc99e7c4f04734d760320a0 (commit)
       via  cda75b2b959fa89a4b23ef5e4e834bb93342ffe6 (commit)
       via  18b1c0c1f30101f99cd6739b52261c6c2f7ce404 (commit)
       via  779e7139fe27d7a22ae08dbd9419ff85d3992acc (commit)
       via  821a1998125e527cad281907e0f719d3318261bd (commit)
       via  7958c77aef51e1d06d1ec615772d400a602e3dec (commit)
       via  0c3bd92fe2b61a67a41842a94214f65a9d2a02ca (commit)
       via  18eef7274225ee97a7ea8170f95fff19b51aa3f8 (commit)
      from  153a17f9e898a744eeec45c983cdccf0055b22ea (commit)

Summary of changes:
 lib/RT/Ticket.pm    |    2 +-
 lib/RT/User.pm      |    2 +-
 t/web/attachments.t |   11 ++++++++++-
 3 files changed, 12 insertions(+), 3 deletions(-)

- Log -----------------------------------------------------------------
commit 2931b56e73c55412fbd5c90f3b6a527e1a3b9c93
Merge: 153a17f 8c6406d
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Tue May 22 13:05:29 2012 -0400

    Merge branch '3.8-trunk' into 4.0-trunk

diff --cc lib/RT/Ticket.pm
index 76b2e19,edf38f0..4b84843
mode 100644,100755..100644
--- a/lib/RT/Ticket.pm
+++ b/lib/RT/Ticket.pm
@@@ -46,3000 -46,25 +46,3000 @@@
  #
  # END BPS TAGGED BLOCK }}}
  
 -# Autogenerated by DBIx::SearchBuilder factory (by <jesse at bestpractical.com>)
 -# WARNING: THIS FILE IS AUTOGENERATED. ALL CHANGES TO THIS FILE WILL BE LOST.  
 -# 
 -# !! DO NOT EDIT THIS FILE !!
 -#
 +=head1 SYNOPSIS
 +
 +  use RT::Ticket;
 +  my $ticket = RT::Ticket->new($CurrentUser);
 +  $ticket->Load($ticket_id);
 +
 +=head1 DESCRIPTION
 +
 +This module lets you manipulate RT\'s ticket object.
 +
 +
 +=head1 METHODS
 +
 +
 +=cut
 +
 +
 +package RT::Ticket;
 +
 +use strict;
 +use warnings;
 +
 +
 +use RT::Queue;
 +use RT::User;
 +use RT::Record;
 +use RT::Links;
 +use RT::Date;
 +use RT::CustomFields;
 +use RT::Tickets;
 +use RT::Transactions;
 +use RT::Reminders;
 +use RT::URI::fsck_com_rt;
 +use RT::URI;
 +use MIME::Entity;
 +use Devel::GlobalDestruction;
 +
 +
 +# A helper table for links mapping to make it easier
 +# to build and parse links between tickets
 +
 +our %LINKTYPEMAP = (
 +    MemberOf => { Type => 'MemberOf',
 +                  Mode => 'Target', },
 +    Parents => { Type => 'MemberOf',
 +         Mode => 'Target', },
 +    Members => { Type => 'MemberOf',
 +                 Mode => 'Base', },
 +    Children => { Type => 'MemberOf',
 +          Mode => 'Base', },
 +    HasMember => { Type => 'MemberOf',
 +                   Mode => 'Base', },
 +    RefersTo => { Type => 'RefersTo',
 +                  Mode => 'Target', },
 +    ReferredToBy => { Type => 'RefersTo',
 +                      Mode => 'Base', },
 +    DependsOn => { Type => 'DependsOn',
 +                   Mode => 'Target', },
 +    DependedOnBy => { Type => 'DependsOn',
 +                      Mode => 'Base', },
 +    MergedInto => { Type => 'MergedInto',
 +                   Mode => 'Target', },
 +
 +);
 +
 +
 +# A helper table for links mapping to make it easier
 +# to build and parse links between tickets
 +
 +our %LINKDIRMAP = (
 +    MemberOf => { Base => 'MemberOf',
 +                  Target => 'HasMember', },
 +    RefersTo => { Base => 'RefersTo',
 +                Target => 'ReferredToBy', },
 +    DependsOn => { Base => 'DependsOn',
 +                   Target => 'DependedOnBy', },
 +    MergedInto => { Base => 'MergedInto',
 +                   Target => 'MergedInto', },
 +
 +);
 +
 +
 +sub LINKTYPEMAP   { return \%LINKTYPEMAP   }
 +sub LINKDIRMAP   { return \%LINKDIRMAP   }
 +
 +our %MERGE_CACHE = (
 +    effective => {},
 +    merged => {},
 +);
 +
 +
 +=head2 Load
 +
 +Takes a single argument. This can be a ticket id, ticket alias or 
 +local ticket uri.  If the ticket can't be loaded, returns undef.
 +Otherwise, returns the ticket id.
 +
 +=cut
 +
 +sub Load {
 +    my $self = shift;
 +    my $id   = shift;
 +    $id = '' unless defined $id;
 +
 +    # TODO: modify this routine to look at EffectiveId and
 +    # do the recursive load thing. be careful to cache all
 +    # the interim tickets we try so we don't loop forever.
 +
 +    unless ( $id =~ /^\d+$/ ) {
 +        $RT::Logger->debug("Tried to load a bogus ticket id: '$id'");
 +        return (undef);
 +    }
 +
 +    $id = $MERGE_CACHE{'effective'}{ $id }
 +        if $MERGE_CACHE{'effective'}{ $id };
 +
 +    my ($ticketid, $msg) = $self->LoadById( $id );
 +    unless ( $self->Id ) {
 +        $RT::Logger->debug("$self tried to load a bogus ticket: $id");
 +        return (undef);
 +    }
 +
 +    #If we're merged, resolve the merge.
 +    if ( $self->EffectiveId && $self->EffectiveId != $self->Id ) {
 +        $RT::Logger->debug(
 +            "We found a merged ticket. "
 +            . $self->id ."/". $self->EffectiveId
 +        );
 +        my $real_id = $self->Load( $self->EffectiveId );
 +        $MERGE_CACHE{'effective'}{ $id } = $real_id;
 +        return $real_id;
 +    }
 +
 +    #Ok. we're loaded. lets get outa here.
 +    return $self->Id;
 +}
 +
 +
 +
 +=head2 Create (ARGS)
 +
 +Arguments: ARGS is a hash of named parameters.  Valid parameters are:
 +
 +  id 
 +  Queue  - Either a Queue object or a Queue Name
 +  Requestor -  A reference to a list of  email addresses or RT user Names
 +  Cc  - A reference to a list of  email addresses or Names
 +  AdminCc  - A reference to a  list of  email addresses or Names
 +  SquelchMailTo - A reference to a list of email addresses - 
 +                  who should this ticket not mail
 +  Type -- The ticket\'s type. ignore this for now
 +  Owner -- This ticket\'s owner. either an RT::User object or this user\'s id
 +  Subject -- A string describing the subject of the ticket
 +  Priority -- an integer from 0 to 99
 +  InitialPriority -- an integer from 0 to 99
 +  FinalPriority -- an integer from 0 to 99
 +  Status -- any valid status (Defined in RT::Queue)
 +  TimeEstimated -- an integer. estimated time for this task in minutes
 +  TimeWorked -- an integer. time worked so far in minutes
 +  TimeLeft -- an integer. time remaining in minutes
 +  Starts -- an ISO date describing the ticket\'s start date and time in GMT
 +  Due -- an ISO date describing the ticket\'s due date and time in GMT
 +  MIMEObj -- a MIME::Entity object with the content of the initial ticket request.
 +  CustomField-<n> -- a scalar or array of values for the customfield with the id <n>
 +
 +Ticket links can be set up during create by passing the link type as a hask key and
 +the ticket id to be linked to as a value (or a URI when linking to other objects).
 +Multiple links of the same type can be created by passing an array ref. For example:
 +
 +  Parents => 45,
 +  DependsOn => [ 15, 22 ],
 +  RefersTo => 'http://www.bestpractical.com',
 +
 +Supported link types are C<MemberOf>, C<HasMember>, C<RefersTo>, C<ReferredToBy>,
 +C<DependsOn> and C<DependedOnBy>. Also, C<Parents> is alias for C<MemberOf> and
 +C<Members> and C<Children> are aliases for C<HasMember>.
 +
 +Returns: TICKETID, Transaction Object, Error Message
 +
 +
 +=cut
 +
 +sub Create {
 +    my $self = shift;
 +
 +    my %args = (
 +        id                 => undef,
 +        EffectiveId        => undef,
 +        Queue              => undef,
 +        Requestor          => undef,
 +        Cc                 => undef,
 +        AdminCc            => undef,
 +        SquelchMailTo      => undef,
 +        TransSquelchMailTo => undef,
 +        Type               => 'ticket',
 +        Owner              => undef,
 +        Subject            => '',
 +        InitialPriority    => undef,
 +        FinalPriority      => undef,
 +        Priority           => undef,
 +        Status             => undef,
 +        TimeWorked         => "0",
 +        TimeLeft           => 0,
 +        TimeEstimated      => 0,
 +        Due                => undef,
 +        Starts             => undef,
 +        Started            => undef,
 +        Resolved           => undef,
 +        MIMEObj            => undef,
 +        _RecordTransaction => 1,
 +        DryRun             => 0,
 +        @_
 +    );
 +
 +    my ($ErrStr, @non_fatal_errors);
 +
 +    my $QueueObj = RT::Queue->new( RT->SystemUser );
 +    if ( ref $args{'Queue'} eq 'RT::Queue' ) {
 +        $QueueObj->Load( $args{'Queue'}->Id );
 +    }
 +    elsif ( $args{'Queue'} ) {
 +        $QueueObj->Load( $args{'Queue'} );
 +    }
 +    else {
 +        $RT::Logger->debug("'". ( $args{'Queue'} ||''). "' not a recognised queue object." );
 +    }
 +
 +    #Can't create a ticket without a queue.
 +    unless ( $QueueObj->Id ) {
 +        $RT::Logger->debug("$self No queue given for ticket creation.");
 +        return ( 0, 0, $self->loc('Could not create ticket. Queue not set') );
 +    }
 +
 +
 +    #Now that we have a queue, Check the ACLS
 +    unless (
 +        $self->CurrentUser->HasRight(
 +            Right  => 'CreateTicket',
 +            Object => $QueueObj
 +        )
 +      )
 +    {
 +        return (
 +            0, 0,
 +            $self->loc( "No permission to create tickets in the queue '[_1]'", $QueueObj->Name));
 +    }
 +
 +    my $cycle = $QueueObj->Lifecycle;
 +    unless ( defined $args{'Status'} && length $args{'Status'} ) {
 +        $args{'Status'} = $cycle->DefaultOnCreate;
 +    }
 +
 +    unless ( $cycle->IsValid( $args{'Status'} ) ) {
 +        return ( 0, 0,
 +            $self->loc("Status '[_1]' isn't a valid status for tickets in this queue.",
 +                $self->loc($args{'Status'}))
 +        );
 +    }
 +
 +    unless ( $cycle->IsTransition( '' => $args{'Status'} ) ) {
 +        return ( 0, 0,
 +            $self->loc("New tickets can not have status '[_1]' in this queue.",
 +                $self->loc($args{'Status'}))
 +        );
 +    }
 +
 +
 +
 +    #Since we have a queue, we can set queue defaults
 +
 +    #Initial Priority
 +    # If there's no queue default initial priority and it's not set, set it to 0
 +    $args{'InitialPriority'} = $QueueObj->InitialPriority || 0
 +        unless defined $args{'InitialPriority'};
 +
 +    #Final priority
 +    # If there's no queue default final priority and it's not set, set it to 0
 +    $args{'FinalPriority'} = $QueueObj->FinalPriority || 0
 +        unless defined $args{'FinalPriority'};
 +
 +    # Priority may have changed from InitialPriority, for the case
 +    # where we're importing tickets (eg, from an older RT version.)
 +    $args{'Priority'} = $args{'InitialPriority'}
 +        unless defined $args{'Priority'};
 +
 +    # Dates
 +    #TODO we should see what sort of due date we're getting, rather +
 +    # than assuming it's in ISO format.
 +
 +    #Set the due date. if we didn't get fed one, use the queue default due in
 +    my $Due = RT::Date->new( $self->CurrentUser );
 +    if ( defined $args{'Due'} ) {
 +        $Due->Set( Format => 'ISO', Value => $args{'Due'} );
 +    }
 +    elsif ( my $due_in = $QueueObj->DefaultDueIn ) {
 +        $Due->SetToNow;
 +        $Due->AddDays( $due_in );
 +    }
 +
 +    my $Starts = RT::Date->new( $self->CurrentUser );
 +    if ( defined $args{'Starts'} ) {
 +        $Starts->Set( Format => 'ISO', Value => $args{'Starts'} );
 +    }
 +
 +    my $Started = RT::Date->new( $self->CurrentUser );
 +    if ( defined $args{'Started'} ) {
 +        $Started->Set( Format => 'ISO', Value => $args{'Started'} );
 +    }
 +
 +    # If the status is not an initial status, set the started date
 +    elsif ( !$cycle->IsInitial($args{'Status'}) ) {
 +        $Started->SetToNow;
 +    }
 +
 +    my $Resolved = RT::Date->new( $self->CurrentUser );
 +    if ( defined $args{'Resolved'} ) {
 +        $Resolved->Set( Format => 'ISO', Value => $args{'Resolved'} );
 +    }
 +
 +    #If the status is an inactive status, set the resolved date
 +    elsif ( $cycle->IsInactive( $args{'Status'} ) )
 +    {
 +        $RT::Logger->debug( "Got a ". $args{'Status'}
 +            ."(inactive) ticket with undefined resolved date. Setting to now."
 +        );
 +        $Resolved->SetToNow;
 +    }
 +
 +    # }}}
 +
 +    # Dealing with time fields
 +
 +    $args{'TimeEstimated'} = 0 unless defined $args{'TimeEstimated'};
 +    $args{'TimeWorked'}    = 0 unless defined $args{'TimeWorked'};
 +    $args{'TimeLeft'}      = 0 unless defined $args{'TimeLeft'};
 +
 +    # }}}
 +
 +    # Deal with setting the owner
 +
 +    my $Owner;
 +    if ( ref( $args{'Owner'} ) eq 'RT::User' ) {
 +        if ( $args{'Owner'}->id ) {
 +            $Owner = $args{'Owner'};
 +        } else {
 +            $RT::Logger->error('Passed an empty RT::User for owner');
 +            push @non_fatal_errors,
 +                $self->loc("Owner could not be set.") . " ".
 +            $self->loc("Invalid value for [_1]",loc('owner'));
 +            $Owner = undef;
 +        }
 +    }
 +
 +    #If we've been handed something else, try to load the user.
 +    elsif ( $args{'Owner'} ) {
 +        $Owner = RT::User->new( $self->CurrentUser );
 +        $Owner->Load( $args{'Owner'} );
 +        if (!$Owner->id) {
 +            $Owner->LoadByEmail( $args{'Owner'} )
 +        }
 +        unless ( $Owner->Id ) {
 +            push @non_fatal_errors,
 +                $self->loc("Owner could not be set.") . " "
 +              . $self->loc( "User '[_1]' could not be found.", $args{'Owner'} );
 +            $Owner = undef;
 +        }
 +    }
 +
 +    #If we have a proposed owner and they don't have the right
 +    #to own a ticket, scream about it and make them not the owner
 +   
 +    my $DeferOwner;  
 +    if ( $Owner && $Owner->Id != RT->Nobody->Id 
 +        && !$Owner->HasRight( Object => $QueueObj, Right  => 'OwnTicket' ) )
 +    {
 +        $DeferOwner = $Owner;
 +        $Owner = undef;
 +        $RT::Logger->debug('going to deffer setting owner');
 +
 +    }
 +
 +    #If we haven't been handed a valid owner, make it nobody.
 +    unless ( defined($Owner) && $Owner->Id ) {
 +        $Owner = RT::User->new( $self->CurrentUser );
 +        $Owner->Load( RT->Nobody->Id );
 +    }
 +
 +    # }}}
 +
 +# We attempt to load or create each of the people who might have a role for this ticket
 +# _outside_ the transaction, so we don't get into ticket creation races
 +    foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
 +        $args{ $type } = [ $args{ $type } ] unless ref $args{ $type };
 +        foreach my $watcher ( splice @{ $args{$type} } ) {
 +            next unless $watcher;
 +            if ( $watcher =~ /^\d+$/ ) {
 +                push @{ $args{$type} }, $watcher;
 +            } else {
 +                my @addresses = RT::EmailParser->ParseEmailAddress( $watcher );
 +                foreach my $address( @addresses ) {
 +                    my $user = RT::User->new( RT->SystemUser );
 +                    my ($uid, $msg) = $user->LoadOrCreateByEmail( $address );
 +                    unless ( $uid ) {
 +                        push @non_fatal_errors,
 +                            $self->loc("Couldn't load or create user: [_1]", $msg);
 +                    } else {
 +                        push @{ $args{$type} }, $user->id;
 +                    }
 +                }
 +            }
 +        }
 +    }
 +
 +    $RT::Handle->BeginTransaction();
 +
 +    my %params = (
 +        Queue           => $QueueObj->Id,
 +        Owner           => $Owner->Id,
 +        Subject         => $args{'Subject'},
 +        InitialPriority => $args{'InitialPriority'},
 +        FinalPriority   => $args{'FinalPriority'},
 +        Priority        => $args{'Priority'},
 +        Status          => $args{'Status'},
 +        TimeWorked      => $args{'TimeWorked'},
 +        TimeEstimated   => $args{'TimeEstimated'},
 +        TimeLeft        => $args{'TimeLeft'},
 +        Type            => $args{'Type'},
 +        Starts          => $Starts->ISO,
 +        Started         => $Started->ISO,
 +        Resolved        => $Resolved->ISO,
 +        Due             => $Due->ISO
 +    );
 +
 +# Parameters passed in during an import that we probably don't want to touch, otherwise
 +    foreach my $attr (qw(id Creator Created LastUpdated LastUpdatedBy)) {
 +        $params{$attr} = $args{$attr} if $args{$attr};
 +    }
 +
 +    # Delete null integer parameters
 +    foreach my $attr
 +        (qw(TimeWorked TimeLeft TimeEstimated InitialPriority FinalPriority))
 +    {
 +        delete $params{$attr}
 +          unless ( exists $params{$attr} && $params{$attr} );
 +    }
 +
 +    # Delete the time worked if we're counting it in the transaction
 +    delete $params{'TimeWorked'} if $args{'_RecordTransaction'};
 +
 +    my ($id,$ticket_message) = $self->SUPER::Create( %params );
 +    unless ($id) {
 +        $RT::Logger->crit( "Couldn't create a ticket: " . $ticket_message );
 +        $RT::Handle->Rollback();
 +        return ( 0, 0,
 +            $self->loc("Ticket could not be created due to an internal error")
 +        );
 +    }
 +
 +    #Set the ticket's effective ID now that we've created it.
 +    my ( $val, $msg ) = $self->__Set(
 +        Field => 'EffectiveId',
 +        Value => ( $args{'EffectiveId'} || $id )
 +    );
 +    unless ( $val ) {
 +        $RT::Logger->crit("Couldn't set EffectiveId: $msg");
 +        $RT::Handle->Rollback;
 +        return ( 0, 0,
 +            $self->loc("Ticket could not be created due to an internal error")
 +        );
 +    }
 +
 +    my $create_groups_ret = $self->_CreateTicketGroups();
 +    unless ($create_groups_ret) {
 +        $RT::Logger->crit( "Couldn't create ticket groups for ticket "
 +              . $self->Id
 +              . ". aborting Ticket creation." );
 +        $RT::Handle->Rollback();
 +        return ( 0, 0,
 +            $self->loc("Ticket could not be created due to an internal error")
 +        );
 +    }
 +
 +    # Set the owner in the Groups table
 +    # We denormalize it into the Ticket table too because doing otherwise would
 +    # kill performance, bigtime. It gets kept in lockstep thanks to the magic of transactionalization
 +    $self->OwnerGroup->_AddMember(
 +        PrincipalId       => $Owner->PrincipalId,
 +        InsideTransaction => 1
 +    ) unless $DeferOwner;
 +
 +
 +
 +    # Deal with setting up watchers
 +
 +    foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
 +        # we know it's an array ref
 +        foreach my $watcher ( @{ $args{$type} } ) {
 +
 +            # Note that we're using AddWatcher, rather than _AddWatcher, as we
 +            # actually _want_ that ACL check. Otherwise, random ticket creators
 +            # could make themselves adminccs and maybe get ticket rights. that would
 +            # be poor
 +            my $method = $type eq 'AdminCc'? 'AddWatcher': '_AddWatcher';
 +
 +            my ($val, $msg) = $self->$method(
 +                Type   => $type,
 +                PrincipalId => $watcher,
 +                Silent => 1,
 +            );
 +            push @non_fatal_errors, $self->loc("Couldn't set [_1] watcher: [_2]", $type, $msg)
 +                unless $val;
 +        }
 +    } 
 +
 +    if ($args{'SquelchMailTo'}) {
 +       my @squelch = ref( $args{'SquelchMailTo'} ) ? @{ $args{'SquelchMailTo'} }
 +        : $args{'SquelchMailTo'};
 +        $self->_SquelchMailTo( @squelch );
 +    }
 +
 +
 +    # }}}
 +
 +    # Add all the custom fields
 +
 +    foreach my $arg ( keys %args ) {
 +        next unless $arg =~ /^CustomField-(\d+)$/i;
 +        my $cfid = $1;
 +
 +        foreach my $value (
 +            UNIVERSAL::isa( $args{$arg} => 'ARRAY' ) ? @{ $args{$arg} } : ( $args{$arg} ) )
 +        {
 +            next unless defined $value && length $value;
 +
 +            # Allow passing in uploaded LargeContent etc by hash reference
 +            my ($status, $msg) = $self->_AddCustomFieldValue(
 +                (UNIVERSAL::isa( $value => 'HASH' )
 +                    ? %$value
 +                    : (Value => $value)
 +                ),
 +                Field             => $cfid,
 +                RecordTransaction => 0,
 +            );
 +            push @non_fatal_errors, $msg unless $status;
 +        }
 +    }
 +
 +    # }}}
 +
 +    # Deal with setting up links
 +
 +    # TODO: Adding link may fire scrips on other end and those scrips
 +    # could create transactions on this ticket before 'Create' transaction.
 +    #
 +    # We should implement different lifecycle: record 'Create' transaction,
 +    # create links and only then fire create transaction's scrips.
 +    #
 +    # Ideal variant: add all links without firing scrips, record create
 +    # transaction and only then fire scrips on the other ends of links.
 +    #
 +    # //RUZ
 +
 +    foreach my $type ( keys %LINKTYPEMAP ) {
 +        next unless ( defined $args{$type} );
 +        foreach my $link (
 +            ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
 +        {
 +            my ( $val, $msg, $obj ) = $self->__GetTicketFromURI( URI => $link );
 +            unless ($val) {
 +                push @non_fatal_errors, $msg;
 +                next;
 +            }
 +
 +            # Check rights on the other end of the link if we must
 +            # then run _AddLink that doesn't check for ACLs
 +            if ( RT->Config->Get( 'StrictLinkACL' ) ) {
 +                if ( $obj && !$obj->CurrentUserHasRight('ModifyTicket') ) {
 +                    push @non_fatal_errors, $self->loc('Linking. Permission denied');
 +                    next;
 +                }
 +            }
 +
 +            if ( $obj && $obj->Status eq 'deleted' ) {
 +                push @non_fatal_errors,
 +                  $self->loc("Linking. Can't link to a deleted ticket");
 +                next;
 +            }
 +
 +            my ( $wval, $wmsg ) = $self->_AddLink(
 +                Type                          => $LINKTYPEMAP{$type}->{'Type'},
 +                $LINKTYPEMAP{$type}->{'Mode'} => $link,
 +                Silent                        => !$args{'_RecordTransaction'} || $self->Type eq 'reminder',
 +                'Silent'. ( $LINKTYPEMAP{$type}->{'Mode'} eq 'Base'? 'Target': 'Base' )
 +                                              => 1,
 +            );
 +
 +            push @non_fatal_errors, $wmsg unless ($wval);
 +        }
 +    }
 +
 +    # }}}
 +    # Now that we've created the ticket and set up its metadata, we can actually go and check OwnTicket on the ticket itself. 
 +    # This might be different than before in cases where extensions like RTIR are doing clever things with RT's ACL system
 +    if (  $DeferOwner ) { 
 +            if (!$DeferOwner->HasRight( Object => $self, Right  => 'OwnTicket')) {
 +    
 +            $RT::Logger->warning( "User " . $DeferOwner->Name . "(" . $DeferOwner->id 
 +                . ") was proposed as a ticket owner but has no rights to own "
 +                . "tickets in " . $QueueObj->Name );
 +            push @non_fatal_errors, $self->loc(
 +                "Owner '[_1]' does not have rights to own this ticket.",
 +                $DeferOwner->Name
 +            );
 +        } else {
 +            $Owner = $DeferOwner;
 +            $self->__Set(Field => 'Owner', Value => $Owner->id);
 +
 +        }
 +        $self->OwnerGroup->_AddMember(
 +            PrincipalId       => $Owner->PrincipalId,
 +            InsideTransaction => 1
 +        );
 +    }
 +
 +    if ( $args{'_RecordTransaction'} ) {
 +
 +        # Add a transaction for the create
 +        my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
 +            Type         => "Create",
 +            TimeTaken    => $args{'TimeWorked'},
 +            MIMEObj      => $args{'MIMEObj'},
 +            CommitScrips => !$args{'DryRun'},
 +            SquelchMailTo => $args{'TransSquelchMailTo'},
 +        );
 +
 +        if ( $self->Id && $Trans ) {
 +
 +            $TransObj->UpdateCustomFields(ARGSRef => \%args);
 +
 +            $RT::Logger->info( "Ticket " . $self->Id . " created in queue '" . $QueueObj->Name . "' by " . $self->CurrentUser->Name );
 +            $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
 +            $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
 +        }
 +        else {
 +            $RT::Handle->Rollback();
 +
 +            $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
 +            $RT::Logger->error("Ticket couldn't be created: $ErrStr");
 +            return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error"));
 +        }
 +
 +        if ( $args{'DryRun'} ) {
 +            $RT::Handle->Rollback();
 +            return ($self->id, $TransObj, $ErrStr);
 +        }
 +        $RT::Handle->Commit();
 +        return ( $self->Id, $TransObj->Id, $ErrStr );
 +
 +        # }}}
 +    }
 +    else {
 +
 +        # Not going to record a transaction
 +        $RT::Handle->Commit();
 +        $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
 +        $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
 +        return ( $self->Id, 0, $ErrStr );
 +
 +    }
 +}
 +
 +
 +
 +
 +=head2 _Parse822HeadersForAttributes Content
 +
 +Takes an RFC822 style message and parses its attributes into a hash.
 +
 +=cut
 +
 +sub _Parse822HeadersForAttributes {
 +    my $self    = shift;
 +    my $content = shift;
 +    my %args;
 +
 +    my @lines = ( split ( /\n/, $content ) );
 +    while ( defined( my $line = shift @lines ) ) {
 +        if ( $line =~ /^(.*?):(?:\s+(.*))?$/ ) {
 +            my $value = $2;
 +            my $tag   = lc($1);
 +
 +            $tag =~ s/-//g;
 +            if ( defined( $args{$tag} ) )
 +            {    #if we're about to get a second value, make it an array
 +                $args{$tag} = [ $args{$tag} ];
 +            }
 +            if ( ref( $args{$tag} ) )
 +            {    #If it's an array, we want to push the value
 +                push @{ $args{$tag} }, $value;
 +            }
 +            else {    #if there's nothing there, just set the value
 +                $args{$tag} = $value;
 +            }
 +        } elsif ($line =~ /^$/) {
 +
 +            #TODO: this won't work, since "" isn't of the form "foo:value"
 +
 +                while ( defined( my $l = shift @lines ) ) {
 +                    push @{ $args{'content'} }, $l;
 +                }
 +            }
 +        
 +    }
 +
 +    foreach my $date (qw(due starts started resolved)) {
 +        my $dateobj = RT::Date->new(RT->SystemUser);
 +        if ( defined ($args{$date}) and $args{$date} =~ /^\d+$/ ) {
 +            $dateobj->Set( Format => 'unix', Value => $args{$date} );
 +        }
 +        else {
 +            $dateobj->Set( Format => 'unknown', Value => $args{$date} );
 +        }
 +        $args{$date} = $dateobj->ISO;
 +    }
 +    $args{'mimeobj'} = MIME::Entity->new();
 +    $args{'mimeobj'}->build(
 +        Type => ( $args{'contenttype'} || 'text/plain' ),
 +        Data => ($args{'content'} || '')
 +    );
 +
 +    return (%args);
 +}
 +
 +
 +
 +=head2 Import PARAMHASH
 +
 +Import a ticket. 
 +Doesn\'t create a transaction. 
 +Doesn\'t supply queue defaults, etc.
 +
 +Returns: TICKETID
 +
 +=cut
 +
 +sub Import {
 +    my $self = shift;
 +    my ( $ErrStr, $QueueObj, $Owner );
 +
 +    my %args = (
 +        id              => undef,
 +        EffectiveId     => undef,
 +        Queue           => undef,
 +        Requestor       => undef,
 +        Type            => 'ticket',
 +        Owner           => RT->Nobody->Id,
 +        Subject         => '[no subject]',
 +        InitialPriority => undef,
 +        FinalPriority   => undef,
 +        Status          => 'new',
 +        TimeWorked      => "0",
 +        Due             => undef,
 +        Created         => undef,
 +        Updated         => undef,
 +        Resolved        => undef,
 +        Told            => undef,
 +        @_
 +    );
 +
 +    if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
 +        $QueueObj = RT::Queue->new(RT->SystemUser);
 +        $QueueObj->Load( $args{'Queue'} );
 +
 +        #TODO error check this and return 0 if it\'s not loading properly +++
 +    }
 +    elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
 +        $QueueObj = RT::Queue->new(RT->SystemUser);
 +        $QueueObj->Load( $args{'Queue'}->Id );
 +    }
 +    else {
 +        $RT::Logger->debug(
 +            "$self " . $args{'Queue'} . " not a recognised queue object." );
 +    }
 +
 +    #Can't create a ticket without a queue.
 +    unless ( defined($QueueObj) and $QueueObj->Id ) {
 +        $RT::Logger->debug("$self No queue given for ticket creation.");
 +        return ( 0, $self->loc('Could not create ticket. Queue not set') );
 +    }
 +
 +    #Now that we have a queue, Check the ACLS
 +    unless (
 +        $self->CurrentUser->HasRight(
 +            Right    => 'CreateTicket',
 +            Object => $QueueObj
 +        )
 +      )
 +    {
 +        return ( 0,
 +            $self->loc("No permission to create tickets in the queue '[_1]'"
 +              , $QueueObj->Name));
 +    }
 +
 +    # Deal with setting the owner
 +
 +    # Attempt to take user object, user name or user id.
 +    # Assign to nobody if lookup fails.
 +    if ( defined( $args{'Owner'} ) ) {
 +        if ( ref( $args{'Owner'} ) ) {
 +            $Owner = $args{'Owner'};
 +        }
 +        else {
 +            $Owner = RT::User->new( $self->CurrentUser );
 +            $Owner->Load( $args{'Owner'} );
 +            if ( !defined( $Owner->id ) ) {
 +                $Owner->Load( RT->Nobody->id );
 +            }
 +        }
 +    }
 +
 +    #If we have a proposed owner and they don't have the right 
 +    #to own a ticket, scream about it and make them not the owner
 +    if (
 +        ( defined($Owner) )
 +        and ( $Owner->Id != RT->Nobody->Id )
 +        and (
 +            !$Owner->HasRight(
 +                Object => $QueueObj,
 +                Right    => 'OwnTicket'
 +            )
 +        )
 +      )
 +    {
 +
 +        $RT::Logger->warning( "$self user "
 +              . $Owner->Name . "("
 +              . $Owner->id
 +              . ") was proposed "
 +              . "as a ticket owner but has no rights to own "
 +              . "tickets in '"
 +              . $QueueObj->Name . "'" );
 +
 +        $Owner = undef;
 +    }
 +
 +    #If we haven't been handed a valid owner, make it nobody.
 +    unless ( defined($Owner) ) {
 +        $Owner = RT::User->new( $self->CurrentUser );
 +        $Owner->Load( RT->Nobody->UserObj->Id );
 +    }
 +
 +    # }}}
 +
 +    unless ( $self->ValidateStatus( $args{'Status'} ) ) {
 +        return ( 0, $self->loc("'[_1]' is an invalid value for status", $args{'Status'}) );
 +    }
 +
 +    $self->{'_AccessibleCache'}{Created}       = { 'read' => 1, 'write' => 1 };
 +    $self->{'_AccessibleCache'}{Creator}       = { 'read' => 1, 'auto'  => 1 };
 +    $self->{'_AccessibleCache'}{LastUpdated}   = { 'read' => 1, 'write' => 1 };
 +    $self->{'_AccessibleCache'}{LastUpdatedBy} = { 'read' => 1, 'auto'  => 1 };
 +
 +    # If we're coming in with an id, set that now.
 +    my $EffectiveId = undef;
 +    if ( $args{'id'} ) {
 +        $EffectiveId = $args{'id'};
 +
 +    }
 +
 +    my $id = $self->SUPER::Create(
 +        id              => $args{'id'},
 +        EffectiveId     => $EffectiveId,
 +        Queue           => $QueueObj->Id,
 +        Owner           => $Owner->Id,
 +        Subject         => $args{'Subject'},        # loc
 +        InitialPriority => $args{'InitialPriority'},    # loc
 +        FinalPriority   => $args{'FinalPriority'},    # loc
 +        Priority        => $args{'InitialPriority'},    # loc
 +        Status          => $args{'Status'},        # loc
 +        TimeWorked      => $args{'TimeWorked'},        # loc
 +        Type            => $args{'Type'},        # loc
 +        Created         => $args{'Created'},        # loc
 +        Told            => $args{'Told'},        # loc
 +        LastUpdated     => $args{'Updated'},        # loc
 +        Resolved        => $args{'Resolved'},        # loc
 +        Due             => $args{'Due'},        # loc
 +    );
 +
 +    # If the ticket didn't have an id
 +    # Set the ticket's effective ID now that we've created it.
 +    if ( $args{'id'} ) {
 +        $self->Load( $args{'id'} );
 +    }
 +    else {
 +        my ( $val, $msg ) =
 +          $self->__Set( Field => 'EffectiveId', Value => $id );
 +
 +        unless ($val) {
 +            $RT::Logger->err(
 +                $self . "->Import couldn't set EffectiveId: $msg" );
 +        }
 +    }
 +
 +    my $create_groups_ret = $self->_CreateTicketGroups();
 +    unless ($create_groups_ret) {
 +        $RT::Logger->crit(
 +            "Couldn't create ticket groups for ticket " . $self->Id );
 +    }
 +
 +    $self->OwnerGroup->_AddMember( PrincipalId => $Owner->PrincipalId );
 +
 +    foreach my $watcher ( @{ $args{'Cc'} } ) {
 +        $self->_AddWatcher( Type => 'Cc', Email => $watcher, Silent => 1 );
 +    }
 +    foreach my $watcher ( @{ $args{'AdminCc'} } ) {
 +        $self->_AddWatcher( Type => 'AdminCc', Email => $watcher,
 +            Silent => 1 );
 +    }
 +    foreach my $watcher ( @{ $args{'Requestor'} } ) {
 +        $self->_AddWatcher( Type => 'Requestor', Email => $watcher,
 +            Silent => 1 );
 +    }
 +
 +    return ( $self->Id, $ErrStr );
 +}
 +
 +
 +
 +
 +=head2 _CreateTicketGroups
 +
 +Create the ticket groups and links for this ticket. 
 +This routine expects to be called from Ticket->Create _inside of a transaction_
 +
 +It will create four groups for this ticket: Requestor, Cc, AdminCc and Owner.
 +
 +It will return true on success and undef on failure.
 +
 +
 +=cut
 +
 +
 +sub _CreateTicketGroups {
 +    my $self = shift;
 +    
 +    my @types = (qw(Requestor Owner Cc AdminCc));
 +
 +    foreach my $type (@types) {
 +        my $type_obj = RT::Group->new($self->CurrentUser);
 +        my ($id, $msg) = $type_obj->CreateRoleGroup(Domain => 'RT::Ticket-Role',
 +                                                       Instance => $self->Id, 
 +                                                       Type => $type);
 +        unless ($id) {
 +            $RT::Logger->error("Couldn't create a ticket group of type '$type' for ticket ".
 +                               $self->Id.": ".$msg);     
 +            return(undef);
 +        }
 +     }
 +    return(1);
 +    
 +}
 +
 +
 +
 +=head2 OwnerGroup
 +
 +A constructor which returns an RT::Group object containing the owner of this ticket.
 +
 +=cut
 +
 +sub OwnerGroup {
 +    my $self = shift;
 +    my $owner_obj = RT::Group->new($self->CurrentUser);
 +    $owner_obj->LoadTicketRoleGroup( Ticket => $self->Id,  Type => 'Owner');
 +    return ($owner_obj);
 +}
 +
 +
 +
 +
 +=head2 AddWatcher
 +
 +AddWatcher takes a parameter hash. The keys are as follows:
 +
 +Type        One of Requestor, Cc, AdminCc
 +
 +PrincipalId The RT::Principal id of the user or group that's being added as a watcher
 +
 +Email       The email address of the new watcher. If a user with this 
 +            email address can't be found, a new nonprivileged user will be created.
 +
 +If the watcher you\'re trying to set has an RT account, set the PrincipalId paremeter to their User Id. Otherwise, set the Email parameter to their Email address.
 +
 +=cut
 +
 +sub AddWatcher {
 +    my $self = shift;
 +    my %args = (
 +        Type  => undef,
 +        PrincipalId => undef,
 +        Email => undef,
 +        @_
 +    );
 +
 +    # ModifyTicket works in any case
 +    return $self->_AddWatcher( %args )
 +        if $self->CurrentUserHasRight('ModifyTicket');
 +    if ( $args{'Email'} ) {
 +        my ($addr) = RT::EmailParser->ParseEmailAddress( $args{'Email'} );
 +        return (0, $self->loc("Couldn't parse address from '[_1]' string", $args{'Email'} ))
 +            unless $addr;
 +
 +        if ( lc $self->CurrentUser->UserObj->EmailAddress
 +            eq lc RT::User->CanonicalizeEmailAddress( $addr->address ) )
 +        {
 +            $args{'PrincipalId'} = $self->CurrentUser->id;
 +            delete $args{'Email'};
 +        }
 +    }
 +
 +    # If the watcher isn't the current user then the current user has no right
 +    # bail
 +    unless ( $args{'PrincipalId'} && $self->CurrentUser->id == $args{'PrincipalId'} ) {
 +        return ( 0, $self->loc("Permission Denied") );
 +    }
 +
 +    #  If it's an AdminCc and they don't have 'WatchAsAdminCc', bail
 +    if ( $args{'Type'} eq 'AdminCc' ) {
 +        unless ( $self->CurrentUserHasRight('WatchAsAdminCc') ) {
 +            return ( 0, $self->loc('Permission Denied') );
 +        }
 +    }
 +
 +    #  If it's a Requestor or Cc and they don't have 'Watch', bail
 +    elsif ( $args{'Type'} eq 'Cc' || $args{'Type'} eq 'Requestor' ) {
 +        unless ( $self->CurrentUserHasRight('Watch') ) {
 +            return ( 0, $self->loc('Permission Denied') );
 +        }
 +    }
 +    else {
 +        $RT::Logger->warning( "AddWatcher got passed a bogus type");
 +        return ( 0, $self->loc('Error in parameters to Ticket->AddWatcher') );
 +    }
 +
 +    return $self->_AddWatcher( %args );
 +}
 +
 +#This contains the meat of AddWatcher. but can be called from a routine like
 +# Create, which doesn't need the additional acl check
 +sub _AddWatcher {
 +    my $self = shift;
 +    my %args = (
 +        Type   => undef,
 +        Silent => undef,
 +        PrincipalId => undef,
 +        Email => undef,
 +        @_
 +    );
 +
 +
 +    my $principal = RT::Principal->new($self->CurrentUser);
 +    if ($args{'Email'}) {
 +        if ( RT::EmailParser->IsRTAddress( $args{'Email'} ) ) {
 +            return (0, $self->loc("[_1] is an address RT receives mail at. Adding it as a '[_2]' would create a mail loop", $args{'Email'}, $self->loc($args{'Type'})));
 +        }
 +        my $user = RT::User->new(RT->SystemUser);
 +        my ($pid, $msg) = $user->LoadOrCreateByEmail( $args{'Email'} );
 +        $args{'PrincipalId'} = $pid if $pid; 
 +    }
 +    if ($args{'PrincipalId'}) {
 +        $principal->Load($args{'PrincipalId'});
 +        if ( $principal->id and $principal->IsUser and my $email = $principal->Object->EmailAddress ) {
 +            return (0, $self->loc("[_1] is an address RT receives mail at. Adding it as a '[_2]' would create a mail loop", $email, $self->loc($args{'Type'})))
 +                if RT::EmailParser->IsRTAddress( $email );
 +
 +        }
 +    } 
 +
 + 
 +    # If we can't find this watcher, we need to bail.
 +    unless ($principal->Id) {
 +            $RT::Logger->error("Could not load create a user with the email address '".$args{'Email'}. "' to add as a watcher for ticket ".$self->Id);
 +        return(0, $self->loc("Could not find or create that user"));
 +    }
 +
 +
 +    my $group = RT::Group->new($self->CurrentUser);
 +    $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->Id);
 +    unless ($group->id) {
 +        return(0,$self->loc("Group not found"));
 +    }
 +
 +    if ( $group->HasMember( $principal)) {
 +
 +        return ( 0, $self->loc('That principal is already a [_1] for this ticket', $self->loc($args{'Type'})) );
 +    }
 +
 +
 +    my ( $m_id, $m_msg ) = $group->_AddMember( PrincipalId => $principal->Id,
 +                                               InsideTransaction => 1 );
 +    unless ($m_id) {
 +        $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id.": ".$m_msg);
 +
 +        return ( 0, $self->loc('Could not make that principal a [_1] for this ticket', $self->loc($args{'Type'})) );
 +    }
 +
 +    unless ( $args{'Silent'} ) {
 +        $self->_NewTransaction(
 +            Type     => 'AddWatcher',
 +            NewValue => $principal->Id,
 +            Field    => $args{'Type'}
 +        );
 +    }
 +
 +        return ( 1, $self->loc('Added principal as a [_1] for this ticket', $self->loc($args{'Type'})) );
 +}
 +
 +
 +
 +
 +=head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS }
 +
 +
 +Deletes a Ticket watcher.  Takes two arguments:
 +
 +Type  (one of Requestor,Cc,AdminCc)
 +
 +and one of
 +
 +PrincipalId (an RT::Principal Id of the watcher you want to remove)
 +    OR
 +Email (the email address of an existing wathcer)
 +
 +
 +=cut
 +
 +
 +sub DeleteWatcher {
 +    my $self = shift;
 +
 +    my %args = ( Type        => undef,
 +                 PrincipalId => undef,
 +                 Email       => undef,
 +                 @_ );
 +
 +    unless ( $args{'PrincipalId'} || $args{'Email'} ) {
 +        return ( 0, $self->loc("No principal specified") );
 +    }
 +    my $principal = RT::Principal->new( $self->CurrentUser );
 +    if ( $args{'PrincipalId'} ) {
 +
 +        $principal->Load( $args{'PrincipalId'} );
 +    }
 +    else {
 +        my $user = RT::User->new( $self->CurrentUser );
 +        $user->LoadByEmail( $args{'Email'} );
 +        $principal->Load( $user->Id );
 +    }
 +
 +    # If we can't find this watcher, we need to bail.
 +    unless ( $principal->Id ) {
 +        return ( 0, $self->loc("Could not find that principal") );
 +    }
 +
 +    my $group = RT::Group->new( $self->CurrentUser );
 +    $group->LoadTicketRoleGroup( Type => $args{'Type'}, Ticket => $self->Id );
 +    unless ( $group->id ) {
 +        return ( 0, $self->loc("Group not found") );
 +    }
 +
 +    # Check ACLS
 +    #If the watcher we're trying to add is for the current user
 +    if ( $self->CurrentUser->PrincipalId == $principal->id ) {
 +
 +        #  If it's an AdminCc and they don't have
 +        #   'WatchAsAdminCc' or 'ModifyTicket', bail
 +        if ( $args{'Type'} eq 'AdminCc' ) {
 +            unless (    $self->CurrentUserHasRight('ModifyTicket')
 +                     or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
 +                return ( 0, $self->loc('Permission Denied') );
 +            }
 +        }
 +
 +        #  If it's a Requestor or Cc and they don't have
 +        #   'Watch' or 'ModifyTicket', bail
 +        elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) )
 +        {
 +            unless (    $self->CurrentUserHasRight('ModifyTicket')
 +                     or $self->CurrentUserHasRight('Watch') ) {
 +                return ( 0, $self->loc('Permission Denied') );
 +            }
 +        }
 +        else {
-             $RT::Logger->warn("$self -> DeleteWatcher got passed a bogus type");
++            $RT::Logger->warning("$self -> DeleteWatcher got passed a bogus type");
 +            return ( 0,
 +                     $self->loc('Error in parameters to Ticket->DeleteWatcher') );
 +        }
 +    }
 +
 +    # If the watcher isn't the current user
 +    # and the current user  doesn't have 'ModifyTicket' bail
 +    else {
 +        unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
 +            return ( 0, $self->loc("Permission Denied") );
 +        }
 +    }
 +
 +    # }}}
 +
 +    # see if this user is already a watcher.
 +
 +    unless ( $group->HasMember($principal) ) {
 +        return ( 0,
 +                 $self->loc( 'That principal is not a [_1] for this ticket',
 +                             $args{'Type'} ) );
 +    }
 +
 +    my ( $m_id, $m_msg ) = $group->_DeleteMember( $principal->Id );
 +    unless ($m_id) {
 +        $RT::Logger->error( "Failed to delete "
 +                            . $principal->Id
 +                            . " as a member of group "
 +                            . $group->Id . ": "
 +                            . $m_msg );
 +
 +        return (0,
 +                $self->loc(
 +                    'Could not remove that principal as a [_1] for this ticket',
 +                    $args{'Type'} ) );
 +    }
 +
 +    unless ( $args{'Silent'} ) {
 +        $self->_NewTransaction( Type     => 'DelWatcher',
 +                                OldValue => $principal->Id,
 +                                Field    => $args{'Type'} );
 +    }
 +
 +    return ( 1,
 +             $self->loc( "[_1] is no longer a [_2] for this ticket.",
 +                         $principal->Object->Name,
 +                         $args{'Type'} ) );
 +}
 +
 +
 +
 +
 +
 +=head2 SquelchMailTo [EMAIL]
 +
 +Takes an optional email address to never email about updates to this ticket.
 +
 +
 +Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes.
 +
 +
 +=cut
 +
 +sub SquelchMailTo {
 +    my $self = shift;
 +    if (@_) {
 +        unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
 +            return ();
 +        }
 +    } else {
 +        unless ( $self->CurrentUserHasRight('ShowTicket') ) {
 +            return ();
 +        }
 +
 +    }
 +    return $self->_SquelchMailTo(@_);
 +}
 +
 +sub _SquelchMailTo {
 +    my $self = shift;
 +    if (@_) {
 +        my $attr = shift;
 +        $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr )
 +            unless grep { $_->Content eq $attr }
 +                $self->Attributes->Named('SquelchMailTo');
 +    }
 +    my @attributes = $self->Attributes->Named('SquelchMailTo');
 +    return (@attributes);
 +}
 +
 +
 +=head2 UnsquelchMailTo ADDRESS
 +
 +Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.
 +
 +Returns a tuple of (status, message)
 +
 +=cut
 +
 +sub UnsquelchMailTo {
 +    my $self = shift;
 +
 +    my $address = shift;
 +    unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
 +        return ( 0, $self->loc("Permission Denied") );
 +    }
 +
 +    my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address);
 +    return ($val, $msg);
 +}
 +
 +
 +
 +=head2 RequestorAddresses
 +
 + B<Returns> String: All Ticket Requestor email addresses as a string.
 +
 +=cut
 +
 +sub RequestorAddresses {
 +    my $self = shift;
 +
 +    unless ( $self->CurrentUserHasRight('ShowTicket') ) {
 +        return undef;
 +    }
 +
 +    return ( $self->Requestors->MemberEmailAddressesAsString );
 +}
 +
 +
 +=head2 AdminCcAddresses
 +
 +returns String: All Ticket AdminCc email addresses as a string
 +
 +=cut
 +
 +sub AdminCcAddresses {
 +    my $self = shift;
 +
 +    unless ( $self->CurrentUserHasRight('ShowTicket') ) {
 +        return undef;
 +    }
 +
 +    return ( $self->AdminCc->MemberEmailAddressesAsString )
 +
 +}
 +
 +=head2 CcAddresses
 +
 +returns String: All Ticket Ccs as a string of email addresses
 +
 +=cut
 +
 +sub CcAddresses {
 +    my $self = shift;
 +
 +    unless ( $self->CurrentUserHasRight('ShowTicket') ) {
 +        return undef;
 +    }
 +    return ( $self->Cc->MemberEmailAddressesAsString);
 +
 +}
 +
 +
 +
 +
 +=head2 Requestors
 +
 +Takes nothing.
 +Returns this ticket's Requestors as an RT::Group object
 +
 +=cut
 +
 +sub Requestors {
 +    my $self = shift;
 +
 +    my $group = RT::Group->new($self->CurrentUser);
 +    if ( $self->CurrentUserHasRight('ShowTicket') ) {
 +        $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
 +    }
 +    return ($group);
 +
 +}
 +
 +
 +
 +=head2 Cc
 +
 +Takes nothing.
 +Returns an RT::Group object which contains this ticket's Ccs.
 +If the user doesn't have "ShowTicket" permission, returns an empty group
 +
 +=cut
 +
 +sub Cc {
 +    my $self = shift;
 +
 +    my $group = RT::Group->new($self->CurrentUser);
 +    if ( $self->CurrentUserHasRight('ShowTicket') ) {
 +        $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
 +    }
 +    return ($group);
 +
 +}
 +
 +
 +
 +=head2 AdminCc
 +
 +Takes nothing.
 +Returns an RT::Group object which contains this ticket's AdminCcs.
 +If the user doesn't have "ShowTicket" permission, returns an empty group
 +
 +=cut
 +
 +sub AdminCc {
 +    my $self = shift;
 +
 +    my $group = RT::Group->new($self->CurrentUser);
 +    if ( $self->CurrentUserHasRight('ShowTicket') ) {
 +        $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
 +    }
 +    return ($group);
 +
 +}
 +
 +
 +
 +
 +# a generic routine to be called by IsRequestor, IsCc and IsAdminCc
 +
 +=head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
 +
 +Takes a param hash with the attributes Type and either PrincipalId or Email
 +
 +Type is one of Requestor, Cc, AdminCc and Owner
 +
 +PrincipalId is an RT::Principal id, and Email is an email address.
 +
 +Returns true if the specified principal (or the one corresponding to the
 +specified address) is a member of the group Type for this ticket.
 +
 +XX TODO: This should be Memoized. 
 +
 +=cut
 +
 +sub IsWatcher {
 +    my $self = shift;
 +
 +    my %args = ( Type  => 'Requestor',
 +        PrincipalId    => undef,
 +        Email          => undef,
 +        @_
 +    );
 +
 +    # Load the relevant group. 
 +    my $group = RT::Group->new($self->CurrentUser);
 +    $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
 +
 +    # Find the relevant principal.
 +    if (!$args{PrincipalId} && $args{Email}) {
 +        # Look up the specified user.
 +        my $user = RT::User->new($self->CurrentUser);
 +        $user->LoadByEmail($args{Email});
 +        if ($user->Id) {
 +            $args{PrincipalId} = $user->PrincipalId;
 +        }
 +        else {
 +            # A non-existent user can't be a group member.
 +            return 0;
 +        }
 +    }
 +
 +    # Ask if it has the member in question
 +    return $group->HasMember( $args{'PrincipalId'} );
 +}
 +
 +
 +
 +=head2 IsRequestor PRINCIPAL_ID
 +  
 +Takes an L<RT::Principal> id.
 +
 +Returns true if the principal is a requestor of the current ticket.
 +
 +=cut
 +
 +sub IsRequestor {
 +    my $self   = shift;
 +    my $person = shift;
 +
 +    return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
 +
 +};
 +
 +
 +
 +=head2 IsCc PRINCIPAL_ID
 +
 +  Takes an RT::Principal id.
 +  Returns true if the principal is a Cc of the current ticket.
 +
 +
 +=cut
 +
 +sub IsCc {
 +    my $self = shift;
 +    my $cc   = shift;
 +
 +    return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
 +
 +}
 +
 +
 +
 +=head2 IsAdminCc PRINCIPAL_ID
 +
 +  Takes an RT::Principal id.
 +  Returns true if the principal is an AdminCc of the current ticket.
 +
 +=cut
 +
 +sub IsAdminCc {
 +    my $self   = shift;
 +    my $person = shift;
 +
 +    return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
 +
 +}
 +
 +
 +
 +=head2 IsOwner
 +
 +  Takes an RT::User object. Returns true if that user is this ticket's owner.
 +returns undef otherwise
 +
 +=cut
 +
 +sub IsOwner {
 +    my $self   = shift;
 +    my $person = shift;
 +
 +    # no ACL check since this is used in acl decisions
 +    # unless ($self->CurrentUserHasRight('ShowTicket')) {
 +    #    return(undef);
 +    #   }    
 +
 +    #Tickets won't yet have owners when they're being created.
 +    unless ( $self->OwnerObj->id ) {
 +        return (undef);
 +    }
 +
 +    if ( $person->id == $self->OwnerObj->id ) {
 +        return (1);
 +    }
 +    else {
 +        return (undef);
 +    }
 +}
 +
 +
 +
 +
 +
 +=head2 TransactionAddresses
 +
 +Returns a composite hashref of the results of L<RT::Transaction/Addresses> for
 +all this ticket's Create, Comment or Correspond transactions. The keys are
 +stringified email addresses. Each value is an L<Email::Address> object.
 +
 +NOTE: For performance reasons, this method might want to skip transactions and go straight for attachments. But to make that work right, we're going to need to go and walk around the access control in Attachment.pm's sub _Value.
 +
 +=cut
 +
 +
 +sub TransactionAddresses {
 +    my $self = shift;
 +    my $txns = $self->Transactions;
 +
 +    my %addresses = ();
 +
 +    my $attachments = RT::Attachments->new( $self->CurrentUser );
 +    $attachments->LimitByTicket( $self->id );
 +    $attachments->Columns( qw( id Headers TransactionId));
 +
 +
 +    foreach my $type (qw(Create Comment Correspond)) {
 +        $attachments->Limit( ALIAS    => $attachments->TransactionAlias,
 +                             FIELD    => 'Type',
 +                             OPERATOR => '=',
 +                             VALUE    => $type,
 +                             ENTRYAGGREGATOR => 'OR',
 +                             CASESENSITIVE   => 1
 +                           );
 +    }
 +
 +    while ( my $att = $attachments->Next ) {
 +        foreach my $addrlist ( values %{$att->Addresses } ) {
 +            foreach my $addr (@$addrlist) {
 +
 +# Skip addresses without a phrase (things that are just raw addresses) if we have a phrase
 +                next
 +                    if (    $addresses{ $addr->address }
 +                         && $addresses{ $addr->address }->phrase
 +                         && not $addr->phrase );
 +
 +                # skips "comment-only" addresses
 +                next unless ( $addr->address );
 +                $addresses{ $addr->address } = $addr;
 +            }
 +        }
 +    }
 +
 +    return \%addresses;
 +
 +}
 +
 +
 +
 +
 +
 +
 +sub ValidateQueue {
 +    my $self  = shift;
 +    my $Value = shift;
 +
 +    if ( !$Value ) {
 +        $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
 +        return (1);
 +    }
 +
 +    my $QueueObj = RT::Queue->new( $self->CurrentUser );
 +    my $id       = $QueueObj->Load($Value);
 +
 +    if ($id) {
 +        return (1);
 +    }
 +    else {
 +        return (undef);
 +    }
 +}
 +
 +
 +
 +sub SetQueue {
 +    my $self     = shift;
 +    my $NewQueue = shift;
 +
 +    #Redundant. ACL gets checked in _Set;
 +    unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
 +        return ( 0, $self->loc("Permission Denied") );
 +    }
 +
 +    my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
 +    $NewQueueObj->Load($NewQueue);
 +
 +    unless ( $NewQueueObj->Id() ) {
 +        return ( 0, $self->loc("That queue does not exist") );
 +    }
 +
 +    if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
 +        return ( 0, $self->loc('That is the same value') );
 +    }
 +    unless ( $self->CurrentUser->HasRight( Right    => 'CreateTicket', Object => $NewQueueObj)) {
 +        return ( 0, $self->loc("You may not create requests in that queue.") );
 +    }
 +
 +    my $new_status;
 +    my $old_lifecycle = $self->QueueObj->Lifecycle;
 +    my $new_lifecycle = $NewQueueObj->Lifecycle;
 +    if ( $old_lifecycle->Name ne $new_lifecycle->Name ) {
 +        unless ( $old_lifecycle->HasMoveMap( $new_lifecycle ) ) {
 +            return ( 0, $self->loc("There is no mapping for statuses between these queues. Contact your system administrator.") );
 +        }
 +        $new_status = $old_lifecycle->MoveMap( $new_lifecycle )->{ $self->Status };
 +        return ( 0, $self->loc("Mapping between queues' lifecycles is incomplete. Contact your system administrator.") )
 +            unless $new_status;
 +    }
 +
 +    if ( $new_status ) {
 +        my $clone = RT::Ticket->new( RT->SystemUser );
 +        $clone->Load( $self->Id );
 +        unless ( $clone->Id ) {
 +            return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
 +        }
 +
 +        my $now = RT::Date->new( $self->CurrentUser );
 +        $now->SetToNow;
 +
 +        my $old_status = $clone->Status;
 +
 +        #If we're changing the status from initial in old to not intial in new,
 +        # record that we've started
 +        if ( $old_lifecycle->IsInitial($old_status) && !$new_lifecycle->IsInitial($new_status)  && $clone->StartedObj->Unix == 0 ) {
 +            #Set the Started time to "now"
 +            $clone->_Set(
 +                Field             => 'Started',
 +                Value             => $now->ISO,
 +                RecordTransaction => 0
 +            );
 +        }
 +
 +        #When we close a ticket, set the 'Resolved' attribute to now.
 +        # It's misnamed, but that's just historical.
 +        if ( $new_lifecycle->IsInactive($new_status) ) {
 +            $clone->_Set(
 +                Field             => 'Resolved',
 +                Value             => $now->ISO,
 +                RecordTransaction => 0,
 +            );
 +        }
 +
 +        #Actually update the status
 +        my ($val, $msg)= $clone->_Set(
 +            Field             => 'Status',
 +            Value             => $new_status,
 +            RecordTransaction => 0,
 +        );
 +        $RT::Logger->error( 'Status change failed on queue change: '. $msg )
 +            unless $val;
 +    }
 +
 +    my ($status, $msg) = $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() );
 +
 +    if ( $status ) {
 +        # Clear the queue object cache;
 +        $self->{_queue_obj} = undef;
 +
 +        # Untake the ticket if we have no permissions in the new queue
 +        unless ( $self->OwnerObj->HasRight( Right => 'OwnTicket', Object => $NewQueueObj ) ) {
 +            my $clone = RT::Ticket->new( RT->SystemUser );
 +            $clone->Load( $self->Id );
 +            unless ( $clone->Id ) {
 +                return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
 +            }
 +            my ($status, $msg) = $clone->SetOwner( RT->Nobody->Id, 'Force' );
 +            $RT::Logger->error("Couldn't set owner on queue change: $msg") unless $status;
 +        }
 +
 +        # On queue change, change queue for reminders too
 +        my $reminder_collection = $self->Reminders->Collection;
 +        while ( my $reminder = $reminder_collection->Next ) {
 +            my ($status, $msg) = $reminder->SetQueue($NewQueue);
 +            $RT::Logger->error('Queue change failed for reminder #' . $reminder->Id . ': ' . $msg) unless $status;
 +        }
 +    }
 +
 +    return ($status, $msg);
 +}
 +
 +
 +
 +=head2 QueueObj
 +
 +Takes nothing. returns this ticket's queue object
 +
 +=cut
 +
 +sub QueueObj {
 +    my $self = shift;
 +
 +    if(!$self->{_queue_obj} || ! $self->{_queue_obj}->id) {
 +
 +        $self->{_queue_obj} = RT::Queue->new( $self->CurrentUser );
 +
 +        #We call __Value so that we can avoid the ACL decision and some deep recursion
 +        my ($result) = $self->{_queue_obj}->Load( $self->__Value('Queue') );
 +    }
 +    return ($self->{_queue_obj});
 +}
 +
 +=head2 SubjectTag
 +
 +Takes nothing. Returns SubjectTag for this ticket. Includes
 +queue's subject tag or rtname if that is not set, ticket
 +id and braces, for example:
 +
 +    [support.example.com #123456]
 +
 +=cut
 +
 +sub SubjectTag {
 +    my $self = shift;
 +    return
 +        '['
 +        . ($self->QueueObj->SubjectTag || RT->Config->Get('rtname'))
 +        .' #'. $self->id
 +        .']'
 +    ;
 +}
 +
 +
 +=head2 DueObj
 +
 +  Returns an RT::Date object containing this ticket's due date
 +
 +=cut
 +
 +sub DueObj {
 +    my $self = shift;
 +
 +    my $time = RT::Date->new( $self->CurrentUser );
 +
 +    # -1 is RT::Date slang for never
 +    if ( my $due = $self->Due ) {
 +        $time->Set( Format => 'sql', Value => $due );
 +    }
 +    else {
 +        $time->Set( Format => 'unix', Value => -1 );
 +    }
 +
 +    return $time;
 +}
 +
 +
 +
 +=head2 DueAsString
 +
 +Returns this ticket's due date as a human readable string
 +
 +=cut
 +
 +sub DueAsString {
 +    my $self = shift;
 +    return $self->DueObj->AsString();
 +}
 +
 +
 +
 +=head2 ResolvedObj
 +
 +  Returns an RT::Date object of this ticket's 'resolved' time.
 +
 +=cut
 +
 +sub ResolvedObj {
 +    my $self = shift;
 +
 +    my $time = RT::Date->new( $self->CurrentUser );
 +    $time->Set( Format => 'sql', Value => $self->Resolved );
 +    return $time;
 +}
 +
 +
 +=head2 FirstActiveStatus
 +
 +Returns the first active status that the ticket could transition to,
 +according to its current Queue's lifecycle.  May return undef if there
 +is no such possible status to transition to, or we are already in it.
 +This is used in L<RT::Action::AutoOpen>, for instance.
 +
 +=cut
 +
 +sub FirstActiveStatus {
 +    my $self = shift;
 +
 +    my $lifecycle = $self->QueueObj->Lifecycle;
 +    my $status = $self->Status;
 +    my @active = $lifecycle->Active;
 +    # no change if no active statuses in the lifecycle
 +    return undef unless @active;
 +
 +    # no change if the ticket is already has first status from the list of active
 +    return undef if lc $status eq lc $active[0];
 +
 +    my ($next) = grep $lifecycle->IsActive($_), $lifecycle->Transitions($status);
 +    return $next;
 +}
 +
 +=head2 SetStarted
 +
 +Takes a date in ISO format or undef
 +Returns a transaction id and a message
 +The client calls "Start" to note that the project was started on the date in $date.
 +A null date means "now"
 +
 +=cut
 +
 +sub SetStarted {
 +    my $self = shift;
 +    my $time = shift || 0;
 +
 +    unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
 +        return ( 0, $self->loc("Permission Denied") );
 +    }
 +
 +    #We create a date object to catch date weirdness
 +    my $time_obj = RT::Date->new( $self->CurrentUser() );
 +    if ( $time ) {
 +        $time_obj->Set( Format => 'ISO', Value => $time );
 +    }
 +    else {
 +        $time_obj->SetToNow();
 +    }
 +
 +    # We need $TicketAsSystem, in case the current user doesn't have
 +    # ShowTicket
 +    my $TicketAsSystem = RT::Ticket->new(RT->SystemUser);
 +    $TicketAsSystem->Load( $self->Id );
 +    # Now that we're starting, open this ticket
 +    # TODO: do we really want to force this as policy? it should be a scrip
 +    my $next = $TicketAsSystem->FirstActiveStatus;
 +
 +    $self->SetStatus( $next ) if defined $next;
 +
 +    return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
 +
 +}
 +
 +
 +
 +=head2 StartedObj
 +
 +  Returns an RT::Date object which contains this ticket's 
 +'Started' time.
 +
 +=cut
 +
 +sub StartedObj {
 +    my $self = shift;
 +
 +    my $time = RT::Date->new( $self->CurrentUser );
 +    $time->Set( Format => 'sql', Value => $self->Started );
 +    return $time;
 +}
 +
 +
 +
 +=head2 StartsObj
 +
 +  Returns an RT::Date object which contains this ticket's 
 +'Starts' time.
 +
 +=cut
 +
 +sub StartsObj {
 +    my $self = shift;
 +
 +    my $time = RT::Date->new( $self->CurrentUser );
 +    $time->Set( Format => 'sql', Value => $self->Starts );
 +    return $time;
 +}
 +
 +
 +
 +=head2 ToldObj
 +
 +  Returns an RT::Date object which contains this ticket's 
 +'Told' time.
 +
 +=cut
 +
 +sub ToldObj {
 +    my $self = shift;
 +
 +    my $time = RT::Date->new( $self->CurrentUser );
 +    $time->Set( Format => 'sql', Value => $self->Told );
 +    return $time;
 +}
 +
 +
 +
 +=head2 ToldAsString
 +
 +A convenience method that returns ToldObj->AsString
 +
 +TODO: This should be deprecated
 +
 +=cut
 +
 +sub ToldAsString {
 +    my $self = shift;
 +    if ( $self->Told ) {
 +        return $self->ToldObj->AsString();
 +    }
 +    else {
 +        return ("Never");
 +    }
 +}
 +
 +
 +
 +=head2 TimeWorkedAsString
 +
 +Returns the amount of time worked on this ticket as a Text String
 +
 +=cut
 +
 +sub TimeWorkedAsString {
 +    my $self = shift;
 +    my $value = $self->TimeWorked;
 +
 +    # return the # of minutes worked turned into seconds and written as
 +    # a simple text string, this is not really a date object, but if we
 +    # diff a number of seconds vs the epoch, we'll get a nice description
 +    # of time worked.
 +    return "" unless $value;
 +    return RT::Date->new( $self->CurrentUser )
 +        ->DurationAsString( $value * 60 );
 +}
 +
 +
 +
 +=head2  TimeLeftAsString
 +
 +Returns the amount of time left on this ticket as a Text String
 +
 +=cut
 +
 +sub TimeLeftAsString {
 +    my $self = shift;
 +    my $value = $self->TimeLeft;
 +    return "" unless $value;
 +    return RT::Date->new( $self->CurrentUser )
 +        ->DurationAsString( $value * 60 );
 +}
 +
 +
 +
 +
 +=head2 Comment
 +
 +Comment on this ticket.
 +Takes a hash with the following attributes:
 +If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
 +comment.
 +
 +MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
 +
 +If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
 +They will, however, be prepared and you'll be able to access them through the TransactionObj
 +
 +Returns: Transaction id, Error Message, Transaction Object
 +(note the different order from Create()!)
 +
 +=cut
 +
 +sub Comment {
 +    my $self = shift;
 +
 +    my %args = ( CcMessageTo  => undef,
 +                 BccMessageTo => undef,
 +                 MIMEObj      => undef,
 +                 Content      => undef,
 +                 TimeTaken => 0,
 +                 DryRun     => 0, 
 +                 @_ );
 +
 +    unless (    ( $self->CurrentUserHasRight('CommentOnTicket') )
 +             or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
 +        return ( 0, $self->loc("Permission Denied"), undef );
 +    }
 +    $args{'NoteType'} = 'Comment';
 +
 +    if ($args{'DryRun'}) {
 +        $RT::Handle->BeginTransaction();
 +        $args{'CommitScrips'} = 0;
 +    }
 +
 +    my @results = $self->_RecordNote(%args);
 +    if ($args{'DryRun'}) {
 +        $RT::Handle->Rollback();
 +    }
 +
 +    return(@results);
 +}
 +
 +
 +=head2 Correspond
 +
 +Correspond on this ticket.
 +Takes a hashref with the following attributes:
 +
 +
 +MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
 +
 +if there's no MIMEObj, Content is used to build a MIME::Entity object
 +
 +If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
 +They will, however, be prepared and you'll be able to access them through the TransactionObj
 +
 +Returns: Transaction id, Error Message, Transaction Object
 +(note the different order from Create()!)
 +
 +
 +=cut
 +
 +sub Correspond {
 +    my $self = shift;
 +    my %args = ( CcMessageTo  => undef,
 +                 BccMessageTo => undef,
 +                 MIMEObj      => undef,
 +                 Content      => undef,
 +                 TimeTaken    => 0,
 +                 @_ );
 +
 +    unless (    ( $self->CurrentUserHasRight('ReplyToTicket') )
 +             or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
 +        return ( 0, $self->loc("Permission Denied"), undef );
 +    }
 +
 +    $args{'NoteType'} = 'Correspond'; 
 +    if ($args{'DryRun'}) {
 +        $RT::Handle->BeginTransaction();
 +        $args{'CommitScrips'} = 0;
 +    }
 +
 +    my @results = $self->_RecordNote(%args);
 +
 +    #Set the last told date to now if this isn't mail from the requestor.
 +    #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
 +    unless ( $self->IsRequestor($self->CurrentUser->id) ) {
 +        my %squelch;
 +        $squelch{$_}++ for map {$_->Content} $self->SquelchMailTo, $results[2]->SquelchMailTo;
 +        $self->_SetTold
 +            if grep {not $squelch{$_}} $self->Requestors->MemberEmailAddresses;
 +    }
 +
 +    if ($args{'DryRun'}) {
 +        $RT::Handle->Rollback();
 +    }
 +
 +    return (@results);
 +
 +}
 +
 +
 +
 +=head2 _RecordNote
 +
 +the meat of both comment and correspond. 
 +
 +Performs no access control checks. hence, dangerous.
 +
 +=cut
 +
 +sub _RecordNote {
 +    my $self = shift;
 +    my %args = ( 
 +        CcMessageTo  => undef,
 +        BccMessageTo => undef,
 +        Encrypt      => undef,
 +        Sign         => undef,
 +        MIMEObj      => undef,
 +        Content      => undef,
 +        NoteType     => 'Correspond',
 +        TimeTaken    => 0,
 +        CommitScrips => 1,
 +        SquelchMailTo => undef,
 +        @_
 +    );
 +
 +    unless ( $args{'MIMEObj'} || $args{'Content'} ) {
 +        return ( 0, $self->loc("No message attached"), undef );
 +    }
 +
 +    unless ( $args{'MIMEObj'} ) {
 +        $args{'MIMEObj'} = MIME::Entity->build(
 +            Data => ( ref $args{'Content'}? $args{'Content'}: [ $args{'Content'} ] )
 +        );
 +    }
 +
 +    # convert text parts into utf-8
 +    RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
 +
 +    # If we've been passed in CcMessageTo and BccMessageTo fields,
 +    # add them to the mime object for passing on to the transaction handler
 +    # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and
 +    # RT-Send-Bcc: headers
 +
 +
 +    foreach my $type (qw/Cc Bcc/) {
 +        if ( defined $args{ $type . 'MessageTo' } ) {
 +
 +            my $addresses = join ', ', (
 +                map { RT::User->CanonicalizeEmailAddress( $_->address ) }
 +                    Email::Address->parse( $args{ $type . 'MessageTo' } ) );
 +            $args{'MIMEObj'}->head->replace( 'RT-Send-' . $type, Encode::encode_utf8( $addresses ) );
 +        }
 +    }
 +
 +    foreach my $argument (qw(Encrypt Sign)) {
 +        $args{'MIMEObj'}->head->replace(
 +            "X-RT-$argument" => Encode::encode_utf8( $args{ $argument } )
 +        ) if defined $args{ $argument };
 +    }
 +
 +    # If this is from an external source, we need to come up with its
 +    # internal Message-ID now, so all emails sent because of this
 +    # message have a common Message-ID
 +    my $org = RT->Config->Get('Organization');
 +    my $msgid = $args{'MIMEObj'}->head->get('Message-ID');
 +    unless (defined $msgid && $msgid =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$org\E>/) {
 +        $args{'MIMEObj'}->head->set(
 +            'RT-Message-ID' => RT::Interface::Email::GenMessageId( Ticket => $self )
 +        );
 +    }
 +
 +    #Record the correspondence (write the transaction)
 +    my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
 +             Type => $args{'NoteType'},
 +             Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
 +             TimeTaken => $args{'TimeTaken'},
 +             MIMEObj   => $args{'MIMEObj'}, 
 +             CommitScrips => $args{'CommitScrips'},
 +             SquelchMailTo => $args{'SquelchMailTo'},
 +    );
 +
 +    unless ($Trans) {
 +        $RT::Logger->err("$self couldn't init a transaction $msg");
 +        return ( $Trans, $self->loc("Message could not be recorded"), undef );
 +    }
 +
 +    return ( $Trans, $self->loc("Message recorded"), $TransObj );
 +}
 +
 +
 +=head2 DryRun
 +
 +Builds a MIME object from the given C<UpdateSubject> and
 +C<UpdateContent>, then calls L</Comment> or L</Correspond> with
 +C<< DryRun => 1 >>, and returns the transaction so produced.
 +
 +=cut
 +
 +sub DryRun {
 +    my $self = shift;
 +    my %args = @_;
 +    my $action;
 +    if (($args{'UpdateType'} || $args{Action}) =~ /^respon(d|se)$/i ) {
 +        $action = 'Correspond';
 +    } else {
 +        $action = 'Comment';
 +    }
 +
 +    my $Message = MIME::Entity->build(
 +        Type    => 'text/plain',
 +        Subject => defined $args{UpdateSubject} ? Encode::encode_utf8( $args{UpdateSubject} ) : "",
 +        Charset => 'UTF-8',
 +        Data    => $args{'UpdateContent'} || "",
 +    );
 +
 +    my ( $Transaction, $Description, $Object ) = $self->$action(
 +        CcMessageTo  => $args{'UpdateCc'},
 +        BccMessageTo => $args{'UpdateBcc'},
 +        MIMEObj      => $Message,
 +        TimeTaken    => $args{'UpdateTimeWorked'},
 +        DryRun       => 1,
 +    );
 +    unless ( $Transaction ) {
 +        $RT::Logger->error("Couldn't fire '$action' action: $Description");
 +    }
 +
 +    return $Object;
 +}
 +
 +=head2 DryRunCreate
 +
 +Prepares a MIME mesage with the given C<Subject>, C<Cc>, and
 +C<Content>, then calls L</Create> with C<< DryRun => 1 >> and returns
 +the resulting L<RT::Transaction>.
 +
 +=cut
 +
 +sub DryRunCreate {
 +    my $self = shift;
 +    my %args = @_;
 +    my $Message = MIME::Entity->build(
 +        Type    => 'text/plain',
 +        Subject => defined $args{Subject} ? Encode::encode_utf8( $args{'Subject'} ) : "",
 +        (defined $args{'Cc'} ?
 +             ( Cc => Encode::encode_utf8( $args{'Cc'} ) ) : ()),
 +        Charset => 'UTF-8',
 +        Data    => $args{'Content'} || "",
 +    );
 +
 +    my ( $Transaction, $Object, $Description ) = $self->Create(
 +        Type            => $args{'Type'} || 'ticket',
 +        Queue           => $args{'Queue'},
 +        Owner           => $args{'Owner'},
 +        Requestor       => $args{'Requestors'},
 +        Cc              => $args{'Cc'},
 +        AdminCc         => $args{'AdminCc'},
 +        InitialPriority => $args{'InitialPriority'},
 +        FinalPriority   => $args{'FinalPriority'},
 +        TimeLeft        => $args{'TimeLeft'},
 +        TimeEstimated   => $args{'TimeEstimated'},
 +        TimeWorked      => $args{'TimeWorked'},
 +        Subject         => $args{'Subject'},
 +        Status          => $args{'Status'},
 +        MIMEObj         => $Message,
 +        DryRun          => 1,
 +    );
 +    unless ( $Transaction ) {
 +        $RT::Logger->error("Couldn't fire Create action: $Description");
 +    }
 +
 +    return $Object;
 +}
 +
 +
 +
 +sub _Links {
 +    my $self = shift;
 +
 +    #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
 +    #tobias meant by $f
 +    my $field = shift;
 +    my $type  = shift || "";
 +
 +    my $cache_key = "$field$type";
 +    return $self->{ $cache_key } if $self->{ $cache_key };
 +
 +    my $links = $self->{ $cache_key }
 +              = RT::Links->new( $self->CurrentUser );
 +    unless ( $self->CurrentUserHasRight('ShowTicket') ) {
 +        $links->Limit( FIELD => 'id', VALUE => 0, SUBCLAUSE => 'acl' );
 +        return $links;
 +    }
 +
 +    # Maybe this ticket is a merge ticket
 +    my $limit_on = 'Local'. $field;
 +    # at least to myself
 +    $links->Limit(
 +        FIELD           => $limit_on,
 +        VALUE           => $self->id,
 +        ENTRYAGGREGATOR => 'OR',
 +    );
 +    $links->Limit(
 +        FIELD           => $limit_on,
 +        VALUE           => $_,
 +        ENTRYAGGREGATOR => 'OR',
 +    ) foreach $self->Merged;
 +    $links->Limit(
 +        FIELD => 'Type',
 +        VALUE => $type,
 +    ) if $type;
 +
 +    return $links;
 +}
 +
 +
 +
 +=head2 DeleteLink
 +
 +Delete a link. takes a paramhash of Base, Target, Type, Silent,
 +SilentBase and SilentTarget. Either Base or Target must be null.
 +The null value will be replaced with this ticket\'s id.
 +
 +If Silent is true then no transaction would be recorded, in other
 +case you can control creation of transactions on both base and
 +target with SilentBase and SilentTarget respectively. By default
 +both transactions are created.
 +
 +=cut 
 +
 +sub DeleteLink {
 +    my $self = shift;
 +    my %args = (
 +        Base   => undef,
 +        Target => undef,
 +        Type   => undef,
 +        Silent => undef,
 +        SilentBase   => undef,
 +        SilentTarget => undef,
 +        @_
 +    );
 +
 +    unless ( $args{'Target'} || $args{'Base'} ) {
 +        $RT::Logger->error("Base or Target must be specified");
 +        return ( 0, $self->loc('Either base or target must be specified') );
 +    }
 +
 +    #check acls
 +    my $right = 0;
 +    $right++ if $self->CurrentUserHasRight('ModifyTicket');
 +    if ( !$right && RT->Config->Get( '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->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
 +         ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
 +    {
 +        return ( 0, $self->loc("Permission Denied") );
 +    }
 +
 +    my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
 +    return ( 0, $Msg ) unless $val;
 +
 +    return ( $val, $Msg ) if $args{'Silent'};
 +
 +    my ($direction, $remote_link);
 +
 +    if ( $args{'Base'} ) {
 +        $remote_link = $args{'Base'};
 +        $direction = 'Target';
 +    }
 +    elsif ( $args{'Target'} ) {
 +        $remote_link = $args{'Target'};
 +        $direction = 'Base';
 +    } 
 +
 +    my $remote_uri = RT::URI->new( $self->CurrentUser );
 +    $remote_uri->FromURI( $remote_link );
 +
 +    unless ( $args{ 'Silent'. $direction } ) {
 +        my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
 +            Type      => 'DeleteLink',
 +            Field     => $LINKDIRMAP{$args{'Type'}}->{$direction},
 +            OldValue  => $remote_uri->URI || $remote_link,
 +            TimeTaken => 0
 +        );
 +        $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
 +    }
 +
 +    if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $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->Config->Get('LinkTransactionsRun1Scrip'),
 +            TimeTaken      => 0,
 +        );
 +        $RT::Logger->error("Couldn't create transaction: $Msg") unless $val;
 +    }
 +
 +    return ( $val, $Msg );
 +}
 +
 +
 +
 +=head2 AddLink
 +
 +Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
 +
 +If Silent is true then no transaction would be recorded, in other
 +case you can control creation of transactions on both base and
 +target with SilentBase and SilentTarget respectively. By default
 +both transactions are created.
 +
 +=cut
 +
 +sub AddLink {
 +    my $self = shift;
 +    my %args = ( Target       => '',
 +                 Base         => '',
 +                 Type         => '',
 +                 Silent       => undef,
 +                 SilentBase   => undef,
 +                 SilentTarget => undef,
 +                 @_ );
 +
 +    unless ( $args{'Target'} || $args{'Base'} ) {
 +        $RT::Logger->error("Base or Target must be specified");
 +        return ( 0, $self->loc('Either base or target must be specified') );
 +    }
 +
 +    my $right = 0;
 +    $right++ if $self->CurrentUserHasRight('ModifyTicket');
 +    if ( !$right && RT->Config->Get( '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->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
 +         ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
 +    {
 +        return ( 0, $self->loc("Permission Denied") );
 +    }
 +
 +    return ( 0, "Can't link to a deleted ticket" )
 +      if $other_ticket && $other_ticket->Status eq 'deleted';
 +
 +    return $self->_AddLink(%args);
 +}
 +
 +sub __GetTicketFromURI {
 +    my $self = shift;
 +    my %args = ( URI => '', @_ );
 +
 +    # If the other URI is an RT::Ticket, we want to make sure the user
 +    # can modify it too...
 +    my $uri_obj = RT::URI->new( $self->CurrentUser );
 +    $uri_obj->FromURI( $args{'URI'} );
 +
 +    unless ( $uri_obj->Resolver && $uri_obj->Scheme ) {
 +        my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} );
 +        $RT::Logger->warning( $msg );
 +        return( 0, $msg );
 +    }
 +    my $obj = $uri_obj->Resolver->Object;
 +    unless ( UNIVERSAL::isa($obj, 'RT::Ticket') && $obj->id ) {
 +        return (1, 'Found not a ticket', undef);
 +    }
 +    return (1, 'Found ticket', $obj);
 +}
 +
 +=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,
 +                 SilentBase   => undef,
 +                 SilentTarget => undef,
 +                 @_ );
 +
 +    my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
 +    return ($val, $msg) if !$val || $exist;
 +    return ($val, $msg) if $args{'Silent'};
 +
 +    my ($direction, $remote_link);
 +    if ( $args{'Target'} ) {
 +        $remote_link  = $args{'Target'};
 +        $direction    = 'Base';
 +    } elsif ( $args{'Base'} ) {
 +        $remote_link  = $args{'Base'};
 +        $direction    = 'Target';
 +    }
 +
 +    my $remote_uri = RT::URI->new( $self->CurrentUser );
 +    $remote_uri->FromURI( $remote_link );
 +
 +    unless ( $args{ 'Silent'. $direction } ) {
 +        my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
 +            Type      => 'AddLink',
 +            Field     => $LINKDIRMAP{$args{'Type'}}->{$direction},
 +            NewValue  =>  $remote_uri->URI || $remote_link,
 +            TimeTaken => 0
 +        );
 +        $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
 +    }
 +
 +    if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $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->Config->Get('LinkTransactionsRun1Scrip'),
 +            TimeTaken      => 0,
 +        );
 +        $RT::Logger->error("Couldn't create transaction: $msg") unless $val;
 +    }
 +
 +    return ( $val, $msg );
 +}
 +
 +
 +
 +
 +=head2 MergeInto
 +
 +MergeInto take the id of the ticket to merge this ticket into.
 +
 +=cut
 +
 +sub MergeInto {
 +    my $self      = shift;
 +    my $ticket_id = shift;
 +
 +    unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
 +        return ( 0, $self->loc("Permission Denied") );
 +    }
 +
 +    # Load up the new ticket.
 +    my $MergeInto = RT::Ticket->new($self->CurrentUser);
 +    $MergeInto->Load($ticket_id);
 +
 +    # make sure it exists.
 +    unless ( $MergeInto->Id ) {
 +        return ( 0, $self->loc("New ticket doesn't exist") );
 +    }
 +
 +    # Make sure the current user can modify the new ticket.
 +    unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
 +        return ( 0, $self->loc("Permission Denied") );
 +    }
 +
 +    delete $MERGE_CACHE{'effective'}{ $self->id };
 +    delete @{ $MERGE_CACHE{'merged'} }{
 +        $ticket_id, $MergeInto->id, $self->id
 +    };
 +
 +    $RT::Handle->BeginTransaction();
 +
 +    $self->_MergeInto( $MergeInto );
 +
 +    $RT::Handle->Commit();
 +
 +    return ( 1, $self->loc("Merge Successful") );
 +}
 +
 +sub _MergeInto {
 +    my $self      = shift;
 +    my $MergeInto = shift;
 +
 +
 +    # We use EffectiveId here even though it duplicates information from
 +    # the links table becasue of the massive performance hit we'd take
 +    # by trying to do a separate database query for merge info everytime 
 +    # loaded a ticket. 
 +
 +    #update this ticket's effective id to the new ticket's id.
 +    my ( $id_val, $id_msg ) = $self->__Set(
 +        Field => 'EffectiveId',
 +        Value => $MergeInto->Id()
 +    );
 +
 +    unless ($id_val) {
 +        $RT::Handle->Rollback();
 +        return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
 +    }
 +
 +
 +    my $force_status = $self->QueueObj->Lifecycle->DefaultOnMerge;
 +    if ( $force_status && $force_status ne $self->__Value('Status') ) {
 +        my ( $status_val, $status_msg )
 +            = $self->__Set( Field => 'Status', Value => $force_status );
 +
 +        unless ($status_val) {
 +            $RT::Handle->Rollback();
 +            $RT::Logger->error(
 +                "Couldn't set status to $force_status. RT's Database may be inconsistent."
 +            );
 +            return ( 0, $self->loc("Merge failed. Couldn't set Status") );
 +        }
 +    }
 +
 +    # update all the links that point to that old ticket
 +    my $old_links_to = RT::Links->new($self->CurrentUser);
 +    $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
 +
 +    my %old_seen;
 +    while (my $link = $old_links_to->Next) {
 +        if (exists $old_seen{$link->Base."-".$link->Type}) {
 +            $link->Delete;
 +        }   
 +        elsif ($link->Base eq $MergeInto->URI) {
 +            $link->Delete;
 +        } else {
 +            # First, make sure the link doesn't already exist. then move it over.
 +            my $tmp = RT::Link->new(RT->SystemUser);
 +            $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
 +            if ($tmp->id)   {
 +                    $link->Delete;
 +            } else { 
 +                $link->SetTarget($MergeInto->URI);
 +                $link->SetLocalTarget($MergeInto->id);
 +            }
 +            $old_seen{$link->Base."-".$link->Type} =1;
 +        }
 +
 +    }
 +
 +    my $old_links_from = RT::Links->new($self->CurrentUser);
 +    $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
 +
 +    while (my $link = $old_links_from->Next) {
 +        if (exists $old_seen{$link->Type."-".$link->Target}) {
 +            $link->Delete;
 +        }   
 +        if ($link->Target eq $MergeInto->URI) {
 +            $link->Delete;
 +        } else {
 +            # First, make sure the link doesn't already exist. then move it over.
 +            my $tmp = RT::Link->new(RT->SystemUser);
 +            $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
 +            if ($tmp->id)   {
 +                    $link->Delete;
 +            } else { 
 +                $link->SetBase($MergeInto->URI);
 +                $link->SetLocalBase($MergeInto->id);
 +                $old_seen{$link->Type."-".$link->Target} =1;
 +            }
 +        }
 +
 +    }
 +
 +    # Update time fields
 +    foreach my $type (qw(TimeEstimated TimeWorked TimeLeft)) {
 +
 +        my $mutator = "Set$type";
 +        $MergeInto->$mutator(
 +            ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
 +
 +    }
 +#add all of this ticket's watchers to that ticket.
 +    foreach my $watcher_type (qw(Requestors Cc AdminCc)) {
 +
 +        my $people = $self->$watcher_type->MembersObj;
 +        my $addwatcher_type =  $watcher_type;
 +        $addwatcher_type  =~ s/s$//;
 +
 +        while ( my $watcher = $people->Next ) {
 +            
 +           my ($val, $msg) =  $MergeInto->_AddWatcher(
 +                Type        => $addwatcher_type,
 +                Silent => 1,
 +                PrincipalId => $watcher->MemberId
 +            );
 +            unless ($val) {
 +                $RT::Logger->debug($msg);
 +            }
 +    }
 +
 +    }
 +
 +    #find all of the tickets that were merged into this ticket. 
 +    my $old_mergees = RT::Tickets->new( $self->CurrentUser );
 +    $old_mergees->Limit(
 +        FIELD    => 'EffectiveId',
 +        OPERATOR => '=',
 +        VALUE    => $self->Id
 +    );
 +
 +    #   update their EffectiveId fields to the new ticket's id
 +    while ( my $ticket = $old_mergees->Next() ) {
 +        my ( $val, $msg ) = $ticket->__Set(
 +            Field => 'EffectiveId',
 +            Value => $MergeInto->Id()
 +        );
 +    }
 +
 +    #make a new link: this ticket is merged into that other ticket.
 +    $self->AddLink( Type   => 'MergedInto', Target => $MergeInto->Id());
 +
 +    $MergeInto->_SetLastUpdated;    
 +}
 +
 +=head2 Merged
 +
 +Returns list of tickets' ids that's been merged into this ticket.
 +
 +=cut
 +
 +sub Merged {
 +    my $self = shift;
 +
 +    my $id = $self->id;
 +    return @{ $MERGE_CACHE{'merged'}{ $id } }
 +        if $MERGE_CACHE{'merged'}{ $id };
 +
 +    my $mergees = RT::Tickets->new( $self->CurrentUser );
 +    $mergees->Limit(
 +        FIELD    => 'EffectiveId',
 +        VALUE    => $id,
 +    );
 +    $mergees->Limit(
 +        FIELD    => 'id',
 +        OPERATOR => '!=',
 +        VALUE    => $id,
 +    );
 +    return @{ $MERGE_CACHE{'merged'}{ $id } ||= [] }
 +        = map $_->id, @{ $mergees->ItemsArrayRef || [] };
 +}
 +
 +
 +
 +
 +
 +=head2 OwnerObj
 +
 +Takes nothing and returns an RT::User object of 
 +this ticket's owner
 +
 +=cut
 +
 +sub OwnerObj {
 +    my $self = shift;
 +
 +    #If this gets ACLed, we lose on a rights check in User.pm and
 +    #get deep recursion. if we need ACLs here, we need
 +    #an equiv without ACLs
 +
 +    my $owner = RT::User->new( $self->CurrentUser );
 +    $owner->Load( $self->__Value('Owner') );
 +
 +    #Return the owner object
 +    return ($owner);
 +}
 +
 +
 +
 +=head2 OwnerAsString
 +
 +Returns the owner's email address
 +
 +=cut
 +
 +sub OwnerAsString {
 +    my $self = shift;
 +    return ( $self->OwnerObj->EmailAddress );
 +
 +}
 +
 +
 +
 +=head2 SetOwner
 +
 +Takes two arguments:
 +     the Id or Name of the owner 
 +and  (optionally) the type of the SetOwner Transaction. It defaults
 +to 'Set'.  'Steal' is also a valid option.
 +
 +
 +=cut
 +
 +sub SetOwner {
 +    my $self     = shift;
 +    my $NewOwner = shift;
 +    my $Type     = shift || "Set";
 +
 +    $RT::Handle->BeginTransaction();
 +
 +    $self->_SetLastUpdated(); # lock the ticket
 +    $self->Load( $self->id ); # in case $self changed while waiting for lock
 +
 +    my $OldOwnerObj = $self->OwnerObj;
 +
 +    my $NewOwnerObj = RT::User->new( $self->CurrentUser );
 +    $NewOwnerObj->Load( $NewOwner );
 +    unless ( $NewOwnerObj->Id ) {
 +        $RT::Handle->Rollback();
 +        return ( 0, $self->loc("That user does not exist") );
 +    }
 +
 +
 +    # must have ModifyTicket rights
 +    # or TakeTicket/StealTicket and $NewOwner is self
 +    # see if it's a take
 +    if ( $OldOwnerObj->Id == RT->Nobody->Id ) {
 +        unless (    $self->CurrentUserHasRight('ModifyTicket')
 +                 || $self->CurrentUserHasRight('TakeTicket') ) {
 +            $RT::Handle->Rollback();
 +            return ( 0, $self->loc("Permission Denied") );
 +        }
 +    }
 +
 +    # see if it's a steal
 +    elsif (    $OldOwnerObj->Id != RT->Nobody->Id
 +            && $OldOwnerObj->Id != $self->CurrentUser->id ) {
 +
 +        unless (    $self->CurrentUserHasRight('ModifyTicket')
 +                 || $self->CurrentUserHasRight('StealTicket') ) {
 +            $RT::Handle->Rollback();
 +            return ( 0, $self->loc("Permission Denied") );
 +        }
 +    }
 +    else {
 +        unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
 +            $RT::Handle->Rollback();
 +            return ( 0, $self->loc("Permission Denied") );
 +        }
 +    }
 +
 +    # If we're not stealing and the ticket has an owner and it's not
 +    # the current user
 +    if ( $Type ne 'Steal' and $Type ne 'Force'
 +         and $OldOwnerObj->Id != RT->Nobody->Id
 +         and $OldOwnerObj->Id != $self->CurrentUser->Id )
 +    {
 +        $RT::Handle->Rollback();
 +        return ( 0, $self->loc("You can only take tickets that are unowned") )
 +            if $NewOwnerObj->id == $self->CurrentUser->id;
 +        return (
 +            0,
 +            $self->loc("You can only reassign tickets that you own or that are unowned" )
 +        );
 +    }
 +
 +    #If we've specified a new owner and that user can't modify the ticket
 +    elsif ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ) {
 +        $RT::Handle->Rollback();
 +        return ( 0, $self->loc("That user may not own tickets in that queue") );
 +    }
 +
 +    # If the ticket has an owner and it's the new owner, we don't need
 +    # To do anything
 +    elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
 +        $RT::Handle->Rollback();
 +        return ( 0, $self->loc("That user already owns that ticket") );
 +    }
 +
 +    # Delete the owner in the owner group, then add a new one
 +    # TODO: is this safe? it's not how we really want the API to work
 +    # for most things, but it's fast.
 +    my ( $del_id, $del_msg );
 +    for my $owner (@{$self->OwnerGroup->MembersObj->ItemsArrayRef}) {
 +        ($del_id, $del_msg) = $owner->Delete();
 +        last unless ($del_id);
 +    }
 +
 +    unless ($del_id) {
 +        $RT::Handle->Rollback();
 +        return ( 0, $self->loc("Could not change owner: [_1]", $del_msg) );
 +    }
 +
 +    my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
 +                                       PrincipalId => $NewOwnerObj->PrincipalId,
 +                                       InsideTransaction => 1 );
 +    unless ($add_id) {
 +        $RT::Handle->Rollback();
 +        return ( 0, $self->loc("Could not change owner: [_1]", $add_msg ) );
 +    }
 +
 +    # We call set twice with slightly different arguments, so
 +    # as to not have an SQL transaction span two RT transactions
 +
 +    my ( $val, $msg ) = $self->_Set(
 +                      Field             => 'Owner',
 +                      RecordTransaction => 0,
 +                      Value             => $NewOwnerObj->Id,
 +                      TimeTaken         => 0,
 +                      TransactionType   => 'Set',
 +                      CheckACL          => 0,                  # don't check acl
 +    );
 +
 +    unless ($val) {
 +        $RT::Handle->Rollback;
 +        return ( 0, $self->loc("Could not change owner: [_1]", $msg) );
 +    }
 +
 +    ($val, $msg) = $self->_NewTransaction(
 +        Type      => 'Set',
 +        Field     => 'Owner',
 +        NewValue  => $NewOwnerObj->Id,
 +        OldValue  => $OldOwnerObj->Id,
 +        TimeTaken => 0,
 +    );
 +
 +    if ( $val ) {
 +        $msg = $self->loc( "Owner changed from [_1] to [_2]",
 +                           $OldOwnerObj->Name, $NewOwnerObj->Name );
 +    }
 +    else {
 +        $RT::Handle->Rollback();
 +        return ( 0, $msg );
 +    }
 +
 +    $RT::Handle->Commit();
 +
 +    return ( $val, $msg );
 +}
 +
 +
 +
 +=head2 Take
  
 -use strict;
 +A convenince method to set the ticket's owner to the current user
  
 +=cut
  
 -=head1 NAME
 +sub Take {
 +    my $self = shift;
 +    return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
 +}
  
 -RT::Ticket
  
  
 -=head1 SYNOPSIS
 +=head2 Untake
  
 -=head1 DESCRIPTION
 +Convenience method to set the owner to 'nobody' if the current user is the owner.
  
 -=head1 METHODS
 +=cut
 +
 +sub Untake {
 +    my $self = shift;
 +    return ( $self->SetOwner( RT->Nobody->UserObj->Id, 'Untake' ) );
 +}
 +
 +
 +
 +=head2 Steal
 +
 +A convenience method to change the owner of the current ticket to the
 +current user. Even if it's owned by another user.
  
  =cut
  
diff --cc lib/RT/User.pm
index 00b230f,b7ebd5b..56a22cd
mode 100644,100755..100644
--- a/lib/RT/User.pm
+++ b/lib/RT/User.pm
@@@ -60,1577 -68,23 +60,1577 @@@
  
  =cut
  
 +
  package RT::User;
 -use RT::Record; 
  
 +use strict;
 +use warnings;
 +
 +
 +use base 'RT::Record';
 +
 +sub Table {'Users'}
 +
 +
 +
 +
 +
 +
 +use Digest::SHA;
 +use Digest::MD5;
 +use RT::Principals;
 +use RT::ACE;
 +use RT::Interface::Email;
 +use Encode;
 +use Text::Password::Pronounceable;
 +
 +sub _OverlayAccessible {
 +    {
 +
 +        Name                    => { public => 1,  admin => 1 },
 +          Password              => { read   => 0 },
 +          EmailAddress          => { public => 1 },
 +          Organization          => { public => 1,  admin => 1 },
 +          RealName              => { public => 1 },
 +          NickName              => { public => 1 },
 +          Lang                  => { public => 1 },
 +          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 },
 +
 +    }
 +}
 +
 +
 +
 +=head2 Create { PARAMHASH }
 +
 +
 +
 +=cut
 +
 +
 +sub Create {
 +    my $self = shift;
 +    my %args = (
 +        Privileged => 0,
 +        Disabled => 0,
 +        EmailAddress => '',
 +        _RecordTransaction => 1,
 +        @_    # get the real argumentlist
 +    );
 +
 +    # remove the value so it does not cripple SUPER::Create
 +    my $record_transaction = delete $args{'_RecordTransaction'};
 +
 +    #Check the ACL
 +    unless ( $self->CurrentUser->HasRight(Right => 'AdminUsers', Object => $RT::System) ) {
 +        return ( 0, $self->loc('Permission Denied') );
 +    }
 +
 +
 +    unless ($self->CanonicalizeUserInfo(\%args)) {
 +        return ( 0, $self->loc("Could not set user info") );
 +    }
 +
 +    $args{'EmailAddress'} = $self->CanonicalizeEmailAddress($args{'EmailAddress'});
 +
 +    # if the user doesn't have a name defined, set it to the email address
 +    $args{'Name'} = $args{'EmailAddress'} unless ($args{'Name'});
 +
 +
 +
 +    my $privileged = delete $args{'Privileged'};
 +
 +
 +    if ($args{'CryptedPassword'} ) {
 +        $args{'Password'} = $args{'CryptedPassword'};
 +        delete $args{'CryptedPassword'};
 +    } elsif ( !$args{'Password'} ) {
 +        $args{'Password'} = '*NO-PASSWORD*';
 +    } else {
 +        my ($ok, $msg) = $self->ValidatePassword($args{'Password'});
 +        return ($ok, $msg) if !$ok;
 +
 +        $args{'Password'} = $self->_GeneratePassword($args{'Password'});
 +    }
 +
 +    #TODO Specify some sensible defaults.
 +
 +    unless ( $args{'Name'} ) {
 +        return ( 0, $self->loc("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, $self->loc('Name in use') ) if ( $TempUser->Id );
 +
 +        my ($val, $message) = $self->ValidateEmailAddress( $args{'EmailAddress'} );
 +        return (0, $message) unless ( $val );
 +    } else {
 +        $RT::Logger->warning( "$self couldn't check for pre-existing users");
 +    }
 +
 +
 +    $RT::Handle->BeginTransaction();
 +    # Groups deal with principal ids, rather than user ids.
 +    # When creating this user, set up a principal Id for it.
 +    my $principal = RT::Principal->new($self->CurrentUser);
 +    my $principal_id = $principal->Create(PrincipalType => 'User',
 +                                Disabled => $args{'Disabled'},
 +                                ObjectId => '0');
 +    # If we couldn't create a principal Id, get the fuck out.
 +    unless ($principal_id) {
 +        $RT::Handle->Rollback();
 +        $RT::Logger->crit("Couldn't create a Principal on new user create.");
 +        $RT::Logger->crit("Strange things are afoot at the circle K");
 +        return ( 0, $self->loc('Could not create user') );
 +    }
 +
 +    $principal->__Set(Field => 'ObjectId', Value => $principal_id);
 +    delete $args{'Disabled'};
 +
 +    $self->SUPER::Create(id => $principal_id , %args);
 +    my $id = $self->Id;
 +
 +    #If the create failed.
 +    unless ($id) {
 +        $RT::Handle->Rollback();
 +        $RT::Logger->error("Could not create a new user - " .join('-', %args));
 +
 +        return ( 0, $self->loc('Could not create user') );
 +    }
 +
 +    my $aclstash = RT::Group->new($self->CurrentUser);
 +    my $stash_id = $aclstash->_CreateACLEquivalenceGroup($principal);
 +
 +    unless ($stash_id) {
 +        $RT::Handle->Rollback();
 +        $RT::Logger->crit("Couldn't stash the user in groupmembers");
 +        return ( 0, $self->loc('Could not create user') );
 +    }
 +
 +
 +    my $everyone = RT::Group->new($self->CurrentUser);
 +    $everyone->LoadSystemInternalGroup('Everyone');
 +    unless ($everyone->id) {
 +        $RT::Logger->crit("Could not load Everyone group on user creation.");
 +        $RT::Handle->Rollback();
 +        return ( 0, $self->loc('Could not create user') );
 +    }
 +
 +
 +    my ($everyone_id, $everyone_msg) = $everyone->_AddMember( InsideTransaction => 1, PrincipalId => $self->PrincipalId);
 +    unless ($everyone_id) {
 +        $RT::Logger->crit("Could not add user to Everyone group on user creation.");
 +        $RT::Logger->crit($everyone_msg);
 +        $RT::Handle->Rollback();
 +        return ( 0, $self->loc('Could not create user') );
 +    }
 +
 +
 +    my $access_class = RT::Group->new($self->CurrentUser);
 +    if ($privileged)  {
 +        $access_class->LoadSystemInternalGroup('Privileged');
 +    } else {
 +        $access_class->LoadSystemInternalGroup('Unprivileged');
 +    }
 +
 +    unless ($access_class->id) {
 +        $RT::Logger->crit("Could not load Privileged or Unprivileged group on user creation");
 +        $RT::Handle->Rollback();
 +        return ( 0, $self->loc('Could not create user') );
 +    }
 +
 +
 +    my ($ac_id, $ac_msg) = $access_class->_AddMember( InsideTransaction => 1, PrincipalId => $self->PrincipalId);
 +
 +    unless ($ac_id) {
 +        $RT::Logger->crit("Could not add user to Privileged or Unprivileged group on user creation. Aborted");
 +        $RT::Logger->crit($ac_msg);
 +        $RT::Handle->Rollback();
 +        return ( 0, $self->loc('Could not create user') );
 +    }
 +
 +
 +    if ( $record_transaction ) {
 +        $self->_NewTransaction( Type => "Create" );
 +    }
 +
 +    $RT::Handle->Commit;
 +
 +    return ( $id, $self->loc('User created') );
 +}
 +
 +=head2 ValidatePassword STRING
 +
 +Returns either (0, "failure reason") or 1 depending on whether the given
 +password is valid.
 +
 +=cut
 +
 +sub ValidatePassword {
 +    my $self = shift;
 +    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 1;
 +}
 +
 +=head2 SetPrivileged BOOL
 +
 +If passed a true value, makes this user a member of the "Privileged"  PseudoGroup.
 +Otherwise, makes this user a member of the "Unprivileged" pseudogroup.
 +
 +Returns a standard RT tuple of (val, msg);
 +
 +
 +=cut
 +
 +sub SetPrivileged {
 +    my $self = shift;
 +    my $val = shift;
 +
 +    #Check the ACL
 +    unless ( $self->CurrentUser->HasRight(Right => 'AdminUsers', Object => $RT::System) ) {
 +        return ( 0, $self->loc('Permission Denied') );
 +    }
 +
 +    $self->_SetPrivileged($val);
 +}
 +
 +sub _SetPrivileged {
 +    my $self = shift;
 +    my $val = shift;
 +    my $priv = RT::Group->new($self->CurrentUser);
 +    $priv->LoadSystemInternalGroup('Privileged');
 +    unless ($priv->Id) {
 +        $RT::Logger->crit("Could not find Privileged pseudogroup");
 +        return(0,$self->loc("Failed to find 'Privileged' users pseudogroup."));
 +    }
 +
 +    my $unpriv = RT::Group->new($self->CurrentUser);
 +    $unpriv->LoadSystemInternalGroup('Unprivileged');
 +    unless ($unpriv->Id) {
 +        $RT::Logger->crit("Could not find unprivileged pseudogroup");
 +        return(0,$self->loc("Failed to find 'Unprivileged' users pseudogroup"));
 +    }
 +
 +    my $principal = $self->PrincipalId;
 +    if ($val) {
 +        if ($priv->HasMember($principal)) {
 +            #$RT::Logger->debug("That user is already privileged");
 +            return (0,$self->loc("That user is already privileged"));
 +        }
 +        if ($unpriv->HasMember($principal)) {
 +            $unpriv->_DeleteMember($principal);
 +        } else {
 +        # if we had layered transactions, life would be good
 +        # sadly, we have to just go ahead, even if something
 +        # bogus happened
 +            $RT::Logger->crit("User ".$self->Id." is neither privileged nor ".
 +                "unprivileged. something is drastically wrong.");
 +        }
 +        my ($status, $msg) = $priv->_AddMember( InsideTransaction => 1, PrincipalId => $principal);
 +        if ($status) {
 +            return (1, $self->loc("That user is now privileged"));
 +        } else {
 +            return (0, $msg);
 +        }
 +    } else {
 +        if ($unpriv->HasMember($principal)) {
 +            #$RT::Logger->debug("That user is already unprivileged");
 +            return (0,$self->loc("That user is already unprivileged"));
 +        }
 +        if ($priv->HasMember($principal)) {
 +            $priv->_DeleteMember( $principal );
 +        } else {
 +        # if we had layered transactions, life would be good
 +        # sadly, we have to just go ahead, even if something
 +        # bogus happened
 +            $RT::Logger->crit("User ".$self->Id." is neither privileged nor ".
 +                "unprivileged. something is drastically wrong.");
 +        }
 +        my ($status, $msg) = $unpriv->_AddMember( InsideTransaction => 1, PrincipalId => $principal);
 +        if ($status) {
 +            return (1, $self->loc("That user is now unprivileged"));
 +        } else {
 +            return (0, $msg);
 +        }
 +    }
 +}
 +
 +=head2 Privileged
 +
 +Returns true if this user is privileged. Returns undef otherwise.
 +
 +=cut
 +
 +sub Privileged {
 +    my $self = shift;
 +    if ( RT->PrivilegedUsers->HasMember( $self->id ) ) {
 +        return(1);
 +    } else {
 +        return(undef);
 +    }
 +}
 +
 +#create a user without validating _any_ data.
 +
 +#To be used only on database init.
 +# We can't localize here because it's before we _have_ a loc framework
 +
 +sub _BootstrapCreate {
 +    my $self = shift;
 +    my %args = (@_);
 +
 +    $args{'Password'} = '*NO-PASSWORD*';
 +
 +
 +    $RT::Handle->BeginTransaction();
 +
 +    # Groups deal with principal ids, rather than user ids.
 +    # When creating this user, set up a principal Id for it.
 +    my $principal = RT::Principal->new($self->CurrentUser);
 +    my $principal_id = $principal->Create(PrincipalType => 'User', ObjectId => '0');
 +    $principal->__Set(Field => 'ObjectId', Value => $principal_id);
 +
 +    # If we couldn't create a principal Id, get the fuck out.
 +    unless ($principal_id) {
 +        $RT::Handle->Rollback();
 +        $RT::Logger->crit("Couldn't create a Principal on new user create. Strange things are afoot at the circle K");
 +        return ( 0, 'Could not create user' );
 +    }
 +    $self->SUPER::Create(id => $principal_id, %args);
 +    my $id = $self->Id;
 +    #If the create failed.
 +      unless ($id) {
 +      $RT::Handle->Rollback();
 +      return ( 0, 'Could not create user' ) ; #never loc this
 +    }
 +
 +    my $aclstash = RT::Group->new($self->CurrentUser);
 +    my $stash_id  = $aclstash->_CreateACLEquivalenceGroup($principal);
 +
 +    unless ($stash_id) {
 +        $RT::Handle->Rollback();
 +        $RT::Logger->crit("Couldn't stash the user in groupmembers");
 +        return ( 0, $self->loc('Could not create user') );
 +    }
 +
 +    $RT::Handle->Commit();
 +
 +    return ( $id, 'User created' );
 +}
 +
 +sub Delete {
 +    my $self = shift;
 +
 +    return ( 0, $self->loc('Deleting this object would violate referential integrity') );
 +
 +}
 +
 +=head2 Load
 +
 +Load a user object from the database. Takes a single argument.
 +If the argument is numerical, load by the column 'id'. If a user
 +object or its subclass passed then loads the same user by 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 ( $identifier !~ /\D/ ) {
 +        return $self->SUPER::LoadById( $identifier );
 +    } elsif ( UNIVERSAL::isa( $identifier, 'RT::User' ) ) {
 +        return $self->SUPER::LoadById( $identifier->Id );
 +    } else {
 +        return $self->LoadByCol( "Name", $identifier );
 +    }
 +}
 +
 +=head2 LoadByEmail
 +
 +Tries to load this user object from the database by the user's email address.
 +
 +=cut
 +
 +sub LoadByEmail {
 +    my $self    = shift;
 +    my $address = shift;
 +
 +    # Never load an empty address as an email address.
 +    unless ($address) {
 +        return (undef);
 +    }
 +
 +    $address = $self->CanonicalizeEmailAddress($address);
 +
 +    #$RT::Logger->debug("Trying to load an email address: $address");
 +    return $self->LoadByCol( "EmailAddress", $address );
 +}
 +
 +=head2 LoadOrCreateByEmail ADDRESS
 +
 +Attempts to find a user who has the provided email address. If that fails, creates an unprivileged user with
 +the provided email address and loads them. Address can be provided either as L<Email::Address> object
 +or string which is parsed using the module.
 +
 +Returns a tuple of the user's id and a status message.
 +0 will be returned in place of the user's id in case of failure.
 +
 +=cut
 +
 +sub LoadOrCreateByEmail {
 +    my $self = shift;
 +    my $email = shift;
 +
 +    my ($message, $name);
 +    if ( UNIVERSAL::isa( $email => 'Email::Address' ) ) {
 +        ($email, $name) = ($email->address, $email->phrase);
 +    } else {
 +        ($email, $name) = RT::Interface::Email::ParseAddressFromHeader( $email );
 +    }
 +
 +    $self->LoadByEmail( $email );
 +    $self->Load( $email ) unless $self->Id;
 +    $message = $self->loc('User loaded');
 +
 +    unless( $self->Id ) {
 +        my $val;
 +        ($val, $message) = $self->Create(
 +            Name         => $email,
 +            EmailAddress => $email,
 +            RealName     => $name,
 +            Privileged   => 0,
 +            Comments     => 'Autocreated when added as a watcher',
 +        );
 +        unless ( $val ) {
 +            # Deal with the race condition of two account creations at once
 +            $self->LoadByEmail( $email );
 +            unless ( $self->Id ) {
 +                sleep 5;
 +                $self->LoadByEmail( $email );
 +            }
 +            if ( $self->Id ) {
 +                $RT::Logger->error("Recovered from creation failure due to race condition");
 +                $message = $self->loc("User loaded");
 +            } else {
 +                $RT::Logger->crit("Failed to create user ". $email .": " .$message);
 +            }
 +        }
 +    }
 +    return (0, $message) unless $self->id;
 +    return ($self->Id, $message);
 +}
 +
 +=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;
 +
 +    # if the email address is null, it's always valid
 +    return (1) if ( !$Value || $Value eq "" );
 +
 +    if ( RT->Config->Get('ValidateUserEmailAddresses') ) {
 +        # We only allow one valid email address
 +        my @addresses = Email::Address->parse($Value);
 +        return ( 0, $self->loc('Invalid syntax for email address') ) unless ( ( scalar (@addresses) == 1 ) && ( $addresses[0]->address ) );
 +    }
 +
 +
 +    my $TempUser = RT::User->new(RT->SystemUser);
 +    $TempUser->LoadByEmail($Value);
 +
 +    if ( $TempUser->id && ( !$self->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 ( 0, $self->loc('Email address in use') );
 +    } else {    #it's a valid email address
 +        return (1);
 +    }
 +}
 +
 +=head2 SetEmailAddress
 +
 +Check to make sure someone else isn't using this email address already
 +so that a better email address can be returned
 +
 +=cut
 +
 +sub SetEmailAddress {
 +    my $self  = shift;
 +    my $Value = shift;
 +    $Value = '' unless defined $Value;
 +
 +    my ($val, $message) = $self->ValidateEmailAddress( $Value );
 +    if ( $val ) {
 +        return $self->_Set( Field => 'EmailAddress', Value => $Value );
 +    } else {
 +        return ( 0, $message )
 +    }
 +
 +}
 +
 +=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.
 +
 +=item empty string returned otherwise.
 +
 +=back
 +
 +=cut
 +
 +sub EmailFrequency {
 +    my $self = shift;
 +    my %args = (
 +        Ticket => undef,
 +        @_
 +    );
 +    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;
 +    my $frequency = RT->Config->Get( 'EmailFrequency', $self ) || '';
 +    return 'daily' if $frequency =~ /daily/i;
 +    return 'weekly' if $frequency =~ /weekly/i;
 +    return '';
 +}
 +
 +=head2 CanonicalizeEmailAddress ADDRESS
 +
 +CanonicalizeEmailAddress converts email addresses into canonical form.
 +it takes one email address in and returns the proper canonical
 +form. You can dump whatever your proper local config is in here.  Note
 +that it may be called as a static method; in this case the first argument
 +is class name not an object.
 +
 +=cut
 +
 +sub CanonicalizeEmailAddress {
 +    my $self = shift;
 +    my $email = shift;
 +    # Example: the following rule would treat all email
 +    # coming from a subdomain as coming from second level domain
 +    # foo.com
 +    if ( my $match   = RT->Config->Get('CanonicalizeEmailAddressMatch') and
 +         my $replace = RT->Config->Get('CanonicalizeEmailAddressReplace') )
 +    {
 +        $email =~ s/$match/$replace/gi;
 +    }
 +    return ($email);
 +}
 +
 +=head2 CanonicalizeUserInfo HASH of ARGS
 +
 +CanonicalizeUserInfo can convert all User->Create options.
 +it takes a hashref of all the params sent to User->Create and
 +returns that same hash, by default nothing is done.
 +
 +This function is intended to allow users to have their info looked up via
 +an outside source and modified upon creation.
 +
 +=cut
 +
 +sub CanonicalizeUserInfo {
 +    my $self = shift;
 +    my $args = shift;
 +    my $success = 1;
 +
 +    return ($success);
 +}
 +
 +
 +=head2 Password and authentication related functions
 +
 +=head3 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
 +
 +sub SetRandomPassword {
 +    my $self = shift;
 +
 +    unless ( $self->CurrentUserCanModify('Password') ) {
 +        return ( 0, $self->loc("Permission Denied") );
 +    }
 +
 +
 +    my $min = ( RT->Config->Get('MinimumPasswordLength') > 6 ?  RT->Config->Get('MinimumPasswordLength') : 6);
 +    my $max = ( RT->Config->Get('MinimumPasswordLength') > 8 ?  RT->Config->Get('MinimumPasswordLength') : 8);
 +
 +    my $pass = $self->GenerateRandomPassword( $min, $max) ;
 +
 +    # 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 );
 +
 +}
 +
 +=head3 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 "PasswordChange".
 +
 +This function is currently unused in the UI, but available for local scripts.
 +
 +=cut
 +
 +sub ResetPassword {
 +    my $self = shift;
 +
 +    unless ( $self->CurrentUserCanModify('Password') ) {
 +        return ( 0, $self->loc("Permission Denied") );
 +    }
 +    my ( $status, $pass ) = $self->SetRandomPassword();
 +
 +    unless ($status) {
 +        return ( 0, "$pass" );
 +    }
 +
 +    my $ret = RT::Interface::Email::SendEmailUsingTemplate(
 +        To        => $self->EmailAddress,
 +        Template  => 'PasswordChange',
 +        Arguments => {
 +            NewPassword => $pass,
 +        },
 +    );
 +
 +    if ($ret) {
 +        return ( 1, $self->loc('New password notification sent') );
 +    } else {
 +        return ( 0, $self->loc('Notification could not be sent') );
 +    }
 +
 +}
 +
 +=head3 GenerateRandomPassword MIN_LEN and MAX_LEN
 +
 +Returns a random password between MIN_LEN and MAX_LEN characters long.
 +
 +=cut
 +
 +sub GenerateRandomPassword {
 +    my $self = shift;   # just to drop it
 +    return Text::Password::Pronounceable->generate(@_);
 +}
 +
 +sub SafeSetPassword {
 +    my $self = shift;
 +    my %args = (
 +        Current      => undef,
 +        New          => undef,
 +        Confirmation => undef,
 +        @_,
 +    );
 +    return (1) unless defined $args{'New'} && length $args{'New'};
 +
 +    my %cond = $self->CurrentUserRequireToSetPassword;
 +
 +    unless ( $cond{'CanSet'} ) {
 +        return (0, $self->loc('You can not set password.') .' '. $cond{'Reason'} );
 +    }
 +
 +    my $error = '';
 +    if ( $cond{'RequireCurrent'} && !$self->CurrentUser->IsPassword($args{'Current'}) ) {
 +        if ( defined $args{'Current'} && length $args{'Current'} ) {
 +            $error = $self->loc("Please enter your current password correctly.");
 +        } else {
 +            $error = $self->loc("Please enter your current password.");
 +        }
 +    } elsif ( $args{'New'} ne $args{'Confirmation'} ) {
 +        $error = $self->loc("Passwords do not match.");
 +    }
 +
 +    if ( $error ) {
 +        $error .= ' '. $self->loc('Password has not been set.');
 +        return (0, $error);
 +    }
 +
 +    return $self->SetPassword( $args{'New'} );
 +}
 +
 +=head3 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, $self->loc('Password: Permission Denied') );
 +    }
 +
 +    if ( !$password ) {
 +        return ( 0, $self->loc("No password set") );
 +    } else {
 +        my ($val, $msg) = $self->ValidatePassword($password);
 +        return ($val, $msg) if !$val;
 +
 +        my $new = !$self->HasPassword;
 +        $password = $self->_GeneratePassword($password);
 +
 +        ( $val, $msg ) = $self->_Set(Field => 'Password', Value => $password);
 +        if ($val) {
 +            return ( 1, $self->loc("Password set") ) if $new;
 +            return ( 1, $self->loc("Password changed") );
 +        } else {
 +            return ( $val, $msg );
 +        }
 +    }
 +
 +}
 +
 +sub _GeneratePassword_sha512 {
 +    my $self = shift;
 +    my ($password, $salt) = @_;
 +
 +    # Generate a 16-character base64 salt
 +    unless ($salt) {
 +        $salt = "";
 +        $salt .= ("a".."z", "A".."Z","0".."9", "+", "/")[rand 64]
 +            for 1..16;
 +    }
 +
 +    my $sha = Digest::SHA->new(512);
 +    $sha->add($salt);
 +    $sha->add(encode_utf8($password));
 +    return join("!", "", "sha512", $salt, $sha->b64digest);
 +}
 +
 +=head3 _GeneratePassword PASSWORD [, SALT]
 +
 +Returns a string to store in the database.  This string takes the form:
 +
 +   !method!salt!hash
 +
 +By default, the method is currently C<sha512>.
 +
 +=cut
 +
 +sub _GeneratePassword {
 +    my $self = shift;
 +    return $self->_GeneratePassword_sha512(@_);
 +}
 +
 +=head3 HasPassword
 +
 +Returns true if the user has a valid password, otherwise returns false.
 +
 +=cut
 +
 +sub HasPassword {
 +    my $self = shift;
 +    my $pwd = $self->__Value('Password');
 +    return undef if !defined $pwd
 +                    || $pwd eq ''
 +                    || $pwd eq '*NO-PASSWORD*';
 +    return 1;
 +}
 +
 +=head3 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->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->warn("Unknown password form");
++        $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.
  
 -use vars qw( @ISA );
 - at ISA= qw( RT::Record );
 +=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>.
  
 -sub _Init {
 -  my $self = shift; 
 +=cut
 +
 +sub ValidateAuthString {
 +    my $self = shift;
 +    my $auth_string = shift;
 +    my $protected = shift;
 +
 +    my $str = $self->AuthToken . $protected;
 +    utf8::encode( $str );
  
 -  $self->Table('Users');
 -  $self->SUPER::_Init(@_);
 +    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
  
diff --cc t/web/attachments.t
index 8c75f6c,87b32a7..0f2c60f
--- a/t/web/attachments.t
+++ b/t/web/attachments.t
@@@ -1,9 -1,10 +1,9 @@@
  #!/usr/bin/perl -w
  use strict;
  
- use RT::Test tests => 25;
 -use RT::Test tests => 17;
 -$RT::Test::SKIP_REQUEST_WORK_AROUND = 1;
++use RT::Test tests => 28;
  
 -use constant LogoFile => $RT::MasonComponentRoot .'/NoAuth/images/bplogo.gif';
 +use constant LogoFile => $RT::MasonComponentRoot .'/NoAuth/images/bpslogo.png';
  use constant FaviconFile => $RT::MasonComponentRoot .'/NoAuth/images/favicon.png';
  
  my ($baseurl, $m) = RT::Test->started_ok;
@@@ -26,10 -27,19 +26,19 @@@ $m->field('Content', 'Some content')
  $m->submit;
  is($m->status, 200, "request successful");
  
 -$m->content_like(qr/Attachments test/, 'we have subject on the page');
 -$m->content_like(qr/Some content/, 'and content');
 -$m->content_like(qr/Download bplogo\.gif/, 'page has file name');
 +$m->content_contains('Attachments test', 'we have subject on the page');
 +$m->content_contains('Some content', 'and content');
 +$m->content_contains('Download bpslogo.png', 'page has file name');
  
+ open LOGO, "<", LogoFile or die "Can't open logo file: $!";
+ binmode LOGO;
+ my $logo_contents = do {local $/; <LOGO>};
+ close LOGO;
 -$m->follow_link_ok({text => "Download bplogo.gif"});
 -is($m->content_type, "image/gif");
++$m->follow_link_ok({text => "Download bpslogo.png"});
++is($m->content_type, "image/png");
+ is($m->content, $logo_contents, "Binary content matches");
+ 
+ $m->back;
  $m->follow_link_ok({text => 'Reply'}, "reply to the ticket");
  $m->form_name('TicketUpdate');
  $m->field('Attach',  LogoFile);

-----------------------------------------------------------------------


More information about the Rt-commit mailing list