[Rt-commit] rt branch, master, updated. rt-4.1.13-276-gdb6c1f5

Alex Vandiver alexmv at bestpractical.com
Fri Jul 5 00:29:48 EDT 2013


The branch, master has been updated
       via  db6c1f52736790972945fbcb19c948ff6cbeb598 (commit)
       via  d59a3cd018f867446f60c88378476de04af8b516 (commit)
       via  dbd5d2c9bba15251d24100117232af18d09da90d (commit)
       via  f8102624808a0e8cc76d2e9a4513d5bf62f02f75 (commit)
       via  288f7d531d3fe84a06ed7fc71dc7c43494f1083a (commit)
       via  f4ca6619e8b3576f6ae44d257f8ac2c2c423d872 (commit)
       via  b37fb52c1d5fc8e788911d9313790cb34e89a376 (commit)
       via  f843c9977d799f336c3ef7dc819f59d9ed499c6f (commit)
       via  c06e0bf755b301f4eadfed035d74c03a20b415a0 (commit)
       via  36ee44b8581b43fa8fd83b74b9696eac16e21ba0 (commit)
       via  9a33ae7f5e9dec93505638189eb2644fbacf4831 (commit)
       via  af919efbe22806191e278d14cf38e77550f85694 (commit)
       via  29b38f697779804e513a4f59bfd6cd8a9012b32e (commit)
       via  59616742cf9fb9b2ccda46c0fe24132355ab693f (commit)
       via  18a4ef86ece73229794e3763d29c80d55fb7a242 (commit)
       via  2ce3591425072e391bc809b177e02e4d721a6f12 (commit)
       via  45a7df37cc576d521d27a99d51e21808ccc9b7d7 (commit)
       via  fbe4a3fd397f893bafe8b07b59ea8bba4775d9e2 (commit)
       via  0ea9d955789bf18e4fc0a824396a56011e58c9ad (commit)
       via  d69d41b0c11b4fc02289cd76ba246055ea4f10be (commit)
       via  7b6ddfb45a5c4c794845fc588806efdbc6daaa77 (commit)
       via  de9278d51aed79e17edd92a7f11d308c4a038643 (commit)
       via  a0b4845c9db82680dfd3af9591e4070683e917cc (commit)
       via  b6ba3bdc2e29c35ab3ac593756ad2aacdd7ae9b4 (commit)
       via  7b12365e082fb93cbae4c75ef52620b99e4e5143 (commit)
       via  891a2fb35943b6e95fb69821593833ad1055a2f2 (commit)
       via  1fdfabc9606dacf82535d52edbcdd98b6bbbc444 (commit)
       via  bf55b9e044bc3ab8be73ac8641ee4a86bc212cf4 (commit)
       via  feeccd1f506458ed2a8aca5612830848a27a0e44 (commit)
       via  cf2c540800f8824d63c6e30aff718e2242a35e72 (commit)
       via  8447e8b59192ceedf65d401a394d5d9bfe4d3039 (commit)
       via  37f0d7e42a413359b5c78057ddc09775694f6fca (commit)
       via  f93a4d3bc6f87bcda670ecde1b0e48264485b48e (commit)
       via  19765b9596dfcb535696a950875ab010a60c5cf2 (commit)
       via  8fe6e357ce2a8025d70dd2fc37c5767ab96342e2 (commit)
       via  147b941122c888b5e4474db5473992c174d2a957 (commit)
       via  fbbd36dba9885920dd2f221c222c3e04b245d41f (commit)
       via  82aa6ab5b62ddbeb78067b5be3c37259370b9b5d (commit)
       via  42bbbfed2e8f1b2615d30a42e9398131de30a3e1 (commit)
       via  68175442e251a8ad244e766d292aca85ce500fa7 (commit)
       via  f95c3bea202be15d62ea3a4e91c6882a461697ff (commit)
       via  9aebb809fe470f5d025853eebe1bfadb99bc0291 (commit)
       via  71fd965909a9245aa33a33c86a07ff3522b56e35 (commit)
       via  0371bcefecf91163082873206787542d2eefb19f (commit)
       via  1e3315a0d100665aef5cfe008d78499c324e0891 (commit)
       via  a9034026f9a20d00520239a87caccc405ea1a70e (commit)
       via  240a9e033394a6d95cb2af4ed6ee452b060ef165 (commit)
       via  1c288588e668fc3c21fd41b7eed8568fdb9c61c4 (commit)
       via  0ae1b98dc5e90af5268666f22d1253b97d04c881 (commit)
       via  662f6893c83f9ea65116f2fd3dcc4dbb70a4eeff (commit)
       via  9299787baa49b7d824d82bd584b628a6fc253c0b (commit)
       via  27fefc1a018b9a06a144e441bed7095c2ca4b7d1 (commit)
       via  75d7f3a267e2a92751b63e11e4119405815479fc (commit)
       via  812b23a8296d258bb7ee910011c153c6a256d489 (commit)
       via  6ba647646e3143183ebe79966dbc27cec5100dc2 (commit)
       via  68860d4da9b213a353d923db357179496ec3a554 (commit)
      from  099507a4da17ef2eebe11efccca2e365bd81f99d (commit)

Summary of changes:
 bin/rt.in                                          | 107 +++++++++++++++------
 devel/tools/license_tag                            |  41 ++++----
 docs/UPGRADING-4.0                                 |  19 ++++
 lib/RT/Action/SendEmail.pm                         |   5 +-
 lib/RT/Attribute.pm                                |   2 +-
 lib/RT/Attributes.pm                               |   5 +-
 lib/RT/EmailParser.pm                              |   2 +-
 lib/RT/Interface/REST.pm                           |  41 +++++++-
 lib/RT/Interface/Web.pm                            |   2 +-
 lib/RT/Record.pm                                   |   6 +-
 lib/RT/Reminders.pm                                |   1 +
 lib/RT/Report/Tickets.pm                           |   4 +-
 lib/RT/Search/Simple.pm                            |   2 +
 lib/RT/Tickets.pm                                  |   4 +-
 sbin/rt-setup-database.in                          |  15 ++-
 share/html/Elements/CollectionList                 |   3 +-
 share/html/Elements/ColumnMap                      |   2 +-
 share/html/Elements/ShowSearch                     |   5 +-
 share/html/Elements/ShowTransactionAttachments     |  27 +++++-
 share/html/Helpers/Autocomplete/autohandler        |   2 +-
 share/html/Helpers/autohandler                     |   2 +-
 .../NoAuth}/css/base/farbtastic.css                |   6 +-
 share/html/REST/1.0/Forms/group/default            |   7 +-
 share/html/REST/1.0/Forms/queue/default            |   7 +-
 share/html/REST/1.0/Forms/ticket/comment           |  36 +------
 share/html/REST/1.0/Forms/ticket/default           |  36 +++++--
 share/html/REST/1.0/Forms/user/default             |   7 +-
 share/html/REST/1.0/ticket/comment                 |  35 +------
 share/html/Search/Build.html                       |   2 +-
 share/html/Search/Simple.html                      |   2 +-
 t/99-policy.t                                      |   4 +-
 t/api/action-createtickets.t                       |   2 +-
 t/customfields/enter_one.t                         |  23 +++++
 t/fts/indexed_mysql.t                              |   2 +-
 t/mail/disposition-outgoing.t                      |   2 +-
 t/web/articles-links.t                             |   2 +-
 t/web/command_line.t                               |  52 +++++-----
 t/web/command_line_cf_edge_cases.t                 |  10 +-
 t/web/reminder-permissions.t                       |   4 +-
 t/web/reminders.t                                  |   8 +-
 t/web/rest_cfs_with_same_name.t                    |   4 +-
 t/web/search_linkdisplay.t                         |  63 ++++++++++++
 42 files changed, 404 insertions(+), 207 deletions(-)
 copy share/{static => html/NoAuth}/css/base/farbtastic.css (81%)
 create mode 100644 t/customfields/enter_one.t
 create mode 100644 t/web/search_linkdisplay.t

- Log -----------------------------------------------------------------
commit db6c1f52736790972945fbcb19c948ff6cbeb598
Merge: 099507a d59a3cd
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Thu Jul 4 15:55:36 2013 -0400

    Merge branch '4.0-trunk'
    
    Both 4.0-trunk and master added additional status messages surrounding
    changes to reminders.  The 4.0 changes were dropped in preference to
    their equivalents on master.
    
    t/web/reminder-permissions.t was updated for a903402
    
    t/web/search_linkdisplay.t was updated for master's imrpoved link
    display, which shows "#2: subject" instead of "2" in search results.
    
    Conflicts:
    	lib/RT/Attribute.pm
    	lib/RT/Config.pm
    	lib/RT/Interface/Web.pm
    	lib/RT/Report/Tickets.pm
    	sbin/rt-setup-database.in
    	share/html/Elements/RT__Ticket/ColumnMap
    	share/html/Elements/ShowTransactionAttachments
    	share/html/NoAuth/css/base/farbtastic.css
    	share/html/REST/1.0/Forms/ticket/comment
    	share/html/Search/Build.html
    	share/html/Search/Chart.html
    	t/99-policy.t

diff --cc lib/RT/Action/SendEmail.pm
index 65f99fd,f1c54dd..d373411
--- a/lib/RT/Action/SendEmail.pm
+++ b/lib/RT/Action/SendEmail.pm
@@@ -404,9 -405,8 +405,9 @@@ sub AddAttachment 
          Type        => $attach->ContentType,
          Charset     => $attach->OriginalEncoding,
          Data        => $attach->OriginalContent,
-         Disposition => $disp, # a false value defaults to inline in MIME::Entity
+         Disposition => $disp,
          Filename    => $self->MIMEEncodeString( $attach->Filename ),
 +        Id          => $attach->GetHeader('Content-ID'),
          'RT-Attachment:' => $self->TicketObj->Id . "/"
              . $self->TransactionObj->Id . "/"
              . $attach->id,
diff --cc lib/RT/Attribute.pm
index 89f28ac,4350df0..c45d48f
--- a/lib/RT/Attribute.pm
+++ b/lib/RT/Attribute.pm
@@@ -145,11 -145,11 +145,11 @@@ sub Create 
                  Content => '',
                  ContentType => '',
                  Object => undef,
 -		  @_);
 +                @_);
  
      if ($args{Object} and UNIVERSAL::can($args{Object}, 'Id')) {
-         $args{ObjectType} = ref($args{Object});
 -	    $args{ObjectType} = $args{Object}->isa("RT::CurrentUser") ? "RT::User" : ref($args{Object});
 -	    $args{ObjectId} = $args{Object}->Id;
++        $args{ObjectType} = $args{Object}->isa("RT::CurrentUser") ? "RT::User" : ref($args{Object});
 +        $args{ObjectId} = $args{Object}->Id;
      } else {
          return(0, $self->loc("Required parameter '[_1]' not specified", 'Object'));
  
diff --cc lib/RT/Reminders.pm
index 05937dc,be716a0..dc18e03
--- a/lib/RT/Reminders.pm
+++ b/lib/RT/Reminders.pm
@@@ -145,6 -134,7 +145,7 @@@ sub Add 
          RefersTo => $self->Ticket,
          Type => 'reminder',
          Queue => $self->TicketObj->Queue,
 -        Status => $self->TicketObj->QueueObj->Lifecycle->ReminderStatusOnOpen,
++        Status => $self->TicketObj->QueueObj->LifecycleObj->ReminderStatusOnOpen,
      );
      $self->TicketObj->_NewTransaction(
          Type => 'AddReminder',
diff --cc lib/RT/Report/Tickets.pm
index 3042ff7,d811e03..dee21c0
--- a/lib/RT/Report/Tickets.pm
+++ b/lib/RT/Report/Tickets.pm
@@@ -54,308 -54,6 +54,308 @@@ use RT::Report::Tickets::Entry
  use strict;
  use warnings;
  
 +use Scalar::Util qw(weaken);
 +
 +our @GROUPINGS = (
 +    Status => 'Enum',                   #loc_left_pair
 +
 +    Queue  => 'Queue',                  #loc_left_pair
 +
 +    Owner         => 'User',            #loc_left_pair
 +    Creator       => 'User',            #loc_left_pair
 +    LastUpdatedBy => 'User',            #loc_left_pair
 +
 +    Requestor     => 'Watcher',         #loc_left_pair
 +    Cc            => 'Watcher',         #loc_left_pair
 +    AdminCc       => 'Watcher',         #loc_left_pair
 +    Watcher       => 'Watcher',         #loc_left_pair
 +
 +    Created       => 'Date',            #loc_left_pair
 +    Starts        => 'Date',            #loc_left_pair
 +    Started       => 'Date',            #loc_left_pair
 +    Resolved      => 'Date',            #loc_left_pair
 +    Due           => 'Date',            #loc_left_pair
 +    Told          => 'Date',            #loc_left_pair
 +    LastUpdated   => 'Date',            #loc_left_pair
 +
 +    CF            => 'CustomField',     #loc_left_pair
 +);
 +our %GROUPINGS;
 +
 +our %GROUPINGS_META = (
 +    Queue => {
 +        Display => sub {
 +            my $self = shift;
 +            my %args = (@_);
 +
 +            my $queue = RT::Queue->new( $self->CurrentUser );
 +            $queue->Load( $args{'VALUE'} );
 +            return $queue->Name;
 +        },
 +        Localize => 1,
 +    },
 +    User => {
 +        SubFields => [grep RT::User->_Accessible($_, "public"), qw(
 +            Name RealName NickName
 +            EmailAddress
 +            Organization
 +            Lang City Country Timezone
 +        )],
 +        Function => 'GenerateUserFunction',
 +    },
 +    Watcher => {
 +        SubFields => [grep RT::User->_Accessible($_, "public"), qw(
 +            Name RealName NickName
 +            EmailAddress
 +            Organization
 +            Lang City Country Timezone
 +        )],
 +        Function => 'GenerateWatcherFunction',
 +    },
 +    Date => {
 +        SubFields => [qw(
 +            Time
 +            Hourly Hour
 +            Date Daily
 +            DayOfWeek Day DayOfMonth DayOfYear
 +            Month Monthly
 +            Year Annually
 +            WeekOfYear
 +        )],
 +        Function => 'GenerateDateFunction',
 +        Display => sub {
 +            my $self = shift;
 +            my %args = (@_);
 +
 +            my $raw = $args{'VALUE'};
 +            return $raw unless defined $raw;
 +
 +            if ( $args{'SUBKEY'} eq 'DayOfWeek' ) {
 +                return $self->loc($RT::Date::DAYS_OF_WEEK[ int $raw ]);
 +            }
 +            elsif ( $args{'SUBKEY'} eq 'Month' ) {
 +                return $self->loc($RT::Date::MONTHS[ int($raw) - 1 ]);
 +            }
 +            return $raw;
 +        },
 +        Sort => 'raw',
 +    },
 +    CustomField => {
 +        SubFields => sub {
 +            my $self = shift;
 +            my $args = shift;
 +
 +
 +            my $queues = $args->{'Queues'};
 +            if ( !$queues && $args->{'Query'} ) {
 +                require RT::Interface::Web::QueryBuilder::Tree;
 +                my $tree = RT::Interface::Web::QueryBuilder::Tree->new('AND');
 +                $tree->ParseSQL( Query => $args->{'Query'}, CurrentUser => $self->CurrentUser );
 +                $queues = $args->{'Queues'} = $tree->GetReferencedQueues;
 +            }
 +            return () unless $queues;
 +
 +            my @res;
 +
 +            my $CustomFields = RT::CustomFields->new( $self->CurrentUser );
 +            foreach my $id (keys %$queues) {
 +                my $queue = RT::Queue->new( $self->CurrentUser );
 +                $queue->Load($id);
 +                next unless $queue->id;
 +
 +                $CustomFields->LimitToQueue($queue->id);
 +            }
 +            $CustomFields->LimitToGlobal;
 +            while ( my $CustomField = $CustomFields->Next ) {
 +                push @res, ["Custom field", $CustomField->Name], "CF.{". $CustomField->id ."}";
 +            }
 +            return @res;
 +        },
 +        Function => 'GenerateCustomFieldFunction',
 +        Label => sub {
 +            my $self = shift;
 +            my %args = (@_);
 +
-             my ($cf) = ( $args{'SUBKEY'} =~ /^{(.*)}$/ );
++            my ($cf) = ( $args{'SUBKEY'} =~ /^\{(.*)\}$/ );
 +            if ( $cf =~ /^\d+$/ ) {
 +                my $obj = RT::CustomField->new( $self->CurrentUser );
 +                $obj->Load( $cf );
 +                $cf = $obj->Name;
 +            }
 +
 +            return 'Custom field [_1]', $self->CurrentUser->loc( $cf );
 +        },
 +    },
 +    Enum => {
 +        Localize => 1,
 +    },
 +);
 +
 +# loc'able strings below generated with:
 +#   perl -MRT=-init -MRT::Report::Tickets -E 'say qq{\# loc("$_->[0]")} while $_ = splice @RT::Report::Tickets::STATISTICS, 0, 2'
 +#
 +# loc("Ticket count")
 +# loc("Summary of time worked")
 +# loc("Total time worked")
 +# loc("Average time worked")
 +# loc("Minimum time worked")
 +# loc("Maximum time worked")
 +# loc("Summary of time estimated")
 +# loc("Total time estimated")
 +# loc("Average time estimated")
 +# loc("Minimum time estimated")
 +# loc("Maximum time estimated")
 +# loc("Summary of time left")
 +# loc("Total time left")
 +# loc("Average time left")
 +# loc("Minimum time left")
 +# loc("Maximum time left")
 +# loc("Summary of Created-Started")
 +# loc("Total Created-Started")
 +# loc("Average Created-Started")
 +# loc("Minimum Created-Started")
 +# loc("Maximum Created-Started")
 +# loc("Summary of Created-Resolved")
 +# loc("Total Created-Resolved")
 +# loc("Average Created-Resolved")
 +# loc("Minimum Created-Resolved")
 +# loc("Maximum Created-Resolved")
 +# loc("Summary of Created-LastUpdated")
 +# loc("Total Created-LastUpdated")
 +# loc("Average Created-LastUpdated")
 +# loc("Minimum Created-LastUpdated")
 +# loc("Maximum Created-LastUpdated")
 +# loc("Summary of Starts-Started")
 +# loc("Total Starts-Started")
 +# loc("Average Starts-Started")
 +# loc("Minimum Starts-Started")
 +# loc("Maximum Starts-Started")
 +# loc("Summary of Due-Resolved")
 +# loc("Total Due-Resolved")
 +# loc("Average Due-Resolved")
 +# loc("Minimum Due-Resolved")
 +# loc("Maximum Due-Resolved")
 +# loc("Summary of Started-Resolved")
 +# loc("Total Started-Resolved")
 +# loc("Average Started-Resolved")
 +# loc("Minimum Started-Resolved")
 +# loc("Maximum Started-Resolved")
 +
 +our @STATISTICS = (
 +    COUNT => ['Ticket count', 'Count', 'id'],
 +);
 +
 +foreach my $field (qw(TimeWorked TimeEstimated TimeLeft)) {
 +    my $friendly = lc join ' ', split /(?<=[a-z])(?=[A-Z])/, $field;
 +    push @STATISTICS, (
 +        "ALL($field)" => ["Summary of $friendly",   'TimeAll',     $field ],
 +        "SUM($field)" => ["Total $friendly",   'Time', 'SUM', $field ],
 +        "AVG($field)" => ["Average $friendly", 'Time', 'AVG', $field ],
 +        "MIN($field)" => ["Minimum $friendly", 'Time', 'MIN', $field ],
 +        "MAX($field)" => ["Maximum $friendly", 'Time', 'MAX', $field ],
 +    );
 +}
 +
 +
 +foreach my $pair (qw(
 +    Created-Started
 +    Created-Resolved
 +    Created-LastUpdated
 +    Starts-Started
 +    Due-Resolved
 +    Started-Resolved
 +)) {
 +    my ($from, $to) = split /-/, $pair;
 +    push @STATISTICS, (
 +        "ALL($pair)" => ["Summary of $pair", 'DateTimeIntervalAll', $from, $to ],
 +        "SUM($pair)" => ["Total $pair", 'DateTimeInterval', 'SUM', $from, $to ],
 +        "AVG($pair)" => ["Average $pair", 'DateTimeInterval', 'AVG', $from, $to ],
 +        "MIN($pair)" => ["Minimum $pair", 'DateTimeInterval', 'MIN', $from, $to ],
 +        "MAX($pair)" => ["Maximum $pair", 'DateTimeInterval', 'MAX', $from, $to ],
 +    );
 +}
 +
 +our %STATISTICS;
 +
 +our %STATISTICS_META = (
 +    Count => {
 +        Function => sub {
 +            my $self = shift;
 +            my $field = shift || 'id';
 +
 +            # UseSQLForACLChecks may add late joins
 +            my $joined = ($self->_isJoined || RT->Config->Get('UseSQLForACLChecks')) ? 1 : 0;
 +            return (
 +                FUNCTION => ($joined ? 'DISTINCT COUNT' : 'COUNT'),
 +                FIELD    => 'id'
 +            );
 +        },
 +    },
 +    Simple => {
 +        Function => sub {
 +            my $self = shift;
 +            my ($function, $field) = @_;
 +            return (FUNCTION => $function, FIELD => $field);
 +        },
 +    },
 +    Time => {
 +        Function => sub {
 +            my $self = shift;
 +            my ($function, $field) = @_;
 +            return (FUNCTION => "$function(?)*60", FIELD => $field);
 +        },
 +        Display => 'DurationAsString',
 +    },
 +    TimeAll => {
 +        SubValues => sub { return ('Minimum', 'Average', 'Maximum', 'Total') },
 +        Function => sub {
 +            my $self = shift;
 +            my $field = shift;
 +            return (
 +                Minimum => { FUNCTION => "MIN(?)*60", FIELD => $field },
 +                Average => { FUNCTION => "AVG(?)*60", FIELD => $field },
 +                Maximum => { FUNCTION => "MAX(?)*60", FIELD => $field },
 +                Total   => { FUNCTION => "SUM(?)*60", FIELD => $field },
 +            );
 +        },
 +        Display => 'DurationAsString',
 +    },
 +    DateTimeInterval => {
 +        Function => sub {
 +            my $self = shift;
 +            my ($function, $from, $to) = @_;
 +
 +            my $interval = $self->_Handle->DateTimeIntervalFunction(
 +                From => { FUNCTION => $self->NotSetDateToNullFunction( FIELD => $from ) },
 +                To   => { FUNCTION => $self->NotSetDateToNullFunction( FIELD => $to ) },
 +            );
 +
 +            return (FUNCTION => "$function($interval)");
 +        },
 +        Display => 'DurationAsString',
 +    },
 +    DateTimeIntervalAll => {
 +        SubValues => sub { return ('Minimum', 'Average', 'Maximum', 'Total') },
 +        Function => sub {
 +            my $self = shift;
 +            my ($from, $to) = @_;
 +
 +            my $interval = $self->_Handle->DateTimeIntervalFunction(
 +                From => { FUNCTION => $self->NotSetDateToNullFunction( FIELD => $from ) },
 +                To   => { FUNCTION => $self->NotSetDateToNullFunction( FIELD => $to ) },
 +            );
 +
 +            return (
 +                Minimum => { FUNCTION => "MIN($interval)" },
 +                Average => { FUNCTION => "AVG($interval)" },
 +                Maximum => { FUNCTION => "MAX($interval)" },
 +                Total   => { FUNCTION => "SUM($interval)" },
 +            );
 +        },
 +        Display => 'DurationAsString',
 +    },
 +);
 +
  sub Groupings {
      my $self = shift;
      my %args = (@_);
@@@ -750,88 -275,31 +750,88 @@@ sub GenerateDateFunction 
      return %args;
  }
  
 +sub GenerateCustomFieldFunction {
 +    my $self = shift;
 +    my %args = @_;
  
-     my ($name) = ( $args{'SUBKEY'} =~ /^\.{(.*)}$/ );
 -# Override the AddRecord from DBI::SearchBuilder::Unique. id isn't id here
 -# wedon't want to disambiguate all the items with a count of 1.
 -sub AddRecord {
++    my ($name) = ( $args{'SUBKEY'} =~ /^\.\{(.*)\}$/ );
 +    my $cf = RT::CustomField->new( $self->CurrentUser );
 +    $cf->Load($name);
 +    unless ( $cf->id ) {
 +        $RT::Logger->error("Couldn't load CustomField #$name");
 +        @args{qw(FUNCTION FIELD)} = ('NULL', undef);
 +    } else {
 +        my ($ticket_cf_alias, $cf_alias) = $self->_CustomFieldJoin($cf->id, $cf);
 +        @args{qw(ALIAS FIELD)} = ($ticket_cf_alias, 'Content');
 +    }
 +    return %args;
 +}
 +
 +sub GenerateUserFunction {
      my $self = shift;
 -    my $record = shift;
 -    push @{$self->{'items'}}, $record;
 -    $self->{'rows'}++;
 +    my %args = @_;
 +
 +    my $column = $args{'SUBKEY'} || 'Name';
 +    my $u_alias = $self->{"_sql_report_$args{FIELD}_users_$column"}
 +        ||= $self->Join(
 +            TYPE   => 'LEFT',
 +            ALIAS1 => 'main',
 +            FIELD1 => $args{'FIELD'},
 +            TABLE2 => 'Users',
 +            FIELD2 => 'id',
 +        );
 +    @args{qw(ALIAS FIELD)} = ($u_alias, $column);
 +    return %args;
  }
  
 -1;
 +sub GenerateWatcherFunction {
 +    my $self = shift;
 +    my %args = @_;
  
 +    my $type = $args{'FIELD'};
 +    $type = '' if $type eq 'Watcher';
  
 +    my $column = $args{'SUBKEY'} || 'Name';
  
 -# Gotta skip over RT::Tickets->Next, since it does all sorts of crazy magic we 
 -# don't want.
 -sub Next {
 +    my $u_alias = $self->{"_sql_report_watcher_users_alias_$type"};
 +    unless ( $u_alias ) {
 +        my ($g_alias, $gm_alias);
 +        ($g_alias, $gm_alias, $u_alias) = $self->_WatcherJoin( Name => $type );
 +        $self->{"_sql_report_watcher_users_alias_$type"} = $u_alias;
 +    }
 +    @args{qw(ALIAS FIELD)} = ($u_alias, $column);
 +
 +    return %args;
 +}
 +
 +sub DurationAsString {
      my $self = shift;
 -    $self->RT::SearchBuilder::Next(@_);
 +    my %args = @_;
 +    my $v = $args{'VALUE'};
 +    unless ( ref $v ) {
 +        return $self->loc("(no value)") unless defined $v && length $v;
 +        return RT::Date->new( $self->CurrentUser )->DurationAsString(
 +            $v, Show => 3, Short => 1
 +        );
 +    }
  
 +    my $date = RT::Date->new( $self->CurrentUser );
 +    my %res = %$v;
 +    foreach my $e ( values %res ) {
 +        $e = $date->DurationAsString( $e, Short => 1, Show => 3 )
 +            if defined $e && length $e;
 +        $e = $self->loc("(no value)") unless defined $e && length $e;
 +    }
 +    return \%res;
  }
  
 -sub NewItem {
 +sub LabelValueCode {
      my $self = shift;
 -    return RT::Report::Tickets::Entry->new(RT->SystemUser); # $self->CurrentUser);
 +    my $name = shift;
 +
 +    my $display = $self->ColumnInfo( $name )->{'META'}{'Display'};
 +    return undef unless $display;
 +    return $self->FindImplementationCode( $display );
  }
  
  
diff --cc lib/RT/Search/Simple.pm
index 83dc6c6,0000000..f368e5a
mode 100644,000000..100644
--- a/lib/RT/Search/Simple.pm
+++ b/lib/RT/Search/Simple.pm
@@@ -1,269 -1,0 +1,271 @@@
 +# BEGIN BPS TAGGED BLOCK {{{
 +#
 +# COPYRIGHT:
 +#
 +# This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
 +#                                          <sales at bestpractical.com>
 +#
 +# (Except where explicitly superseded by other copyright notices)
 +#
 +#
 +# LICENSE:
 +#
 +# This work is made available to you under the terms of Version 2 of
 +# the GNU General Public License. A copy of that license should have
 +# been provided with this software, but in any event can be snarfed
 +# from www.gnu.org.
 +#
 +# This work is distributed in the hope that it will be useful, but
 +# WITHOUT ANY WARRANTY; without even the implied warranty of
 +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 +# General Public License for more details.
 +#
 +# You should have received a copy of the GNU General Public License
 +# along with this program; if not, write to the Free Software
 +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 +# 02110-1301 or visit their web page on the internet at
 +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
 +#
 +#
 +# CONTRIBUTION SUBMISSION POLICY:
 +#
 +# (The following paragraph is not intended to limit the rights granted
 +# to you to modify and distribute this software under the terms of
 +# the GNU General Public License and is only of importance to you if
 +# you choose to contribute your changes and enhancements to the
 +# community by submitting them to Best Practical Solutions, LLC.)
 +#
 +# By intentionally submitting any modifications, corrections or
 +# derivatives to this work, or any other work intended for use with
 +# Request Tracker, to Best Practical Solutions, LLC, you confirm that
 +# you are the copyright holder for those contributions and you grant
 +# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
 +# royalty-free, perpetual, license to use, copy, create derivative
 +# works based on those contributions, and sublicense and distribute
 +# those contributions and any derivatives thereof.
 +#
 +# END BPS TAGGED BLOCK }}}
 +
 +=head1 NAME
 +
 +  RT::Search::Simple
 +
 +=head1 SYNOPSIS
 +
 +=head1 DESCRIPTION
 +
 +Use the argument passed in as a simple set of keywords
 +
 +=head1 METHODS
 +
 +=cut
 +
 +package RT::Search::Simple;
 +
 +use strict;
 +use warnings;
 +use base qw(RT::Search);
 +
 +use Regexp::Common qw/delimited/;
 +
 +# Only a subset of limit types AND themselves together.  "queue:foo
 +# queue:bar" is an OR, but "subject:foo subject:bar" is an AND
 +our %AND = (
 +    content => 1,
 +    subject => 1,
 +);
 +
 +sub _Init {
 +    my $self = shift;
 +    my %args = @_;
 +
 +    $self->{'Queues'} = delete( $args{'Queues'} ) || [];
 +    $self->SUPER::_Init(%args);
 +}
 +
 +sub Describe {
 +    my $self = shift;
 +    return ( $self->loc( "Keyword and intuition-based searching", ref $self ) );
 +}
 +
 +sub Prepare {
 +    my $self = shift;
 +    my $tql  = $self->QueryToSQL( $self->Argument );
 +
 +    $RT::Logger->debug($tql);
 +
 +    $self->TicketsObj->FromSQL($tql);
 +    return (1);
 +}
 +
 +sub QueryToSQL {
 +    my $self = shift;
 +    my $query = shift || $self->Argument;
 +
 +    my %limits;
 +    $query =~ s/^\s*//;
 +    while ($query =~ /^\S/) {
 +        if ($query =~ s/^
 +                        (?:
 +                            (\w+)  # A straight word
 +                            (?:\.  # With an optional .foo
 +                                ($RE{delimited}{-delim=>q['"]}
 +                                |[\w-]+  # Allow \w + dashes
 +                                ) # Which could be ."foo bar", too
 +                            )?
 +                        )
 +                        :  # Followed by a colon
 +                        ($RE{delimited}{-delim=>q['"]}
 +                        |\S+
 +                        ) # And a possibly-quoted foo:"bar baz"
 +                        \s*//ix) {
 +            my ($type, $extra, $value) = ($1, $2, $3);
 +            ($value, my ($quoted)) = $self->Unquote($value);
 +            $extra = $self->Unquote($extra) if defined $extra;
 +            $self->Dispatch(\%limits, $type, $value, $quoted, $extra);
 +        } elsif ($query =~ s/^($RE{delimited}{-delim=>q['"]}|\S+)\s*//) {
 +            # If there's no colon, it's just a word or quoted string
 +            my($val, $quoted) = $self->Unquote($1);
 +            $self->Dispatch(\%limits, $self->GuessType($val, $quoted), $val, $quoted);
 +        }
 +    }
 +    $self->Finalize(\%limits);
 +
 +    my @clauses;
 +    for my $subclause (sort keys %limits) {
 +        next unless @{$limits{$subclause}};
 +
 +        my $op = $AND{lc $subclause} ? "AND" : "OR";
 +        push @clauses, "( ".join(" $op ", @{$limits{$subclause}})." )";
 +    }
 +
 +    return join " AND ", @clauses;
 +}
 +
 +sub Dispatch {
 +    my $self = shift;
 +    my ($limits, $type, $contents, $quoted, $extra) = @_;
 +    $contents =~ s/(['\\])/\\$1/g;
 +    $extra    =~ s/(['\\])/\\$1/g if defined $extra;
 +
 +    my $method = "Handle" . ucfirst(lc($type));
 +    $method = "HandleDefault" unless $self->can($method);
 +    my ($key, @tsql) = $self->$method($contents, $quoted, $extra);
 +    push @{$limits->{$key}}, @tsql;
 +}
 +
 +sub Unquote {
 +    # Given a word or quoted string, unquote it if it is quoted,
 +    # removing escaped quotes.
 +    my $self = shift;
 +    my ($token) = @_;
 +    if ($token =~ /^$RE{delimited}{-delim=>q['"]}{-keep}$/) {
 +        my $quote = $2 || $5;
 +        my $value = $3 || $6;
 +        $value =~ s/\\(\\|$quote)/$1/g;
 +        return wantarray ? ($value, 1) : $value;
 +    } else {
 +        return wantarray ? ($token, 0) : $token;
 +    }
 +}
 +
 +sub Finalize {
 +    my $self = shift;
 +    my ($limits) = @_;
 +
 +    # Apply default "active status" limit if we don't have any status
 +    # limits ourselves, and we're not limited by id
 +    if (not $limits->{status} and not $limits->{id}
 +        and RT::Config->Get('OnlySearchActiveTicketsInSimpleSearch', $self->TicketsObj->CurrentUser)) {
 +        $limits->{status} = [map {s/(['\\])/\\$1/g; "Status = '$_'"} RT::Queue->ActiveStatusArray()];
 +    }
 +
 +    # Respect the "only search these queues" limit if we didn't
 +    # specify any queues ourselves
 +    if (not $limits->{queue} and not $limits->{id}) {
 +        for my $queue ( @{ $self->{'Queues'} } ) {
 +            my $QueueObj = RT::Queue->new( $self->TicketsObj->CurrentUser );
 +            next unless $QueueObj->Load($queue);
 +            my $name = $QueueObj->Name;
 +            $name =~ s/(['\\])/\\$1/g;
 +            push @{$limits->{queue}}, "Queue = '$name'";
 +        }
 +    }
 +}
 +
 +our @GUESS = (
 +    [ 10 => sub { return "subject" if $_[1] } ],
 +    [ 20 => sub { return "id" if /^#?\d+$/ } ],
 +    [ 30 => sub { return "requestor" if /\w+@\w+/} ],
++    [ 35 => sub { return "domain" if /^@\w+/} ],
 +    [ 40 => sub {
 +          return "status" if RT::Queue->new( $_[2] )->IsValidStatus( $_ )
 +      }],
 +    [ 40 => sub { return "status" if /^((in)?active|any)$/i } ],
 +    [ 50 => sub {
 +          my $q = RT::Queue->new( $_[2] );
 +          return "queue" if $q->Load($_) and $q->Id and not $q->Disabled
 +      }],
 +    [ 60 => sub {
 +          my $u = RT::User->new( $_[2] );
 +          return "owner" if $u->Load($_) and $u->Id and $u->Privileged
 +      }],
 +    [ 70 => sub { return "owner" if $_ eq "me" } ],
 +);
 +
 +sub GuessType {
 +    my $self = shift;
 +    my ($val, $quoted) = @_;
 +
 +    my $cu = $self->TicketsObj->CurrentUser;
 +    for my $sub (map $_->[1], sort {$a->[0] <=> $b->[0]} @GUESS) {
 +        local $_ = $val;
 +        my $ret = $sub->($val, $quoted, $cu);
 +        return $ret if $ret;
 +    }
 +    return "default";
 +}
 +
 +# $_[0] is $self
 +# $_[1] is escaped value without surrounding single quotes
 +# $_[2] is a boolean of "was quoted by the user?"
 +#       ensure this is false before you do smart matching like $_[1] eq "me"
 +# $_[3] is escaped subkey, if any (see HandleCf)
 +sub HandleDefault   { return subject   => "Subject LIKE '$_[1]'"; }
 +sub HandleSubject   { return subject   => "Subject LIKE '$_[1]'"; }
 +sub HandleFulltext  { return content   => "Content LIKE '$_[1]'"; }
 +sub HandleContent   { return content   => "Content LIKE '$_[1]'"; }
 +sub HandleId        { $_[1] =~ s/^#//; return id => "Id = $_[1]"; }
 +sub HandleStatus    {
 +    if ($_[1] =~ /^active$/i and !$_[2]) {
 +        return status => map {s/(['\\])/\\$1/g; "Status = '$_'"} RT::Queue->ActiveStatusArray();
 +    } elsif ($_[1] =~ /^inactive$/i and !$_[2]) {
 +        return status => map {s/(['\\])/\\$1/g; "Status = '$_'"} RT::Queue->InactiveStatusArray();
 +    } elsif ($_[1] =~ /^any$/i and !$_[2]) {
 +        return 'status';
 +    } else {
 +        return status => "Status = '$_[1]'";
 +    }
 +}
 +sub HandleOwner     {
 +    if (!$_[2] and $_[1] eq "me") {
 +        return owner => "Owner.id = '__CurrentUser__'";
 +    }
 +    elsif (!$_[2] and $_[1] =~ /\w+@\w+/) {
 +        return owner => "Owner.EmailAddress = '$_[1]'";
 +    } else {
 +        return owner => "Owner = '$_[1]'";
 +    }
 +}
 +sub HandleWatcher     {
 +    return watcher => (!$_[2] and $_[1] eq "me") ? "Watcher.id = '__CurrentUser__'" : "Watcher = '$_[1]'";
 +}
 +sub HandleRequestor { return requestor => "Requestor STARTSWITH '$_[1]'";  }
++sub HandleDomain    { $_[1] =~ s/^@?/@/; return requestor => "Requestor ENDSWITH '$_[1]'";  }
 +sub HandleQueue     { return queue     => "Queue = '$_[1]'";      }
 +sub HandleQ         { return queue     => "Queue = '$_[1]'";      }
 +sub HandleCf        { return "cf.$_[3]" => "'CF.{$_[3]}' LIKE '$_[1]'"; }
 +
 +RT::Base->_ImportOverlays();
 +
 +1;
diff --cc sbin/rt-setup-database.in
index 4ef5b82,fd82fe2..05d0364
--- a/sbin/rt-setup-database.in
+++ b/sbin/rt-setup-database.in
@@@ -73,14 -81,16 +73,16 @@@ use Getopt::Long
  
  $| = 1; # unbuffer all output.
  
- my %args;
+ my %args = (
 -    dba => '@DB_DBA@',
+     package => 'RT',
+ );
  GetOptions(
      \%args,
      'action=s',
      'force', 'debug',
-     'dba=s', 'dba-password=s', 'prompt-for-dba-password',
+     'dba=s', 'dba-password=s', 'prompt-for-dba-password', 'package=s',
      'datafile=s', 'datadir=s', 'skip-create', 'root-password-file=s',
 +    'package=s', 'ext-version=s',
      'help|h',
  );
  
diff --cc share/html/Elements/ShowSearch
index 8ebdc28,7d7df64..75a621b
--- a/share/html/Elements/ShowSearch
+++ b/share/html/Elements/ShowSearch
@@@ -123,24 -122,17 +123,21 @@@ foreach ( $SearchArg, $ProcessedSearchA
      $_->{'Format'} ||= '';
      $_->{'Query'} ||= '';
  
 -    $_->{'Format'} =~ s/__(Web(?:Path|Base|BaseURL))__/scalar RT->Config->Get($1)/ge;
      # extract-message-catalog would "$1", so we avoid quotes for loc calls
      $_->{'Format'} =~ s/__loc\(["']?(\w+)["']?\)__/my $f = "$1"; loc($f)/ge;
-     if ( $_->{'Query'} =~ /__Bookmarked__/ ) {
-         $_->{'Rows'} = 999;
-     }
-     elsif ( $_->{'Query'} =~ /__Bookmarks__/ ) {
+     if ( $_->{'Query'} =~ /__Bookmarks__/ ) {
          $_->{'Rows'} = 999;
  
 -        # DEPRECATED: will be here for a while up to 3.10/4.0
 -        my $bookmarks = $session{'CurrentUser'}->UserObj->FirstAttribute('Bookmarks');
 -        $bookmarks = $bookmarks->Content if $bookmarks;
 -        $bookmarks ||= {};
 -        my $query = join(" OR ", map " id = '$_' ", grep $bookmarks->{ $_ }, keys %$bookmarks ) || 'id=0';
 +        # DEPRECATED: will be here for a while up to 4.4
 +        RT->Deprecated(
 +            Remove  => "4.4",
 +            Instead => "id = '__Bookmarked__'",
 +            Message => "The __Bookmarks__ query syntax is deprecated",
 +            Object  => $search,
 +        );
 +
 +        my @bookmarks = $session{'CurrentUser'}->UserObj->Bookmarks;
 +        my $query = join(" OR ", map " id = '$_' ", @bookmarks ) || 'id=0';
          $_->{'Query'} =~ s/__Bookmarks__/( $query )/g;
      }
  }
diff --cc share/html/Elements/ShowTransactionAttachments
index 6c98ba9,0000000..51abd8c
mode 100644,000000..100644
--- a/share/html/Elements/ShowTransactionAttachments
+++ b/share/html/Elements/ShowTransactionAttachments
@@@ -1,280 -1,0 +1,299 @@@
 +%# BEGIN BPS TAGGED BLOCK {{{
 +%#
 +%# COPYRIGHT:
 +%#
 +%# This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
 +%#                                          <sales at bestpractical.com>
 +%#
 +%# (Except where explicitly superseded by other copyright notices)
 +%#
 +%#
 +%# LICENSE:
 +%#
 +%# This work is made available to you under the terms of Version 2 of
 +%# the GNU General Public License. A copy of that license should have
 +%# been provided with this software, but in any event can be snarfed
 +%# from www.gnu.org.
 +%#
 +%# This work is distributed in the hope that it will be useful, but
 +%# WITHOUT ANY WARRANTY; without even the implied warranty of
 +%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 +%# General Public License for more details.
 +%#
 +%# You should have received a copy of the GNU General Public License
 +%# along with this program; if not, write to the Free Software
 +%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 +%# 02110-1301 or visit their web page on the internet at
 +%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
 +%#
 +%#
 +%# CONTRIBUTION SUBMISSION POLICY:
 +%#
 +%# (The following paragraph is not intended to limit the rights granted
 +%# to you to modify and distribute this software under the terms of
 +%# the GNU General Public License and is only of importance to you if
 +%# you choose to contribute your changes and enhancements to the
 +%# community by submitting them to Best Practical Solutions, LLC.)
 +%#
 +%# By intentionally submitting any modifications, corrections or
 +%# derivatives to this work, or any other work intended for use with
 +%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
 +%# you are the copyright holder for those contributions and you grant
 +%# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
 +%# royalty-free, perpetual, license to use, copy, create derivative
 +%# works based on those contributions, and sublicense and distribute
 +%# those contributions and any derivatives thereof.
 +%#
 +%# END BPS TAGGED BLOCK }}}
 +<%PERL>
 +# Find all the attachments which have parent $Parent
 +# For each of these attachments
 +foreach my $message ( @{ $Attachments->{ $Parent || 0 } || [] } ) {
 +
 +    if (RT->Config->Get('GnuPG')->{'Enable'}) {
 +        $m->comp( 'ShowGnuPGStatus', Attachment => $message, WarnUnsigned => $WarnUnsigned );
 +    }
 +
 +    $m->comp( 'ShowMessageHeaders',
 +              Message        => $message,
 +              DisplayHeaders => \@DisplayHeaders,
 +            );
 +
 +    my $name = defined $message->Filename && length $message->Filename ?  $message->Filename : '';
 +    if ( $message->ContentLength ) {
 +</%PERL>
 +<div class="downloadattachment">
 +<a href="<% $AttachmentPath %>/<% $Transaction->Id %>/<% $message->Id %>/<% $name | u%>"><&|/l&>Download</&> <% length $name ? $name : loc('(untitled)') %></a>\
 +% if ( $DownloadableHeaders && ! length $name && $message->ContentType =~ /text/  ) {
 + / <a href="<% $AttachmentPath %>/WithHeaders/<% $message->Id %>"><% loc('with headers') %></a>
 +% }
 +% $m->callback(CallbackName => 'AfterDownloadLinks', ARGSRef => \%ARGS, Object => $Object, Transaction => $Transaction, Attachment => $message);
 +<br />
 +<span class="downloadcontenttype"><% $message->ContentType %> <% $message->FriendlyContentLength %></span>
 +</div>
 +%   }
 +%# If there is sub-messages, open a dedicated div
 +% if ( $Attachments->{ $message->id } ) {
 +<div class="messageattachments">
 +% } else {
 +<div class="messagebody">
 +% }
 +<%PERL>
 +
 +$render_attachment->( $message );
 +
 +$m->comp(
 +    $m->current_comp,
 +    %ARGS,
 +    Parent    => $message->id,
 +    ParentObj => $message,
 +
 +    displayed_inline => $displayed_inline,
 +);
 +
 +</%PERL>
 +</div>
 +% }
 +<%ARGS>
 +$Transaction
 +$Object => $Transaction->Object
 +$ShowHeaders => 0
 +$DownloadableHeaders => 1
 +$AttachmentPath => undef
 +$Attachments => {}
 +$AttachmentContent => {}
 +$Parent => 0
 +$ParentObj => undef
 +$WarnUnsigned => 0
 +
 +# Keep track of CID images we display inline
 +$displayed_inline => {}
 +</%ARGS>
 +<%INIT>
 +my @DisplayHeaders=qw(_all);
 +if ( $Transaction->Type =~ /EmailRecord$/ ) {
 +    @DisplayHeaders = qw(To Cc Bcc);
 +}
 +
 +# If the transaction has anything attached to it at all
 +elsif (!$ShowHeaders)  {
 +    @DisplayHeaders = qw(To From RT-Send-Cc Cc Bcc Date Subject);
 +    push @DisplayHeaders, 'RT-Send-Bcc' if RT->Config->Get('ShowBccHeader');
 +}
 +
 +$m->callback(CallbackName => 'MassageDisplayHeaders', DisplayHeaders => \@DisplayHeaders, Transaction => $Transaction);
 +
 +my $render_attachment = sub {
 +    my $message = shift;
 +    my $name = defined $message->Filename && length $message->Filename ?  $message->Filename : '';
 +
 +    my $content_type = lc $message->ContentType;
 +
 +    # if it has a content-disposition: attachment, don't show inline
 +    my $disposition = $message->GetHeader('Content-Disposition');
 +
 +    if ( $disposition && $disposition =~ /^\s*attachment/i ) {
 +        $disposition = 'attachment';
 +    } else {
 +        $disposition = 'inline';
 +    }
 +
 +    # If it's text
 +    if ( $content_type =~ m{^(text|message)/} ) {
 +        my $max_size = RT->Config->Get( 'MaxInlineBody', $session{'CurrentUser'} );
 +        if ( $disposition ne 'inline' ) {
 +            $m->out('<p>'. loc( 'Message body is not shown because sender requested not to inline it.' ) .'</p>');
 +            return;
 +        }
 +        elsif ( length $name && RT->Config->Get('SuppressInlineTextFiles', $session{'CurrentUser'} ) ) {
 +            $m->out('<p>'. loc( 'Text file is not shown because it is disabled in preferences.' ) .'</p>');
 +            return;
 +        }
 +        elsif ( $max_size && $message->ContentLength > $max_size ) {
 +            $m->out('<p>'. loc( 'Message body is not shown because it is too large.' ) .'</p>');
 +            return;
 +        }
 +
 +        if (
 +
 +            # it's a toplevel object
 +            !$ParentObj
 +
 +            # or its parent isn't a multipart alternative
 +            || ( $ParentObj->ContentType !~ m{^multipart/(?:alternative|related)$}i )
 +
 +            # or it's of our prefered alterative type
 +            || (
 +                (
 +                    RT->Config->Get('PreferRichText', $session{CurrentUser})
 +                    && ( $content_type =~ m{^text/(?:html|enriched)$} )
 +                )
 +                || ( !RT->Config->Get('PreferRichText', $session{CurrentUser})
 +                    && ( $content_type !~ m{^text/(?:html|enriched)$} )
 +                )
 +            )
 +        ) {
 +
 +            my $content;
 +            # If we've cached the content, use it from there
 +            if (my $x = $AttachmentContent->{ $Transaction->id }->{$message->id}) {
 +                $content = $x->Content;
 +            }
 +            else {
 +                $content = $message->Content;
 +            }
 +
 +            $RT::Logger->debug(
 +                "Rendering attachment #". $message->id
 +                ." of '$content_type' type"
 +            );
 +
 +            # if it's a text/html clean the body and show it
 +            if ( $content_type eq 'text/html' ) {
 +                $content = $m->comp( '/Elements/ScrubHTML', Content => $content );
 +
 +                $m->comp(
 +                    '/Elements/MakeClicky',
 +                    content => \$content,
 +                    html    => 1,
 +                    object  => $Object,
 +                );
 +
 +                if (RT->Config->Get('ShowTransactionImages')) {
 +                    my @rewritten = RT::Interface::Web::RewriteInlineImages(
 +                        Content         => \$content,
 +                        Attachment      => $message,
 +                        # Not technically correct to search all parts of the
 +                        # MIME structure, but it saves having to go to the
 +                        # database again and is unlikely to break display.
 +                        Related         => [ map { @$_ } values %$Attachments ],
 +                        AttachmentPath  => $AttachmentPath,
 +                    );
 +                    $displayed_inline->{$_}++ for @rewritten;
 +                }
 +
-                 require HTML::Quoted;
-                 $content = HTML::Quoted->extract($content) unless length $name;
++                unless (length $name) {
++                    eval {
++                        require HTML::Quoted;
++                        $content = HTML::Quoted->extract($content)
++                    };
++                    if ($@) {
++                        RT->Logger->error(
++                            "HTML::Quoted couldn't process attachment #@{[$message->id]}: $@."
++                          . "  This is a bug, please report it to rt-bugs\@bestpractical.com.");
++                    }
++                }
 +
 +                $m->comp(
 +                    'ShowMessageStanza',
 +                    Message     => $content,
 +                    Transaction => $Transaction,
 +                    ContentType => 'text/html',
 +                );
 +            }
 +
 +            elsif ( $content_type eq 'text/enriched' ) {
 +                $content = $m->comp( '/Elements/ScrubHTML', Content => $content );
 +                $m->out( $content );
 +            }
 +
 +            # It's a text type we don't have special handling for
 +            else {
 +                unless ( length $name ) {
-                     eval { require Text::Quoted;  $content = Text::Quoted::extract($content); };
-                     if ($@) { $RT::Logger->warning( "Text::Quoted failed: $@" ) }
++                    eval {
++                        require Text::Quoted;
++                        # XXX: Deprecate ->can check in 4.2 and simply bump version requirement.
++                        Text::Quoted::set_quote_characters(undef) # only use >
++                            if Text::Quoted->can("set_quote_characters");
++                        $content = Text::Quoted::extract($content);
++                    };
++                    if ($@) {
++                        RT->Logger->error(
++                            "Text::Quoted couldn't process attachment #@{[$message->id]}: $@."
++                          . "  This is a bug, please report it to rt-bugs\@bestpractical.com.");
++                    }
 +                }
 +
 +                $m->comp(
 +                    'ShowMessageStanza',
 +                    Message     => $content,
 +                    Transaction => $Transaction,
 +                    ContentType => 'text/plain',
 +                );
 +            }
 +        }
 +    }
 +
 +    # if it's an image, show it as an image
 +    elsif ( $content_type =~ m{^image/} ) {
 +        if (not RT->Config->Get('ShowTransactionImages')) {
 +            $m->out('<p><i>'. loc( 'Image not shown because display is disabled in system configuration.' ) .'</i></p>');
 +            return;
 +        }
 +        elsif ( $displayed_inline->{$message->Id} ) {
 +            $m->out('<p><i>'. loc( 'Image displayed inline above' ) .'</i></p>');
 +            return;
 +        }
 +        elsif ( $disposition ne 'inline' ) {
 +            $m->out('<p>'. loc( 'Image not shown because sender requested not to inline it.' ) .'</p>');
 +            return;
 +        }
 +
 +        my $filename = length $name ? $name : loc('(untitled)');
 +        my $efilename = $m->interp->apply_escapes( $filename, 'h' );
 +        $m->out(
 +            qq{<img alt="$efilename" title="$efilename"}
 +            . ' src="'. $AttachmentPath .'/'. $Transaction->Id .'/'. $message->Id .'/'
 +                . $m->interp->apply_escapes( $filename, 'u', 'h' )
 +            . '" />'
 +        );
 +    }
 +    elsif ( $message->ContentLength && $message->ContentLength > 0 ) {
 +        $m->out( '<p>' .
 +            loc( 'Message body not shown because it is not plain text.' ) .
 +            '</p>'
 +        );
 +    }
 +};
 +
 +</%INIT>
diff --cc share/html/REST/1.0/Forms/ticket/default
index 05a38a2,f6b4d84..2abf8e8
--- a/share/html/REST/1.0/Forms/ticket/default
+++ b/share/html/REST/1.0/Forms/ticket/default
@@@ -183,15 -187,24 +187,25 @@@ else 
          # people fields allow multiple values
          $v{$_} = vsplit($v{$_}) foreach ( grep $create{lc $_}, @people );
  
-         if ($text) {
+         if ($text || @atts) {
              $v{MIMEObj} =
                  MIME::Entity->build(
+                     Type => "multipart/mixed",
                      From => $session{CurrentUser}->EmailAddress,
                      Subject => $v{Subject},
-                     Data => $text,
                      'X-RT-Interface' => 'REST',
 +                    'Content-Type' => $v{'Content-Type'} || 'text/plain',
                  );
+             $v{MIMEObj}->attach(
+                 Data => $text,
+                 'Content-Type' => $v{'Content-Type'} || 'text/plain',
+             ) if $text;
+             my ($status, $msg) = process_attachments($v{'MIMEObj'}, @atts);
+             unless ($status) {
+                 push(@comments, "# $msg");
+                 goto DONE;
+             }
+             $v{MIMEObj}->make_singlepart;
          }
  
          my($tid,$trid,$terr) = $ticket->Create(%v);    
diff --cc share/html/Search/Build.html
index 2f86a45,6ac285b..d507bac
--- a/share/html/Search/Build.html
+++ b/share/html/Search/Build.html
@@@ -190,7 -190,7 +190,7 @@@ my @new_values = ()
  
  # Try to find if we're adding a clause
  foreach my $arg ( keys %ARGS ) {
-     next unless $arg =~ m/^ValueOf(\w+|CF.{.*?})$/
 -    next unless $arg =~ m/^ValueOf(\w+|'CF\.\{.*?\}')$/
++    next unless $arg =~ m/^ValueOf(\w+|CF.\{.*?\})$/
                  && ( ref $ARGS{$arg} eq "ARRAY"
                       ? grep $_ ne '', @{ $ARGS{$arg} }
                       : $ARGS{$arg} ne '' );
diff --cc t/99-policy.t
index a8e512c,a7b0675..d538532
--- a/t/99-policy.t
+++ b/t/99-policy.t
@@@ -53,24 -50,17 +53,24 @@@ sub check 
              unlike( $content, qr/^#!/, "$file has no shebang" );
          }
  
 -        $check{bps_tag} = -1 if $check{bps_tag} == 1
 +        my $other_copyright = 0;
 +        $other_copyright = 1 if $file =~ /\.(css|js)$/
              and not $content =~ /Copyright\s+\(c\)\s+\d\d\d\d-\d\d\d\d Best Practical Solutions/i
                  and $file =~ /(?:ckeditor|scriptaculous|superfish|tablesorter|farbtastic)/i;
 -        $check{bps_tag} = -1 if $check{bps_tag} == 1
 +        $other_copyright = 1 if $file =~ /\.(css|js)$/
              and not $content =~ /Copyright\s+\(c\)\s+\d\d\d\d-\d\d\d\d Best Practical Solutions/i
                  and ($content =~ /\b(copyright|GPL|Public Domain)\b/i
 -                  or /\(c\)\s+\d\d\d\d(?:-\d\d\d\d)?/i);
 +                  or $content =~ /\(c\)\s+\d\d\d\d(?:-\d\d\d\d)?/i);
 +        $check{bps_tag} = -1 if $check{bps_tag} and $other_copyright;
          if ($check{bps_tag} == 1) {
-             like( $content, qr/[B]EGIN BPS TAGGED BLOCK {{{/, "$file has BPS license tag");
+             like( $content, qr/[B]EGIN BPS TAGGED BLOCK \{\{\{/, "$file has BPS license tag");
          } elsif ($check{bps_tag} == -1) {
-             unlike( $content, qr/[B]EGIN BPS TAGGED BLOCK {{{/, "$file has no BPS license tag"
 -            unlike( $content, qr/[B]EGIN BPS TAGGED BLOCK \{\{\{/, "$file has no BPS license tag");
++            unlike( $content, qr/[B]EGIN BPS TAGGED BLOCK \{\{\{/, "$file has no BPS license tag"
 +                        . ($other_copyright ? " (other copyright)" : ""));
 +        }
 +
 +        if (not $other_copyright and $check{no_tabs}) {
 +            unlike( $content, qr/\t/, "$file has no hard tabs" );
          }
      }
  
diff --cc t/web/reminder-permissions.t
index 3ba2ca0,0000000..dd859cd
mode 100644,000000..100644
--- a/t/web/reminder-permissions.t
+++ b/t/web/reminder-permissions.t
@@@ -1,178 -1,0 +1,178 @@@
 +use strict;
 +use warnings;
 +use RT::Test tests => 40;
 +
 +my $user_a = RT::Test->load_or_create_user(
 +    Name     => 'user_a',
 +    Password => 'password',
 +);
 +
 +ok( $user_a && $user_a->id, 'created user_a' );
 +ok(
 +    RT::Test->add_rights(
 +        {
 +            Principal => $user_a,
 +            Right     => [qw/SeeQueue CreateTicket ShowTicket OwnTicket/]
 +        },
 +    ),
 +    'add basic rights for user_a'
 +);
 +
 +ok(
 +    RT::Test->add_rights(
 +        {
 +            Principal => 'Owner',
 +            Right     => [qw/ModifyTicket/],
 +        },
 +    ),
 +    'add basic rights for owner'
 +);
 +
 +my $ticket = RT::Test->create_ticket(
 +    Subject => 'test reminder permission',
 +    Queue   => 'General',
 +);
 +
 +ok( $ticket->id, 'created a ticket' );
 +
 +my ( $baseurl, $m ) = RT::Test->started_ok;
 +$m->login;
 +
 +my ( $root_reminder_id, $user_a_reminder_id );
 +diag "create two reminders, with owner root and user_a, respectively";
 +{
 +    $m->goto_ticket( $ticket->id );
 +    $m->text_contains( 'New reminder:', 'can create a new reminder' );
 +    $m->form_name('UpdateReminders');
 +    $m->field( 'NewReminder-Subject' => "root reminder" );
 +    $m->submit;
 +    $m->text_contains( "Reminder 'root reminder': Created",
 +        'created root reminder' );
 +
 +    $m->form_name('UpdateReminders');
 +    $m->field( 'NewReminder-Subject' => "user_a reminder", );
 +    $m->field( 'NewReminder-Owner'   => $user_a->id, );
 +    $m->submit;
 +    $m->text_contains( "Reminder 'user_a reminder': Created",
 +        'created user_a reminder' );
 +
 +    my $reminders = RT::Reminders->new($user_a);
 +    $reminders->Ticket( $ticket->id );
 +    my $col = $reminders->Collection;
 +    while ( my $c = $col->Next ) {
 +        if ( $c->Subject eq 'root reminder' ) {
 +            $root_reminder_id = $c->id;
 +        }
 +        elsif ( $c->Subject eq 'user_a reminder' ) {
 +            $user_a_reminder_id = $c->id;
 +        }
 +    }
 +}
 +
 +diag "check root_a can update user_a reminder but not root reminder";
 +my $m_a = RT::Test::Web->new;
 +{
 +    ok( $m_a->login( user_a => 'password' ), 'logged in as user_a' );
 +    $m_a->goto_ticket( $ticket->id );
 +    $m_a->content_lacks( 'New reminder:', 'can not create a new reminder' );
 +    $m_a->content_contains( 'root reminder',   'can see root reminder' );
 +    $m_a->content_contains( 'user_a reminder', 'can see user_a reminder' );
 +    $m_a->content_like(
 +qr!<input[^/]+name="Complete-Reminder-$root_reminder_id"[^/]+disabled="disabled"!,
 +        "root reminder checkbox is disabled"
 +    );
 +
 +    $m_a->form_name('UpdateReminders');
 +    $m_a->tick( "Complete-Reminder-$user_a_reminder_id" => 1 );
 +    $m_a->submit;
 +    $m_a->text_contains(
-         "Reminder 'user_a reminder': Status changed from 'new' to 'resolved'",
++        "Reminder 'user_a reminder': Status changed from 'open' to 'resolved'",
 +        'complete user_a reminder' );
 +
 +    $m_a->follow_link_ok( { id => 'page-reminders' } );
 +    $m_a->title_is( "Reminders for ticket #" . $ticket->id );
 +    $m_a->content_contains( 'root reminder',   'can see root reminder' );
 +    $m_a->content_contains( 'user_a reminder', 'can see user_a reminder' );
 +    $m_a->content_lacks( 'New reminder:', 'can not create a new reminder' );
 +    $m_a->content_like(
 +qr!<input[^/]+name="Complete-Reminder-$root_reminder_id"[^/]+disabled="disabled"!,
 +        "root reminder checkbox is disabled"
 +    );
 +
 +    $m_a->form_name('UpdateReminders');
 +    $m_a->untick( "Complete-Reminder-$user_a_reminder_id", 1 );
 +    $m_a->submit;
 +    $m_a->text_contains(
 +        "Reminder 'user_a reminder': Status changed from 'resolved' to 'open'",
 +        'reopen user_a reminder'
 +    );
 +
 +}
 +
 +diag "set ticket owner to user_a to let user_a grant modify ticket right";
 +{
 +    $ticket->SetOwner( $user_a->id );
 +
 +    $m_a->goto_ticket( $ticket->id );
 +    $m_a->content_contains( 'New reminder:', 'can create a new reminder' );
 +    $m_a->content_like(
 +qr!<input[^/]+name="Complete-Reminder-$root_reminder_id"[^/]+disabled="disabled"!,
 +        "root reminder checkbox is still disabled"
 +    );
 +    $m_a->form_name('UpdateReminders');
 +    $m_a->field( 'NewReminder-Subject' => "user_a from display reminder" );
 +    $m_a->submit;
 +    $m_a->text_contains( "Reminder 'user_a from display reminder': Created",
 +        'created user_a from display reminder' );
 +
 +    $m_a->follow_link_ok( { id => 'page-reminders' } );
 +    $m_a->title_is( "Reminders for ticket #" . $ticket->id );
 +    $m_a->content_contains( 'New reminder:', 'can create a new reminder' );
 +    $m_a->content_like(
 +qr!<input[^/]+name="Complete-Reminder-$root_reminder_id"[^/]+disabled="disabled"!,
 +        "root reminder checkbox is still disabled"
 +    );
 +    $m_a->form_name('UpdateReminders');
 +    $m_a->field( 'NewReminder-Subject' => "user_a from reminders reminder" );
 +    $m_a->submit;
 +    $m_a->text_contains( "Reminder 'user_a from reminders reminder': Created",
 +        'created user_a from reminders reminder' );
 +}
 +
 +diag "grant user_a with ModifyTicket globally";
 +{
 +    ok(
 +        RT::Test->add_rights(
 +            {
 +                Principal => $user_a,
 +                Right     => [qw/ModifyTicket/],
 +            },
 +        ),
 +        'add ModifyTicket rights to user_a'
 +    );
 +
 +    $m_a->goto_ticket( $ticket->id );
 +    $m_a->content_unlike(
 +qr!<input[^/]+name="Complete-Reminder-$root_reminder_id"[^/]+disabled="disabled"!,
 +        "root reminder checkbox is enabled"
 +    );
 +    $m_a->form_name('UpdateReminders');
 +    $m_a->tick( "Complete-Reminder-$root_reminder_id" => 1 );
 +    $m_a->submit;
 +    $m_a->text_contains(
-         "Reminder 'root reminder': Status changed from 'new' to 'resolved'",
++        "Reminder 'root reminder': Status changed from 'open' to 'resolved'",
 +        'complete root reminder' );
 +
 +    $m_a->follow_link_ok( { id => 'page-reminders' } );
 +    $m_a->content_unlike(
 +qr!<input[^/]+name="Complete-Reminder-$root_reminder_id"[^/]+disabled="disabled"!,
 +        "root reminder checkbox is enabled"
 +    );
 +    $m_a->form_name('UpdateReminders');
 +    $m_a->untick( "Complete-Reminder-$root_reminder_id" => 1 );
 +    $m_a->submit;
 +    $m_a->text_contains(
 +        "Reminder 'root reminder': Status changed from 'resolved' to 'open'",
 +        'reopen root reminder' );
 +}
 +
diff --cc t/web/search_linkdisplay.t
index 0000000,c53c0c3..2d525df
mode 000000,100644..100644
--- a/t/web/search_linkdisplay.t
+++ b/t/web/search_linkdisplay.t
@@@ -1,0 -1,63 +1,63 @@@
+ use strict;
+ use warnings;
+ 
+ use RT::Test tests => undef;
+ my ( $baseurl, $m ) = RT::Test->started_ok;
+ 
+ my $ticket = RT::Test->create_ticket(
+     Queue   => 'General',
+     Subject => 'ticket foo',
+ );
+ 
+ my $generic_url = 'http://generic_url.example.com';
+ my $link = RT::Link->new( RT->SystemUser );
+ my ($id,$msg) = $link->Create( Base => $ticket->URI, Target => $generic_url, Type => 'RefersTo' );
+ ok($id, $msg);
+ 
+ 
+ my $ticket2 = RT::Test->create_ticket(
+     Queue   => 'General',
+     Subject => 'ticket bar',
+ );
+ 
+ $link = RT::Link->new( RT->SystemUser );
+ ($id,$msg) = $link->Create( Base => $ticket->URI, Target => $ticket2->URI, Type => 'RefersTo' );
+ ok($id, $msg);
+ 
+ 
+ 
+ my $class = RT::Class->new( RT->SystemUser );
+ ($id, $msg) = $class->Create( Name => 'Test Class' );
+ ok ($id, $msg);
+ 
+ my $article = RT::Article->new( RT->SystemUser );
+ ($id, $msg) = $article->Create( Class => $class->Name, Summary => 'Test Article' );
+ ok ($id, $msg);
+ $article->Load($id);
+ 
+ 
+ $link = RT::Link->new( RT->SystemUser );
+ ($id,$msg) = $link->Create( Base => $ticket->URI, Target => $article->URI, Type => 'RefersTo' );
+ ok($id, $msg);
+ 
+ 
+ ok( $m->login, 'logged in' );
+ 
+ $m->get_ok("/Search/Results.html?Format=id,RefersTo;Query=id=".$ticket->Id);
+ 
+ $m->title_is( 'Found 1 ticket', 'title' );
+ 
+ my $ref = $m->find_link( url_regex => qr!generic_url! );
+ ok( $ref, "found generic link" );
+ is( $ref->text, $generic_url, $generic_url . " is displayed" );
+ 
+ $ref = $m->find_link( url_regex => qr!/Ticket/Display.html! );
+ ok( $ref, "found ticket link" );
 -is( $ref->text, $ticket2->Id, $ticket2->Id . " is displayed" );
++is( $ref->text, "#".$ticket2->Id.": ticket bar", $ticket2->Id . " is displayed" );
+ 
+ $ref = $m->find_link( url_regex => qr!/Article/Display.html! );
+ ok( $ref, "found article link" );
+ is( $ref->text, $article->URIObj->Resolver->AsString, $article->URIObj->Resolver->AsString . " is displayed" );
+ 
+ undef $m;
+ done_testing;

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


More information about the Rt-commit mailing list