[Rt-commit] rt branch, master, updated. rt-4.1.8-476-g61c7261

Thomas Sibley trs at bestpractical.com
Thu May 23 19:27:44 EDT 2013


The branch, master has been updated
       via  61c72619788e7f20b227ddc38d1a856faba007fe (commit)
       via  793e087b3f69b9a73a5088b63dd5ddc9140b7dc2 (commit)
       via  188333d0f612fb488508ffd0cbe3bbb38e5ea2af (commit)
       via  622b072d71755d3754d5e1de6bd19bf23ed6be96 (commit)
       via  2a60f027e6e1c01b060d5161235c2ab955729e0f (commit)
       via  dd88b984096606b91030d578a64f2ee0776dc174 (commit)
       via  415a98c1af6240ad1c2ecfca37cbb40a9eff64f4 (commit)
       via  32250bdaba5989e4548e7f56c9be367646d1a9e3 (commit)
       via  babd35f1bc975123e07f31b649d10cb68b0f1d49 (commit)
       via  e3511b8e05675c1f5fb097ad7a867fd1183c278c (commit)
       via  b8105f1750064fd5d52be2a4e4810bd471c7e443 (commit)
       via  1d1ff68e5ebb7b4d066ce48c95e90c40724ad1d7 (commit)
       via  35d09122e711bf061efab1cd3f127137bcdcf10c (commit)
       via  163a5ed58573e4e5da421342f66fdbabb1ec2506 (commit)
       via  7b63729e3dadf5f62c81959e1bb1b8fa83e0cebb (commit)
       via  5e18b16bcbba75e134db895c23ce416111363fc8 (commit)
       via  e22b8ebfbcf362b502e3f3e5cf5182ef698a5fd8 (commit)
       via  6584cbe01d801a712e5b82fe7dbdaa9549b1fba7 (commit)
       via  04fa891bc0d8eb4494190501b9bf81cb35f45b9a (commit)
       via  aa76abca2e26c5d66c793be58c2d075dc2fb4334 (commit)
       via  8ecdd2bc96b77a5799d9726affaac0da969ecc42 (commit)
       via  47d3ba0aca137ffdfdfdfac5f118b584177f24b8 (commit)
       via  125848b02327cf5f3f23084d786a8a8a8c127e85 (commit)
       via  5ab23daca4db2ef54fb5973b0b7b886a299a9e18 (commit)
       via  04907cfa01be24e6466154a8350bed9c51749fb9 (commit)
       via  8dea1161afae6b5822c002abe5e7e04f4cf3e993 (commit)
       via  53fbab0f164591c564d3e786386b705443233c19 (commit)
       via  cf0a9196867078451acf2e01f9125559305735b2 (commit)
       via  b5711f117310ea4dffdb7f08a604b6c4623e7b5d (commit)
       via  dc9d249ebf19d08643b0346d53c3e28cf0bba548 (commit)
       via  90b6e7cb80c686b6bf41067029e75914748a4525 (commit)
       via  f4dec8a834ec8a21221ab48ce13bbf56f80248b1 (commit)
       via  5ff6f8d0f2a1569e10e74bfa71aac8ec8849cf5a (commit)
       via  42ee5256ef9e8402b3f84b438da6ec61e70eb728 (commit)
       via  263e69cb71714125a1f6424899f882d4ef6bf058 (commit)
       via  03d71ac3ae9581ab3c416ad656f4312775e81a00 (commit)
       via  51ab4ef85f4cabe2838bd078a138031ba690fd9c (commit)
       via  a9f5f6aa1390279b41996c3c404223288bfc3e0e (commit)
       via  082cb7537dd96b16e7b37d68d798d4e971c11fa6 (commit)
       via  fb2c9c07ebc86f5c7fef82d9704e7a6f3d7d69dc (commit)
       via  5dd6965cbb5e85e149ea22045627dcf6fb77fbe9 (commit)
       via  063e1f308ed0a2a2a099a1cc8687fc4a81f3a358 (commit)
       via  f19e3ea836a9e4612ae7256610d52014857a8c96 (commit)
       via  3af6cb052941d0fd6fab1f6bc28feaad85a1f8ee (commit)
       via  15857be24af0955e5eeb46e6bab96378704e1e09 (commit)
       via  6eac780d28eb94eab22ccfd8e0dc5170ff450231 (commit)
       via  798505e632322514ead81eee0d0b8fb32999d202 (commit)
       via  3edf1bbeff99c7903914056672c9b920dc99a76e (commit)
       via  e59d4076dfc00f4c1ce271b17bee210ea9f17de1 (commit)
       via  8a9efd5d727936e6065f6060569f71f3cc8b564a (commit)
       via  647a06d551933590a248331218ddfd96d24a3b32 (commit)
       via  00b7c820fb8faec20d46f808706e5a12cd8d6d18 (commit)
       via  9c5e5a7f8dc36f49c0b91a3094afe8cdfc4d0778 (commit)
       via  504d7bb6ad270ad8dc5a2f712b7cf8bd0a5934c7 (commit)
       via  6d0b9ae59787e0211451b48d02934569bbfc3b34 (commit)
       via  932e41b1f247c88f4cf793f68cc63182c10508bb (commit)
       via  bbbff09492490ef1e723c0d610838cee92f3cb78 (commit)
       via  15c2568d0797966c96e7541869b8e90549c874f2 (commit)
       via  ea85bb1c151507886a274bd5d869a0cd2d489b4c (commit)
       via  6f02e135e724f60ad707034d93916afc1e39362e (commit)
       via  f7100a4b9c4f15446c21575af6c77476a41e2b7b (commit)
       via  b83cb65eca749b21f152cc97f2425a94f6eec414 (commit)
       via  f270c61523c9050d08fead9af03e320706ecd99d (commit)
       via  315f007faed524e600c2c13fc4c13d8ebfa1e456 (commit)
      from  ba81e0d788d1fe4eaa35e8f9133f5920a63a3095 (commit)

Summary of changes:
 bin/rt.in                                          |  10 +-
 docs/backups.pod                                   |   6 +-
 etc/upgrade/4.0.13/schema.Oracle                   |   2 +
 etc/upgrade/4.0.13/schema.Pg                       |   2 +
 etc/upgrade/4.0.13/schema.mysql                    |   2 +
 lib/RT/Interface/Web.pm                            |  18 +-
 lib/RT/Interface/Web/Session.pm                    |  53 ++++-
 lib/RT/Lifecycle.pm                                | 145 +++++++++---
 lib/RT/Record/Role/Status.pm                       |  20 +-
 lib/RT/Reminders.pm                                |   2 +-
 lib/RT/SearchBuilder.pm                            |   2 +
 lib/RT/Ticket.pm                                   |  11 +
 lib/RT/Tickets.pm                                  |   7 +-
 lib/RT/Transaction.pm                              | 104 +++++++--
 sbin/rt-fulltext-indexer.in                        |   4 +-
 share/html/Admin/Elements/SelectNewGroupMembers    |   2 +-
 .../Tools/Shredder/Elements/Object/RT--Attachment  |   2 +-
 share/html/Admin/Users/index.html                  |   2 +-
 share/html/Download/CustomFieldValue/dhandler      |   2 +-
 share/html/Elements/ColumnMap                      |   6 +-
 share/html/Elements/EditCustomFieldBinary          |   2 +-
 share/html/Elements/MakeClicky                     |  19 +-
 share/html/Elements/SelectStatus                   |   2 +-
 share/html/Elements/ShowCustomFieldBinary          |   2 +-
 share/html/NoAuth/Logout.html                      |   2 +-
 share/html/REST/1.0/Forms/ticket/default           |   2 +-
 share/html/REST/1.0/logout                         |   5 +-
 share/html/Search/Results.html                     |   8 +
 share/html/Ticket/Attachment/dhandler              |   2 +-
 share/html/Ticket/Elements/AddAttachments          |   2 +-
 share/html/Ticket/Elements/Reminders               |   2 +-
 share/html/Ticket/Elements/ShowAttachments         |   2 +-
 share/html/m/logout                                |   2 +-
 share/html/m/ticket/show                           |   2 +-
 share/static/js/userautocomplete.js                |  18 +-
 t/api/searchbuilder.t                              |  26 ++-
 t/api/transaction-quoting.t                        | 250 +++++++++++++++++++++
 t/security/CVE-2011-5092-graph-links.t             |   2 +
 t/security/CVE-2012-4730-email-header-injection.t  | 103 +++++++++
 t/security/CVE-2012-4731-create-article.t          |  53 +++++
 t/security/CVE-2012-4732-csrf-blacklist.t          |  42 ++++
 t/security/CVE-2012-4734-login-warning.t           |  30 +++
 .../CVE-2012-4735-incoming-encryption-header.t     |  85 +++++++
 t/security/CVE-2012-4735-sign-any-key.t            |  94 ++++++++
 t/security/CVE-2012-4735-sign-encrypt-header.t     |  55 +++++
 45 files changed, 1103 insertions(+), 111 deletions(-)
 create mode 100644 etc/upgrade/4.0.13/schema.Oracle
 create mode 100644 etc/upgrade/4.0.13/schema.Pg
 create mode 100644 etc/upgrade/4.0.13/schema.mysql
 create mode 100644 t/api/transaction-quoting.t
 create mode 100644 t/security/CVE-2012-4730-email-header-injection.t
 create mode 100644 t/security/CVE-2012-4731-create-article.t
 create mode 100644 t/security/CVE-2012-4732-csrf-blacklist.t
 create mode 100644 t/security/CVE-2012-4734-login-warning.t
 create mode 100644 t/security/CVE-2012-4735-incoming-encryption-header.t
 create mode 100644 t/security/CVE-2012-4735-sign-any-key.t
 create mode 100644 t/security/CVE-2012-4735-sign-encrypt-header.t

- Log -----------------------------------------------------------------
commit 61c72619788e7f20b227ddc38d1a856faba007fe
Merge: ba81e0d 793e087
Author: Thomas Sibley <trs at bestpractical.com>
Date:   Thu May 23 15:53:16 2013 -0700

    Merge branch '4.0-trunk'
    
    Conflicts:
    	lib/RT/Interface/Web.pm
    	lib/RT/Lifecycle.pm
    	lib/RT/Ticket.pm
    	lib/RT/Tickets_SQL.pm
    	lib/RT/Transaction.pm
    	share/html/Elements/ColumnMap
    	share/html/Elements/MakeClicky
    	share/html/NoAuth/js/userautocomplete.js
    	share/html/Ticket/Attachment/dhandler
    	share/html/Ticket/Elements/AddAttachments
    	share/html/Ticket/Elements/Reminders
    	share/html/Ticket/Elements/ShowAttachments
    	share/html/Ticket/Elements/ShowTransactionAttachments
    	share/html/m/ticket/show

diff --cc lib/RT/Interface/Web.pm
index ac6f9a5,dcf89fa..ad9c247
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@@ -1983,21 -1750,18 +1984,21 @@@ sub CreateTicket 
          Interface => RT::Interface::Web::MobileClient() ? 'Mobile' : 'Web',
      );
  
 -    if ( $ARGS{'Attachments'} ) {
 -        my $rv = $MIMEObj->make_multipart;
 -        $RT::Logger->error("Couldn't make multipart message")
 -            if !$rv || $rv !~ /^(?:DONE|ALREADY)$/;
 +    my @attachments;
 +    if ( my $tmp = $session{'Attachments'}{ $ARGS{'Token'} || '' } ) {
-         push @attachments, grep $_, values %$tmp;
++        push @attachments, grep $_, map $tmp->{$_}, sort keys %$tmp;
  
 -        foreach ( map $ARGS{Attachments}->{$_}, sort keys %{ $ARGS{'Attachments'} } ) {
 -            unless ($_) {
 -                $RT::Logger->error("Couldn't add empty attachemnt");
 -                next;
 -            }
 -            $MIMEObj->add_part($_);
 -        }
 +        delete $session{'Attachments'}{ $ARGS{'Token'} || '' }
 +            unless $ARGS{'KeepAttachments'};
 +        $session{'Attachments'} = $session{'Attachments'}
 +            if @attachments;
 +    }
 +    if ( $ARGS{'Attachments'} ) {
-         push @attachments, grep $_, values %{ $ARGS{'Attachments'} };
++        push @attachments, grep $_, map $ARGS{Attachments}->{$_}, sort keys %{ $ARGS{'Attachments'} };
 +    }
 +    if ( @attachments ) {
 +        $MIMEObj->make_multipart;
 +        $MIMEObj->add_part( $_ ) foreach @attachments;
      }
  
      for my $argument (qw(Encrypt Sign)) {
@@@ -2113,17 -1932,10 +2114,18 @@@ sub ProcessUpdateMessage 
          @_
      );
  
 -    if ( $args{ARGSRef}->{'UpdateAttachments'}
 -        && !keys %{ $args{ARGSRef}->{'UpdateAttachments'} } )
 -    {
 -        delete $args{ARGSRef}->{'UpdateAttachments'};
 +    my @attachments;
 +    if ( my $tmp = $session{'Attachments'}{ $args{'ARGSRef'}{'Token'} || '' } ) {
-         push @attachments, grep $_, values %$tmp;
++        push @attachments, grep $_, map $tmp->{$_}, sort keys %$tmp;
 +
 +        delete $session{'Attachments'}{ $args{'ARGSRef'}{'Token'} || '' }
 +            unless $args{'KeepAttachments'};
 +        $session{'Attachments'} = $session{'Attachments'}
 +            if @attachments;
 +    }
 +    if ( $args{ARGSRef}{'UpdateAttachments'} ) {
-         push @attachments, grep $_, values %{ $args{ARGSRef}{'UpdateAttachments'} };
++        push @attachments, grep $_, map $args{ARGSRef}->{UpdateAttachments}{$_},
++                                   sort keys %{ $args{ARGSRef}->{'UpdateAttachments'} };
      }
  
      # Strip the signature
diff --cc lib/RT/Lifecycle.pm
index 2c30a4a,3731cdb..b15a039
--- a/lib/RT/Lifecycle.pm
+++ b/lib/RT/Lifecycle.pm
@@@ -647,58 -649,129 +655,135 @@@ sub FillCache 
      %LIFECYCLES_CACHE = %LIFECYCLES = %$map;
      $_ = { %$_ } foreach values %LIFECYCLES_CACHE;
  
-     for my $lifecycle ( values %LIFECYCLES_CACHE ) {
 -    my %all = (
 -        '' => [],
 -        initial => [],
 -        active => [],
 -        inactive => [],
 -    );
+     foreach my $name ( keys %LIFECYCLES_CACHE ) {
+         next if $name eq "__maps__";
+         my $lifecycle = $LIFECYCLES_CACHE{$name};
+ 
 +        my $type = $lifecycle->{type} ||= 'ticket';
 +        $LIFECYCLES_TYPES{$type} ||= {
 +            '' => [],
 +            initial => [],
 +            active => [],
 +            inactive => [],
 +            actions => [],
 +        };
 +
-         my @res;
+         my @statuses;
+         $lifecycle->{canonical_case} = {};
 -        foreach my $type ( qw(initial active inactive) ) {
 -            for my $status (@{ $lifecycle->{ $type } || [] }) {
 +        foreach my $category ( qw(initial active inactive) ) {
-             my @vals = @{ $lifecycle->{ $category } ||= [] };
-             push @{ $LIFECYCLES_TYPES{$type}{$category} }, @vals;
-             push @res,                                     @vals;
++            for my $status (@{ $lifecycle->{ $category } || [] }) {
+                 if (exists $lifecycle->{canonical_case}{lc $status}) {
+                     warn "Duplicate status @{[lc $status]} in lifecycle $name";
+                 } else {
+                     $lifecycle->{canonical_case}{lc $status} = $status;
+                 }
 -                push @{ $all{ $type } }, $status;
++                push @{ $LIFECYCLES_TYPES{$type}{$category} }, $status;
+                 push @statuses, $status;
+             }
+         }
+ 
+         # Lower-case for consistency
+         # ->{actions} are handled below
+         for my $state (keys %{ $lifecycle->{defaults} || {} }) {
+             my $status = $lifecycle->{defaults}{$state};
+             warn "Nonexistant status @{[lc $status]} in default states in $name lifecycle"
+                 unless $lifecycle->{canonical_case}{lc $status};
+             $lifecycle->{defaults}{$state} =
+                 $lifecycle->{canonical_case}{lc $status} || lc $status;
+         }
+         for my $from (keys %{ $lifecycle->{transitions} || {} }) {
+             warn "Nonexistant status @{[lc $from]} in transitions in $name lifecycle"
+                 unless $from eq '' or $lifecycle->{canonical_case}{lc $from};
+             for my $status ( @{delete($lifecycle->{transitions}{$from}) || []} ) {
+                 warn "Nonexistant status @{[lc $status]} in transitions in $name lifecycle"
+                     unless $lifecycle->{canonical_case}{lc $status};
+                 push @{ $lifecycle->{transitions}{lc $from} },
+                     $lifecycle->{canonical_case}{lc $status} || lc $status;
+             }
+         }
+         for my $schema (keys %{ $lifecycle->{rights} || {} }) {
+             my ($from, $to) = split /\s*->\s*/, $schema, 2;
+             unless ($from and $to) {
+                 warn "Invalid right transition $schema in $name lifecycle";
+                 next;
+             }
+             warn "Nonexistant status @{[lc $from]} in right transition in $name lifecycle"
+                 unless $from eq '*' or $lifecycle->{canonical_case}{lc $from};
+             warn "Nonexistant status @{[lc $to]} in right transition in $name lifecycle"
+                 unless $to eq '*' or $lifecycle->{canonical_case}{lc $to};
+             $lifecycle->{rights}{lc($from) . " -> " .lc($to)}
+                 = delete $lifecycle->{rights}{$schema};
          }
  
          my %seen;
-         @res = grep !$seen{ lc $_ }++, @res;
-         $lifecycle->{''} = \@res;
+         @statuses = grep !$seen{ lc $_ }++, @statuses;
+         $lifecycle->{''} = \@statuses;
  
          unless ( $lifecycle->{'transitions'}{''} ) {
-             $lifecycle->{'transitions'}{''} = [ grep $_ ne 'deleted', @res ];
+             $lifecycle->{'transitions'}{''} = [ grep lc $_ ne 'deleted', @statuses ];
+         }
+ 
+         my @actions;
+         if ( ref $lifecycle->{'actions'} eq 'HASH' ) {
+             foreach my $k ( sort keys %{ $lifecycle->{'actions'} } ) {
+                 push @actions, $k, $lifecycle->{'actions'}{ $k };
+             }
+         } elsif ( ref $lifecycle->{'actions'} eq 'ARRAY' ) {
+             @actions = @{ $lifecycle->{'actions'} };
+         }
+ 
+         $lifecycle->{'actions'} = [];
+         while ( my ($transition, $info) = splice @actions, 0, 2 ) {
+             my ($from, $to) = split /\s*->\s*/, $transition, 2;
+             unless ($from and $to) {
+                 warn "Invalid action status change $transition in $name lifecycle";
+                 next;
+             }
+             warn "Nonexistant status @{[lc $from]} in action in $name lifecycle"
+                 unless $from eq '*' or $lifecycle->{canonical_case}{lc $from};
+             warn "Nonexistant status @{[lc $to]} in action in $name lifecycle"
+                 unless $to eq '*' or $lifecycle->{canonical_case}{lc $to};
+             push @{ $lifecycle->{'actions'} },
+                 { %$info,
+                   from => ($lifecycle->{canonical_case}{lc $from} || lc $from),
+                   to   => ($lifecycle->{canonical_case}{lc $to}   || lc $to),   };
+         }
+     }
+ 
+     # Lower-case the transition maps
+     for my $mapname (keys %{ $LIFECYCLES_CACHE{'__maps__'} || {} }) {
+         my ($from, $to) = split /\s*->\s*/, $mapname, 2;
+         unless ($from and $to) {
+             warn "Invalid lifecycle mapping $mapname";
+             next;
+         }
+         warn "Nonexistant lifecycle $from in $mapname lifecycle map"
+             unless $LIFECYCLES_CACHE{$from};
+         warn "Nonexistant lifecycle $to in $mapname lifecycle map"
+             unless $LIFECYCLES_CACHE{$to};
+         my $map = delete $LIFECYCLES_CACHE{'__maps__'}{$mapname};
+         $LIFECYCLES_CACHE{'__maps__'}{"$from -> $to"} = $map;
+         for my $status (keys %{ $map }) {
+             warn "Nonexistant status @{[lc $status]} in $from in $mapname lifecycle map"
+                 if $LIFECYCLES_CACHE{$from}
+                     and not $LIFECYCLES_CACHE{$from}{canonical_case}{lc $status};
+             warn "Nonexistant status @{[lc $map->{$status}]} in $to in $mapname lifecycle map"
+                 if $LIFECYCLES_CACHE{$to}
+                     and not $LIFECYCLES_CACHE{$to}{canonical_case}{lc $map->{$status}};
+             $map->{lc $status} = lc delete $map->{$status};
          }
      }
+ 
 -    foreach my $type ( qw(initial active inactive), '' ) {
 -        my %seen;
 -        @{ $all{ $type } } = grep !$seen{ lc $_ }++, @{ $all{ $type } };
 -        push @{ $all{''} }, @{ $all{ $type } } if $type;
 +    for my $type (keys %LIFECYCLES_TYPES) {
 +        for my $category ( qw(initial active inactive), '' ) {
 +            my %seen;
 +            @{ $LIFECYCLES_TYPES{$type}{$category} } =
 +                grep !$seen{ lc $_ }++, @{ $LIFECYCLES_TYPES{$type}{$category} };
 +            push @{ $LIFECYCLES_TYPES{$type}{''} },
 +                @{ $LIFECYCLES_TYPES{$type}{$category} } if $category;
 +        }
      }
 -    $LIFECYCLES_CACHE{''} = \%all;
  
-     foreach my $lifecycle ( values %LIFECYCLES_CACHE ) {
-         my @res;
-         if ( ref $lifecycle->{'actions'} eq 'HASH' ) {
-             foreach my $k ( sort keys %{ $lifecycle->{'actions'} } ) {
-                 push @res, $k, $lifecycle->{'actions'}{ $k };
-             }
-         } elsif ( ref $lifecycle->{'actions'} eq 'ARRAY' ) {
-             @res = @{ $lifecycle->{'actions'} };
-         }
- 
-         my @tmp = splice @res;
-         while ( my ($transition, $info) = splice @tmp, 0, 2 ) {
-             my ($from, $to) = split /\s*->\s*/, $transition, 2;
-             push @res, { %$info, from => $from, to => $to };
-         }
-         $lifecycle->{'actions'} = \@res;
-     }
      return;
  }
  
diff --cc lib/RT/Record/Role/Status.pm
index ad96c2a,0000000..f0c99cc
mode 100644,000000..100644
--- a/lib/RT/Record/Role/Status.pm
+++ b/lib/RT/Record/Role/Status.pm
@@@ -1,302 -1,0 +1,314 @@@
 +# 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 }}}
 +
 +use strict;
 +use warnings;
 +
 +package RT::Record::Role::Status;
 +use Role::Basic;
 +use Scalar::Util qw(blessed);
 +
 +=head1 NAME
 +
 +RT::Record::Role::Status - Common methods for records which have a Status column
 +
 +=head1 DESCRIPTION
 +
 +Lifecycles are generally set on container records, and Statuses on records
 +which belong to one of those containers.  L<RT::Record::Role::Lifecycle>
 +handles the containers with the I<Lifecycle> column.  This role is for the
 +records with a I<Status> column within those containers.  It includes
 +convenience methods for grabbing an L<RT::Lifecycle> object as well setters for
 +validating I<Status> and the column which points to the container object.
 +
 +=head1 REQUIRES
 +
 +=head2 L<RT::Record::Role>
 +
 +=head2 LifecycleColumn
 +
 +Used as a role parameter.  Must return a string of the column name which points
 +to the container object that consumes L<RT::Record::Role::Lifecycle> (or
 +conforms to it).  The resulting string is used to construct two method names:
 +as-is to fetch the column value and suffixed with "Obj" to fetch the object.
 +
 +=head2 Status
 +
 +A Status method which returns a lifecycle name is required.  Currently
 +unenforced at compile-time due to poor interactions with
 +L<DBIx::SearchBuilder::Record/AUTOLOAD>.  You'll hit run-time errors if this
 +method isn't available in consuming classes, however.
 +
 +=cut
 +
 +with 'RT::Record::Role';
 +requires 'LifecycleColumn';
 +
- # XXX: can't require column methods due to DBIx::SB::Record's AUTOLOAD
- #requires 'Status';
- 
 +=head1 PROVIDES
 +
++=head2 Status
++
++Returns the Status for this record, in the canonical casing.
++
++=cut
++
++sub Status {
++    my $self = shift;
++    my $value = $self->_Value( 'Status' );
++    my $lifecycle = $self->LifecycleObj;
++    return $value unless $lifecycle;
++    return $lifecycle->CanonicalCase( $value );
++}
++
 +=head2 LifecycleObj
 +
 +Returns an L<RT::Lifecycle> object for this record's C<Lifecycle>.  If called
 +as a class method, returns an L<RT::Lifecycle> object which is an aggregation
 +of all lifecycles of the appropriate type.
 +
 +=cut
 +
 +sub LifecycleObj {
 +    my $self = shift;
 +    my $obj  = $self->LifecycleColumn . "Obj";
 +    return $self->$obj->LifecycleObj;
 +}
 +
 +=head2 Lifecycle
 +
 +Returns the L<RT::Lifecycle/Name> of this record's L</LifecycleObj>.
 +
 +=cut
 +
 +sub Lifecycle {
 +    my $self = shift;
 +    return $self->LifecycleObj->Name;
 +}
 +
 +=head2 ValidateStatus
 +
 +Takes a status.  Returns true if that status is a valid status for this record,
 +otherwise returns false.
 +
 +=cut
 +
 +sub ValidateStatus {
 +    my $self = shift;
 +    return $self->LifecycleObj->IsValid(@_);
 +}
 +
 +=head2 ValidateStatusChange
 +
 +Validates the new status with the current lifecycle.  Returns a tuple of (OK,
 +message).
 +
 +Expected to be called from this role's L</SetStatus> or the consuming class'
 +equivalent.
 +
 +=cut
 +
 +sub ValidateStatusChange {
 +    my $self = shift;
 +    my $new  = shift;
 +    my $old  = $self->Status;
 +
 +    my $lifecycle = $self->LifecycleObj;
 +
 +    unless ( $lifecycle->IsValid( $new ) ) {
 +        return (0, $self->loc("Status '[_1]' isn't a valid status for this [_2].", $self->loc($new), $self->loc($lifecycle->Type)));
 +    }
 +
 +    unless ( $lifecycle->IsTransition( $old => $new ) ) {
 +        return (0, $self->loc("You can't change status from '[_1]' to '[_2]'.", $self->loc($old), $self->loc($new)));
 +    }
 +
 +    my $check_right = $lifecycle->CheckRight( $old => $new );
 +    unless ( $self->CurrentUser->HasRight( Right => $check_right, Object => $self ) ) {
 +        return ( 0, $self->loc('Permission Denied') );
 +    }
 +
 +    return 1;
 +}
 +
 +=head2 SetStatus
 +
 +Validates the status transition before updating the Status column.  This method
 +may want to be overridden by a more specific method in the consuming class.
 +
 +=cut
 +
 +sub SetStatus {
 +    my $self = shift;
 +    my $new  = shift;
 +
 +    my ($valid, $error) = $self->ValidateStatusChange($new);
 +    return ($valid, $error) unless $valid;
 +
 +    return $self->_SetStatus( Status => $new );
 +}
 +
 +=head2 _SetStatus
 +
 +Sets the Status column without validating the change.  Intended to be used
 +as-is by methods provided by the role, or overridden in the consuming class to
 +take additional action.  For example, L<RT::Ticket/_SetStatus> sets the Started
 +and Resolved dates on the ticket as necessary.
 +
 +Takes a paramhash where the only required key is Status.  Other keys may
 +include Lifecycle and NewLifecycle when called from L</_SetLifecycleColumn>,
 +which may assist consuming classes.  NewLifecycle defaults to Lifecycle if not
 +provided; this indicates the lifecycle isn't changing.
 +
 +=cut
 +
 +sub _SetStatus {
 +    my $self = shift;
 +    my %args = (
 +        Status      => undef,
 +        Lifecycle   => $self->LifecycleObj,
 +        @_,
 +    );
++    $args{Status} = lc $args{Status} if defined $args{Status};
 +    $args{NewLifecycle} ||= $args{Lifecycle};
 +
 +    return $self->_Set(
 +        Field   => 'Status',
 +        Value   => $args{Status},
 +    );
 +}
 +
 +=head2 _SetLifecycleColumn
 +
 +Validates and updates the column named by L</LifecycleColumn>.  The Status
 +column is also updated if necessary (via lifecycle transition maps).
 +
 +On success, returns a tuple of (1, I<message>, I<new status>) where I<new
 +status> is the status that was transitioned to, if any.  On failure, returns
 +(0, I<error message>).
 +
 +Takes a paramhash with keys I<Value> and (optionally) I<RequireRight>.
 +I<RequireRight> is a right name which the current user must have on the new
 +L</LifecycleColumn> object in order for the method to succeed.
 +
 +This method is expected to be used from within another method such as
 +L<RT::Ticket/SetQueue>.
 +
 +=cut
 +
 +sub _SetLifecycleColumn {
 +    my $self = shift;
 +    my %args = @_;
 +
 +    my $column     = $self->LifecycleColumn;
 +    my $column_obj = "${column}Obj";
 +
 +    my $current = $self->$column_obj;
 +    my $class   = blessed($current);
 +
 +    my $new = $class->new( $self->CurrentUser );
 +    $new->Load($args{Value});
 +
 +    return (0, $self->loc("[_1] [_2] does not exist", $self->loc($column), $args{Value}))
 +        unless $new->id;
 +
 +    my $name = eval { $current->Name } || $current->id;
 +
 +    return (0, $self->loc("[_1] [_2] is disabled", $self->loc($column), $name))
 +        if $new->Disabled;
 +
 +    return (0, $self->loc("[_1] is already set to [_2]", $self->loc($column), $name))
 +        if $new->id == $current->id;
 +
 +    return (0, $self->loc("Permission Denied"))
 +        if $args{RequireRight} and not $self->CurrentUser->HasRight(
 +            Right   => $args{RequireRight},
 +            Object  => $new,
 +        );
 +
 +    my $new_status;
 +    my $old_lifecycle = $current->LifecycleObj;
 +    my $new_lifecycle = $new->LifecycleObj;
 +    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 lifecycle [_1] and [_2]. Contact your system administrator.", $old_lifecycle->Name, $new_lifecycle->Name) );
 +        }
-         $new_status = $old_lifecycle->MoveMap( $new_lifecycle )->{ $self->Status };
++        $new_status = $old_lifecycle->MoveMap( $new_lifecycle )->{ lc $self->Status };
 +        return ( 0, $self->loc("Mapping between lifecycle [_1] and [_2] is incomplete. Contact your system administrator.", $old_lifecycle->Name, $new_lifecycle->Name) )
 +            unless $new_status;
 +    }
 +
 +    my ($ok, $msg) = $self->_Set( Field => $column, Value => $new->id );
 +    if ($ok) {
 +        if ( $new_status ) {
 +            my $as_system = blessed($self)->new( RT->SystemUser );
 +            $as_system->Load( $self->Id );
 +            unless ( $as_system->Id ) {
 +                return ( 0, $self->loc("Couldn't load copy of [_1] #[_2]", blessed($self), $self->Id) );
 +            }
 +
 +            my ($val, $msg) = $as_system->_SetStatus(
 +                Lifecycle       => $old_lifecycle,
 +                NewLifecycle    => $new_lifecycle,
 +                Status          => $new_status,
 +            );
 +
 +            if ($val) {
 +                # Pick up the change made by the clone above
 +                $self->Load( $self->id );
 +            } else {
 +                RT->Logger->error("Status change to $new_status failed on $column change: $msg");
 +                undef $new_status;
 +            }
 +        }
 +        return (1, $msg, $new_status);
 +    } else {
 +        return (0, $msg);
 +    }
 +}
 +
 +1;
diff --cc lib/RT/Ticket.pm
index e540403,5acdb31..0c745de
--- a/lib/RT/Ticket.pm
+++ b/lib/RT/Ticket.pm
@@@ -2354,35 -3176,27 +2364,36 @@@ sub SetStatus 
      # this option was added for rtir initially
      $args{SetStarted} = 1 unless exists $args{SetStarted};
  
 +    my ($valid, $msg) = $self->ValidateStatusChange($args{Status});
 +    return ($valid, $msg) unless $valid;
  
 -    my $lifecycle = $self->QueueObj->Lifecycle;
 -
 -    my $new = lc $args{'Status'};
 -    unless ( $lifecycle->IsValid( $new ) ) {
 -        return (0, $self->loc("Status '[_1]' isn't a valid status for tickets in this queue.", $self->loc($new)));
 -    }
 +    my $lifecycle = $self->LifecycleObj;
  
 -    my $old = $self->__Value('Status');
 -    unless ( $lifecycle->IsTransition( $old => $new ) ) {
 -        return (0, $self->loc("You can't change status from '[_1]' to '[_2]'.", $self->loc($old), $self->loc($new)));
 +    if (   !$args{Force}
 +        && !$lifecycle->IsInactive($self->Status)
 +        && $lifecycle->IsInactive($args{Status})
 +        && $self->HasUnresolvedDependencies )
 +    {
 +        return ( 0, $self->loc('That ticket has unresolved dependencies') );
      }
  
 -    my $check_right = $lifecycle->CheckRight( $old => $new );
 -    unless ( $self->CurrentUserHasRight( $check_right ) ) {
 -        return ( 0, $self->loc('Permission Denied') );
 -    }
 +    return $self->_SetStatus(
 +        Status     => $args{Status},
 +        SetStarted => $args{SetStarted},
 +    );
 +}
  
 -    if ( !$args{Force} && $lifecycle->IsInactive( $new ) && $self->HasUnresolvedDependencies) {
 -        return (0, $self->loc('That ticket has unresolved dependencies'));
 -    }
 +sub _SetStatus {
 +    my $self = shift;
 +    my %args = (
 +        Status => undef,
 +        SetStarted => 1,
 +        RecordTransaction => 1,
 +        Lifecycle => $self->LifecycleObj,
 +        @_,
 +    );
++    $args{Status} = lc $args{Status} if defined $args{Status};
 +    $args{NewLifecycle} ||= $args{Lifecycle};
  
      my $now = RT::Date->new( $self->CurrentUser );
      $now->SetToNow();
diff --cc lib/RT/Tickets.pm
index df0a4c5,41aa313..8b718ff
--- a/lib/RT/Tickets.pm
+++ b/lib/RT/Tickets.pm
@@@ -370,8 -380,10 +370,10 @@@ sub _EnumLimit 
          $value = $o->Id || 0;
      } elsif ( $field eq "Type" ) {
          $value = lc $value if $value =~ /^(ticket|approval|reminder)$/i;
+     } elsif ($field eq "Status") {
+         $value = lc $value;
      }
 -    $sb->_SQLLimit(
 +    $sb->Limit(
          FIELD    => $field,
          VALUE    => $value,
          OPERATOR => $op,
@@@ -2832,212 -3571,6 +2834,213 @@@ BUG: There should be an API for thi
  
  =cut
  
 +=head2 FromSQL
 +
 +Convert a RT-SQL string into a set of SearchBuilder restrictions.
 +
 +Returns (1, 'Status message') on success and (0, 'Error Message') on
 +failure.
 +
 +=cut
 +
 +sub _parser {
 +    my ($self,$string) = @_;
 +    my $ea = '';
 +
 +    # Bundling of joins is implemented by dynamically tracking a parallel query
 +    # tree in %sub_tree as the TicketSQL is parsed.
 +    #
 +    # Only positive, OR'd watcher conditions are bundled currently.  Each key
 +    # in %sub_tree is a watcher type (Requestor, Cc, AdminCc) or the generic
 +    # "Watcher" for any watcher type.  Owner is not bundled because it is
 +    # denormalized into a Tickets column and doesn't need a join.  AND'd
 +    # conditions are not bundled since a record may have multiple watchers
 +    # which independently match the conditions, thus necessitating two joins.
 +    #
 +    # The values of %sub_tree are arrayrefs made up of:
 +    #
 +    #   * Open parentheses "(" pushed on by the OpenParen callback
 +    #   * Arrayrefs of bundled join aliases pushed on by the Condition callback
 +    #   * Entry aggregators (AND/OR) pushed on by the EntryAggregator callback
 +    #
 +    # The CloseParen callback takes care of backing off the query trees until
 +    # outside of the just-closed parenthetical, thus restoring the tree state
 +    # an equivalent of before the parenthetical was entered.
 +    #
 +    # The Condition callback handles starting a new subtree or extending an
 +    # existing one, determining if bundling the current condition with any
 +    # subtree is possible, and pruning any dangling entry aggregators from
 +    # trees.
 +    #
 +
 +    my %sub_tree;
 +    my $depth = 0;
 +
 +    my %callback;
 +    $callback{'OpenParen'} = sub {
 +      $self->_OpenParen;
 +      $depth++;
 +      push @$_, '(' foreach values %sub_tree;
 +    };
 +    $callback{'CloseParen'} = sub {
 +      $self->_CloseParen;
 +      $depth--;
 +      foreach my $list ( values %sub_tree ) {
 +          if ( $list->[-1] eq '(' ) {
 +              pop @$list;
 +              pop @$list if $list->[-1] =~ /^(?:AND|OR)$/i;
 +          }
 +          else {
 +              pop @$list while $list->[-2] ne '(';
 +              $list->[-1] = pop @$list;
 +          }
 +      }
 +    };
 +    $callback{'EntryAggregator'} = sub {
 +      $ea = $_[0] || '';
 +      push @$_, $ea foreach grep @$_ && $_->[-1] ne '(', values %sub_tree;
 +    };
 +    $callback{'Condition'} = sub {
 +        my ($key, $op, $value) = @_;
 +
 +        my $negative_op = ($op eq '!=' || $op =~ /\bNOT\b/i);
 +        my $null_op = ( 'is not' eq lc($op) || 'is' eq lc($op) );
 +        # key has dot then it's compound variant and we have subkey
 +        my $subkey = '';
 +        ($key, $subkey) = ($1, $2) if $key =~ /^([^\.]+)\.(.+)$/;
 +
 +        # normalize key and get class (type)
 +        my $class;
 +        if (exists $LOWER_CASE_FIELDS{lc $key}) {
 +            $key = $LOWER_CASE_FIELDS{lc $key};
 +            $class = $FIELD_METADATA{$key}->[0];
 +        }
 +        die "Unknown field '$key' in '$string'" unless $class;
 +
 +        # replace __CurrentUser__ with id
 +        $value = $self->CurrentUser->id if $value eq '__CurrentUser__';
 +
 +
 +        unless( $dispatch{ $class } ) {
 +            die "No dispatch method for class '$class'"
 +        }
 +        my $sub = $dispatch{ $class };
 +
 +        my @res; my $bundle_with;
 +        if ( $class eq 'WATCHERFIELD' && $key ne 'Owner' && !$negative_op && (!$null_op || $subkey) ) {
 +            if ( !$sub_tree{$key} ) {
 +              $sub_tree{$key} = [ ('(')x$depth, \@res ];
 +            } else {
 +              $bundle_with = $self->_check_bundling_possibility( $string, @{ $sub_tree{$key} } );
 +              if ( $sub_tree{$key}[-1] eq '(' ) {
 +                    push @{ $sub_tree{$key} }, \@res;
 +              }
 +            }
 +        }
 +
 +        # Remove our aggregator from subtrees where our condition didn't get added
 +        pop @$_ foreach grep @$_ && $_->[-1] =~ /^(?:AND|OR)$/i, values %sub_tree;
 +
 +        # A reference to @res may be pushed onto $sub_tree{$key} from
 +        # above, and we fill it here.
 +        @res = $sub->( $self, $key, $op, $value,
 +                SUBCLAUSE       => '',  # don't need anymore
 +                ENTRYAGGREGATOR => $ea,
 +                SUBKEY          => $subkey,
 +                BUNDLE          => $bundle_with,
 +              );
 +        $ea = '';
 +    };
 +    RT::SQL::Parse($string, \%callback);
 +}
 +
 +sub FromSQL {
 +    my ($self,$query) = @_;
 +
 +    {
 +        # preserve first_row and show_rows across the CleanSlate
 +        local ($self->{'first_row'}, $self->{'show_rows'}, $self->{_sql_looking_at});
 +        $self->CleanSlate;
 +        $self->_InitSQL();
 +    }
 +
 +    return (1, $self->loc("No Query")) unless $query;
 +
 +    $self->{_sql_query} = $query;
 +    eval {
 +        local $self->{parsing_ticketsql} = 1;
 +        $self->_parser( $query );
 +    };
 +    if ( $@ ) {
-         $RT::Logger->error( $@ );
-         return (0, $@);
++        my $error = "$@";
++        $RT::Logger->error("Couldn't parse query: $error");
++        return (0, $error);
 +    }
 +
 +    # We only want to look at EffectiveId's (mostly) for these searches.
 +    unless ( $self->{_sql_looking_at}{effectiveid} ) {
 +        # instead of EffectiveId = id we do IsMerged IS NULL
 +        $self->Limit(
 +            FIELD           => 'IsMerged',
 +            OPERATOR        => 'IS',
 +            VALUE           => 'NULL',
 +            ENTRYAGGREGATOR => 'AND',
 +            QUOTEVALUE      => 0,
 +        );
 +    }
 +    unless ( $self->{_sql_looking_at}{type} ) {
 +        $self->Limit( FIELD => 'Type', VALUE => 'ticket' );
 +    }
 +
 +    # We don't want deleted tickets unless 'allow_deleted_search' is set
 +    unless( $self->{'allow_deleted_search'} ) {
 +        $self->Limit(
 +            FIELD    => 'Status',
 +            OPERATOR => '!=',
 +            VALUE => 'deleted',
 +        );
 +    }
 +
 +    # set SB's dirty flag
 +    $self->{'must_redo_search'} = 1;
 +    $self->{'RecalcTicketLimits'} = 0;
 +
 +    return (1, $self->loc("Valid Query"));
 +}
 +
 +=head2 Query
 +
 +Returns the last string passed to L</FromSQL>.
 +
 +=cut
 +
 +sub Query {
 +    my $self = shift;
 +    return $self->{_sql_query};
 +}
 +
 +sub _check_bundling_possibility {
 +    my $self = shift;
 +    my $string = shift;
 +    my @list = reverse @_;
 +    while (my $e = shift @list) {
 +        next if $e eq '(';
 +        if ( lc($e) eq 'and' ) {
 +            return undef;
 +        }
 +        elsif ( lc($e) eq 'or' ) {
 +            return shift @list;
 +        }
 +        else {
 +            # should not happen
 +            $RT::Logger->error(
 +                "Joins optimization failed when parsing '$string'. It's bug in RT, contact Best Practical"
 +            );
 +            die "Internal error. Contact your system administrator.";
 +        }
 +    }
 +    return undef;
 +}
  
  
  =head2 NewItem
diff --cc lib/RT/Transaction.pm
index 67e650c,28c8d9e..1d2773e
--- a/lib/RT/Transaction.pm
+++ b/lib/RT/Transaction.pm
@@@ -645,94 -692,34 +708,97 @@@ sub BriefDescriptionAsHTML 
          return $self->loc("No transaction type specified");
      }
  
 -    my $obj_type = $self->FriendlyObjectType;
 +    my ($template, @params);
 +
 +    my @code = grep { ref eq 'CODE' } map { $_BriefDescriptions{$_} }
 +        ( $field
 +            ? ("$objecttype-$type-$field", "$type-$field")
 +            : () ),
 +        "$objecttype-$type", $type;
  
 -    if ( $type eq 'Create' ) {
 -        return ( $self->loc( "[_1] created", $obj_type ) );
 +    if (@code) {
 +        ($template, @params) = $code[0]->($self);
      }
 -    elsif ( $type eq 'Enabled' ) {
 -        return ( $self->loc( "[_1] enabled", $obj_type ) );
 +
 +    unless ($template) {
 +        ($template, @params) = (
 +            "Default: [_1]/[_2] changed from [_3] to [_4]", #loc
 +            $type,
 +            $field,
 +            (
 +                $self->OldValue
 +                ? "'" . $self->OldValue . "'"
 +                : $self->loc("(no value)")
 +            ),
 +            (
 +                $self->NewValue
 +                ? "'" . $self->NewValue . "'"
 +                : $self->loc("(no value)")
 +            ),
 +        );
      }
 -    elsif ( $type eq 'Disabled' ) {
 -        return ( $self->loc( "[_1] disabled", $obj_type ) );
 +    return $self->loc($template, $self->_ProcessReturnValues(@params));
 +}
 +
 +sub _ProcessReturnValues {
 +    my $self   = shift;
 +    my @values = @_;
 +    return map {
 +        if    (ref eq 'ARRAY')  { $_ = join "", $self->_ProcessReturnValues(@$_) }
 +        elsif (ref eq 'SCALAR') { $_ = $$_ }
 +        else                    { RT::Interface::Web::EscapeHTML(\$_) }
 +        $_
 +    } @values;
 +}
 +
 +sub _FormatPrincipal {
 +    my $self = shift;
 +    my $principal = shift;
 +    if ($principal->IsUser) {
 +        return $self->_FormatUser( $principal->Object );
 +    } else {
 +        return $self->loc("group [_1]", $principal->Object->Name);
      }
 -    elsif ( $type =~ /Status/ ) {
 +}
 +
 +sub _FormatUser {
 +    my $self = shift;
 +    my $user = shift;
 +    return [
 +        \'<span class="user" data-replace="user" data-user-id="', $user->id, \'">',
 +        $user->Format,
 +        \'</span>'
 +    ];
 +}
 +
 +%_BriefDescriptions = (
 +    Create => sub {
 +        my $self = shift;
 +        return ( "[_1] created", $self->FriendlyObjectType );   #loc
 +    },
 +    Enabled => sub {
 +        my $self = shift;
 +        return ( "[_1] enabled", $self->FriendlyObjectType );   #loc
 +    },
 +    Disabled => sub {
 +        my $self = shift;
 +        return ( "[_1] disabled", $self->FriendlyObjectType );  #loc
 +    },
 +    Status => sub {
 +        my $self = shift;
          if ( $self->Field eq 'Status' ) {
              if ( $self->NewValue eq 'deleted' ) {
 -                return ( $self->loc( "[_1] deleted", $obj_type ) );
 +                return ( "[_1] deleted", $self->FriendlyObjectType );   #loc
              }
              else {
 -                my $canon = $self->Object->can("QueueObj")
 -                    ? sub { $self->Object->QueueObj->Lifecycle->CanonicalCase(@_) }
++                my $canon = $self->Object->DOES("RT::Record::Role::Status")
++                    ? sub { $self->Object->LifecycleObj->CanonicalCase(@_) }
+                     : sub { return $_[0] };
                  return (
 -                    $self->loc(
 -                        "Status changed from [_1] to [_2]",
 -                        "'" . $self->loc( $canon->($self->OldValue) ) . "'",
 -                        "'" . $self->loc( $canon->($self->NewValue) ) . "'"
 -                    )
 +                    "Status changed from [_1] to [_2]",                 #loc
-                     "'" . $self->loc( $self->OldValue ) . "'",
-                     "'" . $self->loc( $self->NewValue ) . "'"
++                    "'" . $self->loc( $canon->($self->OldValue) ) . "'",
++                    "'" . $self->loc( $canon->($self->NewValue) ) . "'"
                  );
 -
              }
          }
  
diff --cc share/html/Download/CustomFieldValue/dhandler
index 850d22e,a919049..00b6105
--- a/share/html/Download/CustomFieldValue/dhandler
+++ b/share/html/Download/CustomFieldValue/dhandler
@@@ -61,13 -61,13 +61,13 @@@ unless ($OCFV->id) 
      Abort("Bad OCFV id. Couldn't find OCFV '$id'\n");
  }
  
 -my $content_type = $OCFV->ContentType || 'text/plain';
 +my $content_type = $OCFV->ContentType || 'text/plain; charset=utf-8';
      
  if (RT->Config->Get('AlwaysDownloadAttachments')) {
-     $r->headers_out->{'Content-Disposition'} = "attachment; filename=" . $OCFV->Content;
+     $r->headers_out->{'Content-Disposition'} = "attachment";
  }
  elsif (!RT->Config->Get('TrustHTMLAttachments')) {
 -    $content_type = 'text/plain' if ($content_type =~ /^text\/html/i);
 +    $content_type = 'text/plain; charset=utf-8' if ($content_type =~ /^text\/html/i);
  }
  
  $r->content_type( $content_type );
diff --cc share/html/Elements/ColumnMap
index f9e9b5f,330aced..01ba3ab
--- a/share/html/Elements/ColumnMap
+++ b/share/html/Elements/ColumnMap
@@@ -209,8 -182,10 +209,10 @@@ $m->callback( COLUMN_MAP => $COLUMN_MAP
  $m->callback( COLUMN_MAP => $COLUMN_MAP );
  
  # first deal with class specific things
- my $class_map = $m->comp("/Elements/$Class/ColumnMap", Attr => $Attr, Name => $Name, GenericMap => $COLUMN_MAP );
- return $class_map if defined $class_map;
+ if (RT::Interface::Web->ComponentPathIsSafe($Class) and $m->comp_exists("/Elements/$Class/ColumnMap")) {
 -    my $class_map = $m->comp("/Elements/$Class/ColumnMap", Attr => $Attr, Name => $Name );
++    my $class_map = $m->comp("/Elements/$Class/ColumnMap", Attr => $Attr, Name => $Name, GenericMap => $COLUMN_MAP );
+     return $class_map if defined $class_map;
+ }
  return GetColumnMapEntry( Map => $COLUMN_MAP, Name => $Name, Attribute => $Attr );
  
  </%INIT>
diff --cc share/html/Elements/EditCustomFieldBinary
index e79580b,3222554..475f4a4
--- a/share/html/Elements/EditCustomFieldBinary
+++ b/share/html/Elements/EditCustomFieldBinary
@@@ -47,14 -47,11 +47,14 @@@
  %# END BPS TAGGED BLOCK }}}
  % while ( $Values and my $value = $Values->Next ) {
  %# XXX - let user download the file(s) here?
- <input type="checkbox" name="<%$NamePrefix%><%$CustomField->Id%>-DeleteValueIds" class="checkbox CF-<%$CustomField->id%>-Edit" value="<% $value->Id %>" /><a href="<%RT->Config->Get('WebPath')%>/Download/CustomFieldValue/<% $value->Id %>/<% $value->Content %>"><% $value->Content %></a><br />
+ <input type="checkbox" name="<%$NamePrefix%><%$CustomField->Id%>-DeleteValueIds" class="checkbox CF-<%$CustomField->id%>-Edit" value="<% $value->Id %>" /><a href="<%RT->Config->Get('WebPath')%>/Download/CustomFieldValue/<% $value->Id %>/<% $value->Content |un %>"><% $value->Content %></a><br />
  % }
 -% if (!$MaxValues || !$Values || $Values->Count < $MaxValues) {
 -<input type="file" name="<% $NamePrefix %><% $CustomField->Id %>-Upload" class="CF-<%$CustomField->id%>-Edit" />
 +% if ($MaxValues && $Values && $Values->Count >= $MaxValues ) {
 +<div class="hints">
 +<&|/l&>Reached maximum number, so new values will override old ones.</&>
 +</div>
  % }
 +<input type="file" name="<% $NamePrefix %><% $CustomField->Id %>-Upload" class="CF-<%$CustomField->id%>-Edit" />
  <%ARGS>
  $Object => undef
  $CustomField => undef
diff --cc share/html/Elements/MakeClicky
index e10418f,dabe3bb..a34a896
--- a/share/html/Elements/MakeClicky
+++ b/share/html/Elements/MakeClicky
@@@ -48,6 -48,12 +48,12 @@@
  <%ONCE>
  use Regexp::Common qw(URI);
  
+ my $escaper = sub {
+     my $content = shift;
 -    RT::Interface::Web::EscapeUTF8( \$content );
++    RT::Interface::Web::EscapeHTML( \$content );
+     return $content;
+ };
+ 
  my %actions = (
      default => sub {
          my %args = @_;
diff --cc share/html/Ticket/Attachment/dhandler
index 5907427,f30232e..f55e4be
--- a/share/html/Ticket/Attachment/dhandler
+++ b/share/html/Ticket/Attachment/dhandler
@@@ -46,58 -46,50 +46,58 @@@
  %#
  %# END BPS TAGGED BLOCK }}}
  <%perl>
 -    my ($ticket, $trans,$attach, $filename);
 -    my $arg = $m->dhandler_arg;                # get rest of path
 -    if ($arg =~ m{^(\d+)/(\d+)}) {
 -        $trans = $1;
 -        $attach = $2;
 -    }
 -    else {
 -        Abort("Corrupted attachment URL.");
 -    }
 -     my $AttachmentObj = RT::Attachment->new($session{'CurrentUser'});
 -     $AttachmentObj->Load($attach) || Abort("Attachment '$attach' could not be loaded");
 +my ( $ticket, $trans, $attach, $filename );
 +my $arg = $m->dhandler_arg;    # get rest of path
 +if ( $arg =~ m{^(\d+)/(\d+)} ) {
 +    $trans  = $1;
 +    $attach = $2;
 +}
 +else {
 +    Abort("Corrupted attachment URL.");
 +}
 +my $AttachmentObj = RT::Attachment->new( $session{'CurrentUser'} );
 +$AttachmentObj->Load($attach) || Abort("Attachment '$attach' could not be loaded");
  
 +unless ( $AttachmentObj->id ) {
 +    Abort("Bad attachment id. Couldn't find attachment '$attach'\n");
 +}
 +unless ( $AttachmentObj->TransactionId() == $trans ) {
 +    Abort("Bad transaction number for attachment. $trans should be". $AttachmentObj->TransactionId() . "\n");
 +}
  
 -     unless ($AttachmentObj->id) {
 -        Abort("Bad attachment id. Couldn't find attachment '$attach'\n");
 -    }
 -     unless ($AttachmentObj->TransactionId() == $trans ) {
 -        Abort("Bad transaction number for attachment. $trans should be".$AttachmentObj->TransactionId() ."\n");
 +my $content = $AttachmentObj->OriginalContent;
 +my $content_type = $AttachmentObj->ContentType || 'text/plain';
  
 -     }
 +if ( RT->Config->Get('AlwaysDownloadAttachments') ) {
-     $r->headers_out->{'Content-Disposition'} = "attachment; filename=" . $AttachmentObj->Filename;
++    $r->headers_out->{'Content-Disposition'} = "attachment";
 +}
 +elsif ( !RT->Config->Get('TrustHTMLAttachments') ) {
 +    $content_type = 'text/plain' if ( $content_type =~ /^text\/html/i );
 +}
 +elsif (lc $content_type eq 'text/html') {
 +    # If we're trusting and serving HTML for display not download, try to do
 +    # inline <img> rewriting to be extra helpful.
 +    my $count = RT::Interface::Web::RewriteInlineImages(
 +        Content     => \$content,
 +        Attachment  => $AttachmentObj,
 +    );
 +    RT->Logger->debug("Rewrote $count CID images when displaying original HTML attachment #$attach");
 +}
  
 -     my $content_type = $AttachmentObj->ContentType || 'text/plain';
 +my $enc  = $AttachmentObj->OriginalEncoding || 'utf-8';
 +my $iana = Encode::find_encoding($enc);
 +   $iana = $iana ? $iana->mime_name : $enc;
  
 -     if (RT->Config->Get('AlwaysDownloadAttachments')) {
 -         $r->headers_out->{'Content-Disposition'} = "attachment";
 -     }
 -     elsif (!RT->Config->Get('TrustHTMLAttachments')) {
 -         $content_type = 'text/plain' if ($content_type =~ /^text\/html/i);
 -     }
 +require MIME::Types;
 +my $mimetype = MIME::Types->new->type($content_type);
 +unless ( $mimetype && $mimetype->isBinary ) {
 +    $content_type .= ";charset=$iana";
 +}
  
 -     my $enc = $AttachmentObj->OriginalEncoding || 'utf-8';
 -     my $iana = Encode::find_encoding( $enc );
 -     $iana = $iana? $iana->mime_name : $enc;
 -
 -     require MIME::Types;
 -     my $mimetype = MIME::Types->new->type($content_type);
 -     unless ( $mimetype && $mimetype->isBinary ) {
 -	    $content_type .= ";charset=$iana";
 -     }
 -
 -     $r->content_type( $content_type );
 -     $m->clear_buffer();
 -     $m->out($AttachmentObj->OriginalContent);
 -     $m->abort; 
 +$r->content_type($content_type);
 +$m->clear_buffer();
 +$m->out($content);
 +$m->abort;
  </%perl>
  <%attr>
  AutoFlush => 0
diff --cc share/html/Ticket/Elements/AddAttachments
index d86ef5b,cdf6281..a237f45
--- a/share/html/Ticket/Elements/AddAttachments
+++ b/share/html/Ticket/Elements/AddAttachments
@@@ -49,10 -49,8 +49,10 @@@
  <tr><td class="label"><&|/l&>Attached file</&>:</td>
  <td>
  <&|/l&>Check box to delete</&><br />
- % foreach my $attach_name ( keys %$attachments ) {
 -% foreach my $attach_name (sort keys %{$session{'Attachments'}}) {
 -<input type="checkbox" class="checkbox" name="DeleteAttach-<%$attach_name%>" value="1" /><%$attach_name%><br />
++% foreach my $attach_name ( sort keys %$attachments ) {
 +<input type="checkbox" class="checkbox" name="DeleteAttach" value="<% $attach_name %>" id="DeleteAttach-<%$attach_name%>" />
 +<label for="DeleteAttach-<%$attach_name%>"><% $attach_name %></label>
 +<br />
  % } # end of foreach
  </td>
  </tr>
diff --cc share/html/Ticket/Elements/Reminders
index 7f5a108,8ed870e..967dcff
--- a/share/html/Ticket/Elements/Reminders
+++ b/share/html/Ticket/Elements/Reminders
@@@ -115,15 -112,11 +115,15 @@@ my $reminder_collection = $count_remind
  % }
  % }
  
- % if ($Ticket->Status ne "deleted" and $Ticket->QueueObj->CurrentUserHasRight('CreateTicket') and $Ticket->CurrentUserHasRight('ModifyTicket') ) {
 -% if (lc $Ticket->Status ne "deleted") {
++% if (lc $Ticket->Status ne "deleted" and $Ticket->QueueObj->CurrentUserHasRight('CreateTicket') and $Ticket->CurrentUserHasRight('ModifyTicket') ) {
  <&|/l&>New reminder:</&>
  <& SELF:NewReminder, Ticket => $Ticket &>
 +% $editable = 1;
 +% }
 +
 +% if ( $editable && $ShowSave ) {
 +<div align="right"><input type="submit" class="button" value="<&|/l&>Save</&>" /></div>
  % }
 -% return(lc $Ticket->Status ne "deleted" or $visible);
  <%method NewReminder>
  <%args>
  $Ticket
diff --cc share/html/Ticket/Elements/ShowAttachments
index 61ca902,c487fee..ab3fcea
--- a/share/html/Ticket/Elements/ShowAttachments
+++ b/share/html/Ticket/Elements/ShowAttachments
@@@ -56,10 -56,34 +56,10 @@@
  <%$key%><br />
  <ul>
  % foreach my $rev (@{$documents{$key}}) {
 -
 -<%PERL>
 -my $size = $rev->ContentLength;
 -
 -if ($size) {
 -    my $kb = int($size/102.4) / 10;
 -    my $units = RT->Config->Get('AttachmentUnits');
 -
 -    if (!defined($units)) {
 -        if ($size > 1024) {
 -            $size = $kb . "k";
 -        }
 -        else {
 -            $size = $size . "b";
 -        }
 -    }
 -    elsif ($units eq 'k') {
 -        $size = $kb . "k";
 -    }
 -    else {
 -        $size = $size . "b";
 -    }
 -
 -</%PERL>
 -
 +% if ($rev->ContentLength) {
  <li><font size="-2">
- <a href="<%RT->Config->Get('WebPath')%>/Ticket/Attachment/<%$rev->TransactionId%>/<%$rev->Id%>/<%$rev->Filename | u%>">
+ <a href="<%RT->Config->Get('WebPath')%>/Ticket/Attachment/<%$rev->TransactionId%>/<%$rev->Id%>/<%$rev->Filename | un %>">
 -% my $desc = loc("[_1] ([_2]) by [_3]", $rev->CreatedAsString, $size, $m->scomp('/Elements/ShowUser', User => $rev->CreatorObj));
 +% my $desc = loc("[_1] ([_2]) by [_3]", $rev->CreatedAsString, $rev->FriendlyContentLength, $m->scomp('/Elements/ShowUser', User => $rev->CreatorObj));
  <% $desc |n%>
  </a>
  </font></li>
diff --cc share/html/m/ticket/show
index 480664e,ead23a7..68ee823
--- a/share/html/m/ticket/show
+++ b/share/html/m/ticket/show
@@@ -313,10 -318,34 +313,10 @@@ my $print_value = sub 
  <%$key%><br />
  <ul>
  % foreach my $rev (@{$documents{$key}}) {
 -
 -<%PERL>
 -my $size = $rev->ContentLength;
 -
 -if ($size) {
 -    my $kb = int($size/102.4) / 10;
 -    my $units = RT->Config->Get('AttachmentUnits');
 -
 -    if (!defined($units)) {
 -        if ($size > 1024) {
 -            $size = $kb . "k";
 -        }
 -        else {
 -            $size = $size . "b";
 -        }
 -    }
 -    elsif ($units eq 'k') {
 -        $size = $kb . "k";
 -    }
 -    else {
 -        $size = $size . "b";
 -    }
 -
 -</%PERL>
 -
 +% if ($rev->ContentLength) {
  <li><font size="-2">
- <a href="<%RT->Config->Get('WebPath')%>/Ticket/Attachment/<%$rev->TransactionId%>/<%$rev->Id%>/<%$rev->Filename | u%>">
 -<a href="<%RT->Config->Get('WebPath')%>/Ticket/Attachment/<%$rev->TransactionId%>/<%$rev->Id%>/<%$rev->Filename | un%>">
 -<&|/l, $rev->CreatedAsString, $size, $rev->CreatorObj->Name &>[_1] ([_2]) by [_3]</&>
++<a href="<%RT->Config->Get('WebPath')%>/Ticket/Attachment/<%$rev->TransactionId%>/<%$rev->Id%>/<%$rev->Filename | un %>">
 +<&|/l, $rev->CreatedAsString, $rev->FriendlyContentLength, $rev->CreatorObj->Name &>[_1] ([_2]) by [_3]</&>
  </a>
  </font></li>
  % }
diff --cc share/static/js/userautocomplete.js
index de7d6a0,0000000..a6ae742
mode 100644,000000..100644
--- a/share/static/js/userautocomplete.js
+++ b/share/static/js/userautocomplete.js
@@@ -1,63 -1,0 +1,79 @@@
 +jQuery(function() {
 +    // inputs that accept multiple email addresses
 +    var multipleCompletion = new Array("Requestors", "To", "Bcc", "Cc", "AdminCc", "WatcherAddressEmail[123]", "UpdateCc", "UpdateBcc");
 +
 +    // inputs with only a single email address allowed
 +    var singleCompletion   = new Array("(Add|Delete)Requestor", "(Add|Delete)Cc", "(Add|Delete)AdminCc");
 +
 +    // inputs for only privileged users
 +    var privilegedCompletion = new Array("AddPrincipalForRights-user");
 +
 +    // build up the regexps we'll use to match
 +    var applyto  = new RegExp('^(' + multipleCompletion.concat(singleCompletion, privilegedCompletion).join('|') + ')$');
 +    var acceptsMultiple = new RegExp('^(' + multipleCompletion.join('|') + ')$');
 +    var onlyPrivileged = new RegExp('^(' + privilegedCompletion.join('|') + ')$');
 +
 +    var inputs = document.getElementsByTagName("input");
 +
 +    for (var i = 0; i < inputs.length; i++) {
 +        var input = inputs[i];
 +        var inputName = input.getAttribute("name");
 +
 +        if (!inputName || !inputName.match(applyto))
 +            continue;
 +
 +        var options = {
 +            source: RT.Config.WebHomePath + "/Helpers/Autocomplete/Users"
 +        };
 +
 +        var queryargs = [];
 +
 +        if (inputName.match("AddPrincipalForRights-user")) {
 +            queryargs.push("return=Name");
 +            options.select = addprincipal_onselect;
 +            options.change = addprincipal_onchange;
 +        }
 +
 +        if (inputName.match(onlyPrivileged)) {
 +            queryargs.push("privileged=1");
 +        }
 +
 +        if (inputName.match(acceptsMultiple)) {
 +            queryargs.push("delim=,");
 +
 +            options.focus = function () {
 +                // prevent value inserted on focus
 +                return false;
 +            }
 +
 +            options.select = function(event, ui) {
 +                var terms = this.value.split(/,\s*/);
 +                terms.pop();                    // remove current input
 +                terms.push( ui.item.value );    // add selected item
 +                this.value = terms.join(", ");
 +                return false;
 +            }
 +        }
 +
 +        if (queryargs.length)
 +            options.source += "?" + queryargs.join("&");
 +
-         jQuery(input).autocomplete(options);
++        jQuery(input)
++            .addClass('autocompletes-user')
++            .autocomplete(options)
++            .data("ui-autocomplete")
++            ._renderItem = function(ul, item) {
++                var rendered = jQuery("<a/>");
++
++                if (item.html == null)
++                    rendered.text( item.label );
++                else
++                    rendered.html( item.html );
++
++                return jQuery("<li/>")
++                    .data( "item.autocomplete", item )
++                    .append( rendered )
++                    .appendTo( ul );
++            };
 +    }
 +});

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


More information about the Rt-commit mailing list