diff options
| author | ivan <ivan> | 2010-07-20 00:59:02 +0000 | 
|---|---|---|
| committer | ivan <ivan> | 2010-07-20 00:59:02 +0000 | 
| commit | 25591d52b441b5bfa34f0ffef1095098e1d2f1b2 (patch) | |
| tree | 3f2881729adba24830ac73690523a1c5e64987b7 | |
| parent | 1a62d4a284a58919716f076baa594f02dda2472d (diff) | |
RT custom fields patch, RT#8449
| -rw-r--r-- | rt/FREESIDE_MODIFIED | 10 | ||||
| -rw-r--r-- | rt/lib/RT/CustomField_Overlay.pm | 16 | ||||
| -rw-r--r-- | rt/lib/RT/Interface/Web.pm | 2329 | ||||
| -rwxr-xr-x | rt/lib/RT/Record.pm | 31 | ||||
| -rw-r--r-- | rt/lib/RT/Tickets_Overlay.pm | 102 | ||||
| -rw-r--r-- | rt/share/html/Elements/EditCustomFieldDate | 62 | ||||
| -rw-r--r-- | rt/share/html/Elements/ShowCustomFieldDate | 57 | ||||
| -rw-r--r-- | rt/share/html/Search/Build.html | 2 | ||||
| -rw-r--r-- | rt/share/html/Search/Elements/PickCFs | 51 | 
9 files changed, 1841 insertions, 819 deletions
diff --git a/rt/FREESIDE_MODIFIED b/rt/FREESIDE_MODIFIED index d1f214681..0e34a979e 100644 --- a/rt/FREESIDE_MODIFIED +++ b/rt/FREESIDE_MODIFIED @@ -5,10 +5,13 @@ config.layout.in   etc/RT_Config.pm   etc/RT_SiteConfig.pm   lib/RT/Config.pm +lib/RT/CustomField_Overlay.pm #customfield date patch +lib/RT/Interface/Web.pm #customfield date patch  lib/RT/Interface/Web_Vendor.pm - lib/RT/Record.pm + lib/RT/Record.pm #and customfield date patch  lib/RT/SearchBuilder.pm #need DBIx::SearchBuilder >= 1.36 for Pg 8.1+  lib/RT/Transaction_Overlay.pm +lib/RT/Tickets_Overlay.pm #customfield date patch   lib/RT/Ticket_Overlay.pm   lib/RT/Users_Overlay.pm   lib/RT/Groups_Overlay.pm @@ -18,13 +21,16 @@ lib/RT/URI/freeside/XMLRPC.pm   share/html/Admin/Users/Modify.html   share/html/Elements/ColumnMap   share/html/Elements/CollectionList +share/html/Elements/EditCustomFieldDate #customfield date patch (NEW)   share/html/Elements/Header   share/html/Elements/PageLayout   #html/Elements/QuickCreate + share/html/Elements/ShowCustomFieldDate #customfield date patch (NEW)   share/html/Elements/SelectDate   share/html/Elements/Footer   html/Ticket/Create.html #XXX TODO - share/html/Search/Build.html + share/html/Search/Build.html #and customfield date patch + share/html/Search/Elements/PickCFs #customfield date patch   share/html/Ticket/Display.html  share/html/Ticket/Elements/AddCustomers  share/html/Ticket/Elements/EditCustomers diff --git a/rt/lib/RT/CustomField_Overlay.pm b/rt/lib/RT/CustomField_Overlay.pm index 355dd203c..c91f12037 100644 --- a/rt/lib/RT/CustomField_Overlay.pm +++ b/rt/lib/RT/CustomField_Overlay.pm @@ -97,6 +97,11 @@ our %FieldTypes = (          'Enter one value with autocompletion',            # loc          'Enter up to [_1] values with autocompletion',    # loc      ], +    Date => [ +        'Select multiple dates',	# loc +        'Select date',			# loc +        'Select up to [_1] dates',	# loc +    ],  ); @@ -829,7 +834,7 @@ Returns an array of all possible composite values for custom fields.  sub TypeComposites {      my $self = shift; -    return grep !/(?:[Tt]ext|Combobox)-0/, map { ("$_-1", "$_-0") } $self->Types; +    return grep !/(?:[Tt]ext|Combobox|Date)-0/, map { ("$_-1", "$_-0") } $self->Types;  }  =head2 SetLookupType @@ -1160,6 +1165,15 @@ sub AddValueForObject {              $extra_values--;          }      } +    # For date, we need to store Content as ISO date +    if ($self->Type eq 'Date') { +        my $DateObj = new RT::Date( $self->CurrentUser ); +        $DateObj->Set( +            Format => 'unknown', +            Value  => $args{'Content'}, +        ); +        $args{'Content'} = $DateObj->ISO; +    }      my $newval = RT::ObjectCustomFieldValue->new( $self->CurrentUser );      my $val    = $newval->Create(          ObjectType   => ref($obj), diff --git a/rt/lib/RT/Interface/Web.pm b/rt/lib/RT/Interface/Web.pm index 5097f54a4..edb719df5 100644 --- a/rt/lib/RT/Interface/Web.pm +++ b/rt/lib/RT/Interface/Web.pm @@ -1,147 +1,791 @@ -# BEGIN LICENSE BLOCK -#  -# Copyright (c) 1996-2003 Jesse Vincent <jesse@bestpractical.com> -#  -# (Except where explictly superceded by other copyright notices) -#  +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC +#                                          <jesse@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. -#  -# Unless otherwise specified, all modifications, corrections or -# extensions to this work which alter its source code become the -# property of Best Practical Solutions, LLC when submitted for -# inclusion in the work. -#  -#  -# END LICENSE BLOCK +# +# 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 }}} +  ## Portions Copyright 2000 Tobias Brox <tobix@fsck.com>  ## This is a library of static subs to be used by the Mason web  ## interface to RT -  =head1 NAME  RT::Interface::Web -=begin testing -use_ok(RT::Interface::Web); +=cut + +use strict; +use warnings; -=end testing +package RT::Interface::Web; + +use RT::SavedSearches; +use URI qw(); +use RT::Interface::Web::Session; +use Digest::MD5 (); +use Encode qw(); + +# {{{ EscapeUTF8 + +=head2 EscapeUTF8 SCALARREF + +does a css-busting but minimalist escaping of whatever html you're passing in.  =cut +sub EscapeUTF8 { +    my $ref = shift; +    return unless defined $$ref; + +    $$ref =~ s/&/&/g; +    $$ref =~ s/</</g; +    $$ref =~ s/>/>/g; +    $$ref =~ s/\(/(/g; +    $$ref =~ s/\)/)/g; +    $$ref =~ s/"/"/g; +    $$ref =~ s/'/'/g; +} -package RT::Interface::Web; -use strict; +# }}} +# {{{ EscapeURI +=head2 EscapeURI SCALARREF +Escapes URI component according to RFC2396 +=cut -# {{{ sub NewApacheHandler  +sub EscapeURI { +    my $ref = shift; +    return unless defined $$ref; -=head2 NewApacheHandler +    use bytes; +    $$ref =~ s/([^a-zA-Z0-9_.!~*'()-])/uc sprintf("%%%02X", ord($1))/eg; +} + +# }}} -  Takes extra options to pass to HTML::Mason::ApacheHandler->new -  Returns a new Mason::ApacheHandler object +# {{{ WebCanonicalizeInfo + +=head2 WebCanonicalizeInfo(); + +Different web servers set different environmental varibles. This +function must return something suitable for REMOTE_USER. By default, +just downcase $ENV{'REMOTE_USER'}  =cut -sub NewApacheHandler { -    require HTML::Mason::ApacheHandler; -    my $ah = new HTML::Mason::ApacheHandler(  -     -        comp_root                    => [ -            [ local    => $RT::MasonLocalComponentRoot ], -            [ standard => $RT::MasonComponentRoot ] -        ], -        args_method => "CGI", -        default_escape_flags => 'h', -        allow_globals        => [qw(%session)], -        data_dir => "$RT::MasonDataDir", -        @_ -    ); +sub WebCanonicalizeInfo { +    return $ENV{'REMOTE_USER'} ? lc $ENV{'REMOTE_USER'} : $ENV{'REMOTE_USER'}; +} + +# }}} + +# {{{ WebExternalAutoInfo + +=head2 WebExternalAutoInfo($user); + +Returns a hash of user attributes, used when WebExternalAuto is set. + +=cut + +sub WebExternalAutoInfo { +    my $user = shift; + +    my %user_info; + +    # default to making Privileged users, even if they specify +    # some other default Attributes +    if ( !$RT::AutoCreate +        || ( ref($RT::AutoCreate) && not exists $RT::AutoCreate->{Privileged} ) ) +    { +        $user_info{'Privileged'} = 1; +    } + +    if ( $^O !~ /^(?:riscos|MacOS|MSWin32|dos|os2)$/ ) { + +        # Populate fields with information from Unix /etc/passwd + +        my ( $comments, $realname ) = ( getpwnam($user) )[ 5, 6 ]; +        $user_info{'Comments'} = $comments if defined $comments; +        $user_info{'RealName'} = $realname if defined $realname; +    } elsif ( $^O eq 'MSWin32' and eval 'use Net::AdminMisc; 1' ) { -    $ah->interp->set_escape( h => \&RT::Interface::Web::EscapeUTF8 ); -     -    return ($ah); +        # Populate fields with information from NT domain controller +    } + +    # and return the wad of stuff +    return {%user_info};  }  # }}} -# {{{ sub NewCGIHandler  +sub HandleRequest { +    my $ARGS = shift; + +    $HTML::Mason::Commands::r->content_type("text/html; charset=utf-8"); + +    $HTML::Mason::Commands::m->{'rt_base_time'} = [ Time::HiRes::gettimeofday() ]; + +    # Roll back any dangling transactions from a previous failed connection +    $RT::Handle->ForceRollback() if $RT::Handle->TransactionDepth; + +    MaybeEnableSQLStatementLog(); + +    # avoid reentrancy, as suggested by masonbook +    local *HTML::Mason::Commands::session unless $HTML::Mason::Commands::m->is_subrequest; + +    $HTML::Mason::Commands::m->autoflush( $HTML::Mason::Commands::m->request_comp->attr('AutoFlush') ) +        if ( $HTML::Mason::Commands::m->request_comp->attr_exists('AutoFlush') ); + +    DecodeARGS($ARGS); +    PreprocessTimeUpdates($ARGS); + +    MaybeShowInstallModePage(); + +    $HTML::Mason::Commands::m->comp( '/Elements/SetupSessionCookie', %$ARGS ); +    SendSessionCookie(); +    $HTML::Mason::Commands::session{'CurrentUser'} = RT::CurrentUser->new() unless _UserLoggedIn(); + +    MaybeShowNoAuthPage($ARGS); + +    AttemptExternalAuth($ARGS) if RT->Config->Get('WebExternalAuthContinuous') or not _UserLoggedIn(); + +    _ForceLogout() unless _UserLoggedIn(); + +    # Process per-page authentication callbacks +    $HTML::Mason::Commands::m->callback( %$ARGS, CallbackName => 'Auth', CallbackPage => '/autohandler' ); + +    unless ( _UserLoggedIn() ) { +        _ForceLogout(); + +        # If the user is logging in, let's authenticate +        if ( defined $ARGS->{user} && defined $ARGS->{pass} ) { +            AttemptPasswordAuthentication($ARGS); +        } else { +            # if no credentials then show him login page +            $HTML::Mason::Commands::m->comp( '/Elements/Login', %$ARGS ); +            $HTML::Mason::Commands::m->abort; +        } +    } + +    # now it applies not only to home page, but any dashboard that can be used as a workspace +    $HTML::Mason::Commands::session{'home_refresh_interval'} = $ARGS->{'HomeRefreshInterval'} +        if ( $ARGS->{'HomeRefreshInterval'} ); + +    # Process per-page global callbacks +    $HTML::Mason::Commands::m->callback( %$ARGS, CallbackName => 'Default', CallbackPage => '/autohandler' ); + +    ShowRequestedPage($ARGS); +    LogRecordedSQLStatements(); +} + +sub _ForceLogout { + +    delete $HTML::Mason::Commands::session{'CurrentUser'}; +} + +sub _UserLoggedIn { +    if ( $HTML::Mason::Commands::session{CurrentUser} && $HTML::Mason::Commands::session{'CurrentUser'}->id ) { +        return 1; +    } else { +        return undef; +    } + +} + +=head2 MaybeShowInstallModePage  + +This function, called exclusively by RT's autohandler, dispatches +a request to RT's Installation workflow, only if Install Mode is enabled in the configuration file. + +If it serves a page, it stops mason processing. Otherwise, mason just keeps running through the autohandler + +=cut  + +sub MaybeShowInstallModePage { +    return unless RT->InstallMode; + +    my $m = $HTML::Mason::Commands::m; +    if ( $m->base_comp->path =~ RT->Config->Get('WebNoAuthRegex') ) { +        $m->call_next(); +    } elsif ( $m->request_comp->path !~ '^(/+)Install/' ) { +        RT::Interface::Web::Redirect( RT->Config->Get('WebURL') . "Install/index.html" ); +    } else { +        $m->call_next(); +    } +    $m->abort(); +} + +=head2 MaybeShowNoAuthPage  \%ARGS -=head2 NewCGIHandler +This function, called exclusively by RT's autohandler, dispatches +a request to the page a user requested (but only if it matches the "noauth" regex. -  Returns a new Mason::CGIHandler object +If it serves a page, it stops mason processing. Otherwise, mason just keeps running through the autohandler + +=cut  + +sub MaybeShowNoAuthPage { +    my $ARGS = shift; + +    my $m = $HTML::Mason::Commands::m; + +    return unless $m->base_comp->path =~ RT->Config->Get('WebNoAuthRegex'); + +    # If it's a noauth file, don't ask for auth. +    SendSessionCookie(); +    $m->comp( { base_comp => $m->request_comp }, $m->fetch_next, %$ARGS ); +    $m->abort; +} + +=head2 ShowRequestedPage  \%ARGS + +This function, called exclusively by RT's autohandler, dispatches +a request to the page a user requested (making sure that unpriviled users +can only see self-service pages. + +=cut  + +sub ShowRequestedPage { +    my $ARGS = shift; + +    my $m = $HTML::Mason::Commands::m; + +    SendSessionCookie(); + +    # If the user isn't privileged, they can only see SelfService +    unless ( $HTML::Mason::Commands::session{'CurrentUser'}->Privileged ) { + +        # if the user is trying to access a ticket, redirect them +        if ( $m->request_comp->path =~ '^(/+)Ticket/Display.html' && $ARGS->{'id'} ) { +            RT::Interface::Web::Redirect( RT->Config->Get('WebURL') . "SelfService/Display.html?id=" . $ARGS->{'id'} ); +        } + +        # otherwise, drop the user at the SelfService default page +        elsif ( $m->base_comp->path !~ RT->Config->Get('SelfServiceRegex') ) { +            RT::Interface::Web::Redirect( RT->Config->Get('WebURL') . "SelfService/" ); +        } + +        # if user is in SelfService dir let him do anything +        else { +            $m->comp( { base_comp => $m->request_comp }, $m->fetch_next, %$ARGS ); +        } +    } else { +        $m->comp( { base_comp => $m->request_comp }, $m->fetch_next, %$ARGS ); +    } + +} + +sub AttemptExternalAuth { +    my $ARGS = shift; + +    return unless ( RT->Config->Get('WebExternalAuth') ); + +    my $user = $ARGS->{user}; +    my $m    = $HTML::Mason::Commands::m; + +    # If RT is configured for external auth, let's go through and get REMOTE_USER + +    # do we actually have a REMOTE_USER equivlent? +    if ( RT::Interface::Web::WebCanonicalizeInfo() ) { +        my $orig_user = $user; + +        $user = RT::Interface::Web::WebCanonicalizeInfo(); +        my $load_method = RT->Config->Get('WebExternalGecos') ? 'LoadByGecos' : 'Load'; + +        if ( $^O eq 'MSWin32' and RT->Config->Get('WebExternalGecos') ) { +            my $NodeName = Win32::NodeName(); +            $user =~ s/^\Q$NodeName\E\\//i; +        } + +        InstantiateNewSession() unless _UserLoggedIn; +        $HTML::Mason::Commands::session{'CurrentUser'} = RT::CurrentUser->new(); +        $HTML::Mason::Commands::session{'CurrentUser'}->$load_method($user); + +        if ( RT->Config->Get('WebExternalAuto') and not _UserLoggedIn() ) { + +            # Create users on-the-fly +            my $UserObj = RT::User->new($RT::SystemUser); +            my ( $val, $msg ) = $UserObj->Create( +                %{ ref RT->Config->Get('AutoCreate') ? RT->Config->Get('AutoCreate') : {} }, +                Name  => $user, +                Gecos => $user, +            ); + +            if ($val) { + +                # now get user specific information, to better create our user. +                my $new_user_info = RT::Interface::Web::WebExternalAutoInfo($user); + +                # set the attributes that have been defined. +                foreach my $attribute ( $UserObj->WritableAttributes ) { +                    $m->callback( +                        Attribute    => $attribute, +                        User         => $user, +                        UserInfo     => $new_user_info, +                        CallbackName => 'NewUser', +                        CallbackPage => '/autohandler' +                    ); +                    my $method = "Set$attribute"; +                    $UserObj->$method( $new_user_info->{$attribute} ) if defined $new_user_info->{$attribute}; +                } +                $HTML::Mason::Commands::session{'CurrentUser'}->Load($user); +            } else { + +                # we failed to successfully create the user. abort abort abort. +                delete $HTML::Mason::Commands::session{'CurrentUser'}; +                $m->comp( '/Elements/Login', %$ARGS, Error => HTML::Mason::Commands::loc( 'Cannot create user: [_1]', $msg ) ) +                    if RT->Config->Get('WebFallbackToInternalAuth');; +                $m->abort(); +            } +        } + +        if ( _UserLoggedIn() ) { +            $m->callback( %$ARGS, CallbackName => 'ExternalAuthSuccessfulLogin', CallbackPage => '/autohandler' ); +        } else { +            delete $HTML::Mason::Commands::session{'CurrentUser'}; +            $user = $orig_user; + +            if ( RT->Config->Get('WebExternalOnly') ) { +                $m->comp( '/Elements/Login', %$ARGS, Error => HTML::Mason::Commands::loc('You are not an authorized user') ); +                $m->abort(); +            } +        } +    } elsif ( RT->Config->Get('WebFallbackToInternalAuth') ) { +        unless ( defined $HTML::Mason::Commands::session{'CurrentUser'} ) { +            # XXX unreachable due to prior defaulting in HandleRequest (check c34d108) +            $m->comp( '/Elements/Login', %$ARGS, Error => HTML::Mason::Commands::loc('You are not an authorized user') ); +            $m->abort(); +        } +    } else { + +        # WebExternalAuth is set, but we don't have a REMOTE_USER. abort +        # XXX: we must return AUTH_REQUIRED status or we fallback to +        # internal auth here too. +        delete $HTML::Mason::Commands::session{'CurrentUser'} +            if defined $HTML::Mason::Commands::session{'CurrentUser'}; +    } +} + +sub AttemptPasswordAuthentication { +    my $ARGS     = shift; +    my $user_obj = RT::CurrentUser->new(); +    $user_obj->Load( $ARGS->{user} ); + +    my $m = $HTML::Mason::Commands::m; + +    unless ( $user_obj->id && $user_obj->IsPassword( $ARGS->{pass} ) ) { +        $RT::Logger->error("FAILED LOGIN for @{[$ARGS->{user}]} from $ENV{'REMOTE_ADDR'}"); +        $m->comp( '/Elements/Login', %$ARGS, Error => HTML::Mason::Commands::loc('Your username or password is incorrect'), ); +        $m->callback( %$ARGS, CallbackName => 'FailedLogin', CallbackPage => '/autohandler' ); +        $m->abort; +    } + +    $RT::Logger->info("Successful login for @{[$ARGS->{user}]} from $ENV{'REMOTE_ADDR'}"); +    InstantiateNewSession(); +    $HTML::Mason::Commands::session{'CurrentUser'} = $user_obj; +    $m->callback( %$ARGS, CallbackName => 'SuccessfulLogin', CallbackPage => '/autohandler' ); +} + +=head2 LoadSessionFromCookie + +Load or setup a session cookie for the current user.  =cut -sub NewCGIHandler { -    my %args = ( -        @_ -    ); +sub _SessionCookieName { +    my $cookiename = "RT_SID_" . RT->Config->Get('rtname'); +    $cookiename .= "." . $ENV{'SERVER_PORT'} if $ENV{'SERVER_PORT'}; +    return $cookiename; +} + +sub LoadSessionFromCookie { + +    my %cookies       = CGI::Cookie->fetch; +    my $cookiename    = _SessionCookieName(); +    my $SessionCookie = ( $cookies{$cookiename} ? $cookies{$cookiename}->value : undef ); +    tie %HTML::Mason::Commands::session, 'RT::Interface::Web::Session', $SessionCookie; +    unless ( $SessionCookie && $HTML::Mason::Commands::session{'_session_id'} eq $SessionCookie ) { +        undef $cookies{$cookiename}; +    } +    if ( int RT->Config->Get('AutoLogoff') ) { +        my $now = int( time / 60 ); +        my $last_update = $HTML::Mason::Commands::session{'_session_last_update'} || 0; + +        if ( $last_update && ( $now - $last_update - RT->Config->Get('AutoLogoff') ) > 0 ) { +            InstantiateNewSession(); +        } + +        # save session on each request when AutoLogoff is turned on +        $HTML::Mason::Commands::session{'_session_last_update'} = $now if $now != $last_update; +    } +} + +sub InstantiateNewSession { +    tied(%HTML::Mason::Commands::session)->delete if tied(%HTML::Mason::Commands::session); +    tie %HTML::Mason::Commands::session, 'RT::Interface::Web::Session', undef; +} -    my $handler = HTML::Mason::CGIHandler->new( -        comp_root                    => [ -            [ local    => $RT::MasonLocalComponentRoot ], -            [ standard => $RT::MasonComponentRoot ] -        ], -        data_dir => "$RT::MasonDataDir", -        default_escape_flags => 'h', -        allow_globals        => [qw(%session)] +sub SendSessionCookie { +    my $cookie = CGI::Cookie->new( +        -name   => _SessionCookieName(), +        -value  => $HTML::Mason::Commands::session{_session_id}, +        -path   => RT->Config->Get('WebPath'), +        -secure => ( RT->Config->Get('WebSecureCookies') ? 1 : 0 )      ); -   -    $handler->interp->set_escape( h => \&RT::Interface::Web::EscapeUTF8 ); +    $HTML::Mason::Commands::r->err_headers_out->{'Set-Cookie'} = $cookie->as_string; +} + +=head2 Redirect URL +This routine ells the current user's browser to redirect to URL.   +Additionally, it unties the user's currently active session, helping to avoid  +A bug in Apache::Session 1.81 and earlier which clobbers sessions if we try to use  +a cached DBI statement handle twice at the same time. -    return ($handler); +=cut + +sub Redirect { +    my $redir_to = shift; +    untie $HTML::Mason::Commands::session; +    my $uri        = URI->new($redir_to); +    my $server_uri = URI->new( RT->Config->Get('WebURL') ); + +    # If the user is coming in via a non-canonical +    # hostname, don't redirect them to the canonical host, +    # it will just upset them (and invalidate their credentials) +    # don't do this if $RT::CanoniaclRedirectURLs is true +    if (   !RT->Config->Get('CanonicalizeRedirectURLs') +        && $uri->host eq $server_uri->host +        && $uri->port eq $server_uri->port ) +    { +        if ( defined $ENV{HTTPS} and $ENV{'HTTPS'} eq 'on' ) { +            $uri->scheme('https'); +        } else { +            $uri->scheme('http'); +        } +        # [rt3.fsck.com #12716] Apache recommends use of $SERVER_HOST +        $uri->host( $ENV{'SERVER_HOST'} || $ENV{'HTTP_HOST'} ); +        $uri->port( $ENV{'SERVER_PORT'} ); +    } + +    # not sure why, but on some systems without this call mason doesn't +    # set status to 302, but 200 instead and people see blank pages +    $HTML::Mason::Commands::r->status(302); + +    # Perlbal expects a status message, but Mason's default redirect status +    # doesn't provide one. See also rt.cpan.org #36689. +    $HTML::Mason::Commands::m->redirect( $uri->canonical, "302 Found" ); + +    $HTML::Mason::Commands::m->abort;  } -# }}} +=head2 StaticFileHeaders  -# {{{ EscapeUTF8 +Send the browser a few headers to try to get it to (somewhat agressively) +cache RT's static Javascript and CSS files. -=head2 EscapeUTF8 SCALARREF +This routine could really use _accurate_ heuristics. (XXX TODO) -does a css-busting but minimalist escaping of whatever html you're passing in. +=cut + +sub StaticFileHeaders { +    my $date = RT::Date->new($RT::SystemUser); + +    # make cache public +    $HTML::Mason::Commands::r->headers_out->{'Cache-Control'} = 'max-age=259200, public'; + +    # Expire things in a month. +    $date->Set( Value => time + 30 * 24 * 60 * 60 ); +    $HTML::Mason::Commands::r->headers_out->{'Expires'} = $date->RFC2616; + +    # if we set 'Last-Modified' then browser request a comp using 'If-Modified-Since' +    # request, but we don't handle it and generate full reply again +    # Last modified at server start time +    # $date->Set( Value => $^T ); +    # $HTML::Mason::Commands::r->headers_out->{'Last-Modified'} = $date->RFC2616; +} + +=head2 PathIsSafe + +Takes a C<< Path => path >> and returns a boolean indicating that +the path is safely within RT's control or not. The path I<must> be +relative. + +This function does not consult the filesystem at all; it is merely +a logical sanity checking of the path. This explicitly does not handle +symlinks; if you have symlinks in RT's webroot pointing outside of it, +then we assume you know what you are doing.  =cut -sub EscapeUTF8  { -        my  $ref = shift; -        my $val = $$ref; -        use bytes; -        $val =~ s/&/&/g; -        $val =~ s/</</g;  -        $val =~ s/>/>/g; -        $val =~ s/\(/(/g; -        $val =~ s/\)/)/g; -        $val =~ s/"/"/g; -        $val =~ s/'/'/g; -        $$ref = $val; -        Encode::_utf8_on($$ref); +sub PathIsSafe { +    my $self = shift; +    my %args = @_; +    my $path = $args{Path}; + +    # Get File::Spec to clean up extra /s, ./, etc +    my $cleaned_up = File::Spec->canonpath($path); + +    if (!defined($cleaned_up)) { +        $RT::Logger->info("Rejecting path that canonpath doesn't understand: $path"); +        return 0; +    } + +    # Forbid too many ..s. We can't just sum then check because +    # "../foo/bar/baz" should be illegal even though it has more +    # downdirs than updirs. So as soon as we get a negative score +    # (which means "breaking out" of the top level) we reject the path. + +    my @components = split '/', $cleaned_up; +    my $score = 0; +    for my $component (@components) { +        if ($component eq '..') { +            $score--; +            if ($score < 0) { +                $RT::Logger->info("Rejecting unsafe path: $path"); +                return 0; +            } +        } +        elsif ($component eq '.' || $component eq '') { +            # these two have no effect on $score +        } +        else { +            $score++; +        } +    } +    return 1;  } -# }}} +=head2 SendStaticFile  + +Takes a File => path and a Type => Content-type + +If Type isn't provided and File is an image, it will +figure out a sane Content-type, otherwise it will +send application/octet-stream + +Will set caching headers using StaticFileHeaders + +=cut + +sub SendStaticFile { +    my $self = shift; +    my %args = @_; +    my $file = $args{File}; +    my $type = $args{Type}; +    my $relfile = $args{RelativeFile}; + +    if (defined($relfile) && !$self->PathIsSafe(Path => $relfile)) { +        $HTML::Mason::Commands::r->status(400); +        $HTML::Mason::Commands::m->abort; +    } + +    $self->StaticFileHeaders(); + +    unless ($type) { +        if ( $file =~ /\.(gif|png|jpe?g)$/i ) { +            $type = "image/$1"; +            $type =~ s/jpg/jpeg/gi; +        } +        $type ||= "application/octet-stream"; +    } +    $HTML::Mason::Commands::r->content_type($type); +    open my $fh, "<$file" or die "couldn't open file: $!"; +    binmode($fh); +    { +        local $/ = \16384; +        $HTML::Mason::Commands::m->out($_) while (<$fh>); +        $HTML::Mason::Commands::m->flush_buffer; +    } +    close $fh; +} + +sub StripContent { +    my %args    = @_; +    my $content = $args{Content}; +    return '' unless $content; + +    # Make the content have no 'weird' newlines in it +    $content =~ s/\r+\n/\n/g; + +    my $return_content = $content; + +    my $html = $args{ContentType} && $args{ContentType} eq "text/html"; +    my $sigonly = $args{StripSignature}; + +    # massage content to easily detect if there's any real content +    $content =~ s/\s+//g; # yes! remove all the spaces +    if ( $html ) { +        # remove html version of spaces and newlines +        $content =~ s! !!g; +        $content =~ s!<br/?>!!g; +    } + +    # Filter empty content when type is text/html +    return '' if $html && $content !~ /\S/; + +    # If we aren't supposed to strip the sig, just bail now. +    return $return_content unless $sigonly; + +    # Find the signature +    my $sig = $args{'CurrentUser'}->UserObj->Signature || ''; +    $sig =~ s/\s+//g; + +    # Check for plaintext sig +    return '' if not $html and $content =~ /^(--)?\Q$sig\E$/; + +    # Check for html-formatted sig +    RT::Interface::Web::EscapeUTF8( \$sig ); +    return '' +      if $html +          and $content =~ m{^(?:<p>)?(--)?\Q$sig\E(?:</p>)?$}s; + +    # Pass it through +    return $return_content; +} + +sub DecodeARGS { +    my $ARGS = shift; + +    %{$ARGS} = map { + +        # if they've passed multiple values, they'll be an array. if they've +        # passed just one, a scalar whatever they are, mark them as utf8 +        my $type = ref($_); +        ( !$type ) +            ? Encode::is_utf8($_) +                ? $_ +                : Encode::decode( 'UTF-8' => $_, Encode::FB_PERLQQ ) +            : ( $type eq 'ARRAY' ) +            ? [ map { ( ref($_) or Encode::is_utf8($_) ) ? $_ : Encode::decode( 'UTF-8' => $_, Encode::FB_PERLQQ ) } +                @$_ ] +            : ( $type eq 'HASH' ) +            ? { map { ( ref($_) or Encode::is_utf8($_) ) ? $_ : Encode::decode( 'UTF-8' => $_, Encode::FB_PERLQQ ) } +                %$_ } +            : $_ +    } %$ARGS; +} +sub PreprocessTimeUpdates { +    my $ARGS = shift; + +    # Later in the code we use +    # $m->comp( { base_comp => $m->request_comp }, $m->fetch_next, %ARGS ); +    # instead of $m->call_next to avoid problems with UTF8 keys in arguments. +    # The call_next method pass through original arguments and if you have +    # an argument with unicode key then in a next component you'll get two +    # records in the args hash: one with key without UTF8 flag and another +    # with the flag, which may result into errors. "{ base_comp => $m->request_comp }" +    # is copied from mason's source to get the same results as we get from +    # call_next method, this feature is not documented, so we just leave it +    # here to avoid possible side effects. + +    # This code canonicalizes time inputs in hours into minutes +    foreach my $field ( keys %$ARGS ) { +        next unless $field =~ /^(.*)-TimeUnits$/i && $ARGS->{$1}; +        my $local = $1; +        $ARGS->{$local} =~ s{\b (?: (\d+) \s+ )? (\d+)/(\d+) \b} +                      {($1 || 0) + $3 ? $2 / $3 : 0}xe; +        if ( $ARGS->{$field} && $ARGS->{$field} =~ /hours/i ) { +            $ARGS->{$local} *= 60; +        } +        delete $ARGS->{$field}; +    } + +} + +sub MaybeEnableSQLStatementLog { + +    my $log_sql_statements = RT->Config->Get('StatementLog'); + +    if ($log_sql_statements) { +        $RT::Handle->ClearSQLStatementLog; +        $RT::Handle->LogSQLStatements(1); +    } + +} + +sub LogRecordedSQLStatements { +    my $log_sql_statements = RT->Config->Get('StatementLog'); + +    return unless ($log_sql_statements); + +    my @log = $RT::Handle->SQLStatementLog; +    $RT::Handle->ClearSQLStatementLog; +    for my $stmt (@log) { +        my ( $time, $sql, $bind, $duration ) = @{$stmt}; +        my @bind; +        if ( ref $bind ) { +            @bind = @{$bind}; +        } else { + +            # Older DBIx-SB +            $duration = $bind; +        } +        $RT::Logger->log( +            level   => $log_sql_statements, +            message => "SQL(" +                . sprintf( "%.6f", $duration ) +                . "s): $sql;" +                . ( @bind ? "  [ bound values: @{[map{qq|'$_'|} @bind]} ]" : "" ) +        ); +    } + +}  package HTML::Mason::Commands; -use strict; -use vars qw/$r $m %session/; +use vars qw/$r $m %session/;  # {{{ loc @@ -156,19 +800,26 @@ through  sub loc { -    if ($session{'CurrentUser'} &&  -        UNIVERSAL::can($session{'CurrentUser'}, 'loc')){ -        return($session{'CurrentUser'}->loc(@_)); -    } -    else  { -        my $u = RT::CurrentUser->new($RT::SystemUser); -        return ($u->loc(@_)); +    if ( $session{'CurrentUser'} +        && UNIVERSAL::can( $session{'CurrentUser'}, 'loc' ) ) +    { +        return ( $session{'CurrentUser'}->loc(@_) ); +    } elsif ( +        my $u = eval { +            RT::CurrentUser->new(); +        } +        ) +    { +        return ( $u->loc(@_) ); +    } else { + +        # pathetic case -- SystemUser is gone. +        return $_[0];      }  }  # }}} -  # {{{ loc_fuzzy  =head2 loc_fuzzy STRING @@ -182,40 +833,41 @@ inside the lexicon file.  =cut  sub loc_fuzzy { -    my $msg  = shift; -     -    if ($session{'CurrentUser'} &&  -        UNIVERSAL::can($session{'CurrentUser'}, 'loc')){ -        return($session{'CurrentUser'}->loc_fuzzy($msg)); -    } -    else  { -        my $u = RT::CurrentUser->new($RT::SystemUser); -        return ($u->loc_fuzzy($msg)); +    my $msg = shift; + +    if ( $session{'CurrentUser'} +        && UNIVERSAL::can( $session{'CurrentUser'}, 'loc' ) ) +    { +        return ( $session{'CurrentUser'}->loc_fuzzy($msg) ); +    } else { +        my $u = RT::CurrentUser->new( $RT::SystemUser->Id ); +        return ( $u->loc_fuzzy($msg) );      }  }  # }}} -  # {{{ sub Abort  # Error - calls Error and aborts  sub Abort { +    my $why  = shift; +    my %args = @_; -    if ($session{'ErrorDocument'} &&  -        $session{'ErrorDocumentType'}) { -        $r->content_type($session{'ErrorDocumentType'}); -        $m->comp($session{'ErrorDocument'} , Why => shift); +    if (   $session{'ErrorDocument'} +        && $session{'ErrorDocumentType'} ) +    { +        $r->content_type( $session{'ErrorDocumentType'} ); +        $m->comp( $session{'ErrorDocument'}, Why => $why, %args );          $m->abort; -    }  -    else  { -        $m->comp("/Elements/Error" , Why => shift); +    } else { +        $m->comp( "/Elements/Error", Why => $why, %args );          $m->abort;      }  }  # }}} -# {{{ sub CreateTicket  +# {{{ sub CreateTicket  =head2 CreateTicket ARGS @@ -239,80 +891,162 @@ sub CreateTicket {          Abort('You have no permission to create tickets in that queue.');      } -    my $due = new RT::Date( $session{'CurrentUser'} ); -    $due->Set( Format => 'unknown', Value => $ARGS{'Due'} ); -    my $starts = new RT::Date( $session{'CurrentUser'} ); -    $starts->Set( Format => 'unknown', Value => $ARGS{'Starts'} ); +    my $due; +    if ( defined $ARGS{'Due'} and $ARGS{'Due'} =~ /\S/ ) { +        $due = new RT::Date( $session{'CurrentUser'} ); +        $due->Set( Format => 'unknown', Value => $ARGS{'Due'} ); +    } +    my $starts; +    if ( defined $ARGS{'Starts'} and $ARGS{'Starts'} =~ /\S/ ) { +        $starts = new RT::Date( $session{'CurrentUser'} ); +        $starts->Set( Format => 'unknown', Value => $ARGS{'Starts'} ); +    } -    my @Requestors = split ( /\s*,\s*/, $ARGS{'Requestors'} ); -    my @Cc         = split ( /\s*,\s*/, $ARGS{'Cc'} ); -    my @AdminCc    = split ( /\s*,\s*/, $ARGS{'AdminCc'} ); +    my $sigless = RT::Interface::Web::StripContent( +        Content        => $ARGS{Content}, +        ContentType    => $ARGS{ContentType}, +        StripSignature => 1, +        CurrentUser    => $session{'CurrentUser'}, +    );      my $MIMEObj = MakeMIMEEntity( -        Subject             => $ARGS{'Subject'}, -        From                => $ARGS{'From'}, -        Cc                  => $ARGS{'Cc'}, -        Body                => $ARGS{'Content'}, +        Subject => $ARGS{'Subject'}, +        From    => $ARGS{'From'}, +        Cc      => $ARGS{'Cc'}, +        Body    => $sigless, +        Type    => $ARGS{'ContentType'},      ); -    if ($ARGS{'Attachments'}) { -        $MIMEObj->make_multipart; -        $MIMEObj->add_part($_) foreach values %{$ARGS{'Attachments'}}; +    if ( $ARGS{'Attachments'} ) { +        my $rv = $MIMEObj->make_multipart; +        $RT::Logger->error("Couldn't make multipart message") +            if !$rv || $rv !~ /^(?:DONE|ALREADY)$/; + +        foreach ( values %{ $ARGS{'Attachments'} } ) { +            unless ($_) { +                $RT::Logger->error("Couldn't add empty attachemnt"); +                next; +            } +            $MIMEObj->add_part($_); +        } +    } + +    foreach my $argument (qw(Encrypt Sign)) { +        $MIMEObj->head->add( +            "X-RT-$argument" => Encode::encode_utf8( $ARGS{$argument} ) +        ) if defined $ARGS{$argument};      }      my %create_args = ( -        Queue           => $ARGS{'Queue'}, -        Owner           => $ARGS{'Owner'}, +        Type => $ARGS{'Type'} || 'ticket', +        Queue => $ARGS{'Queue'}, +        Owner => $ARGS{'Owner'}, + +        # note: name change +        Requestor       => $ARGS{'Requestors'}, +        Cc              => $ARGS{'Cc'}, +        AdminCc         => $ARGS{'AdminCc'},          InitialPriority => $ARGS{'InitialPriority'},          FinalPriority   => $ARGS{'FinalPriority'},          TimeLeft        => $ARGS{'TimeLeft'}, -        TimeEstimated        => $ARGS{'TimeEstimated'}, +        TimeEstimated   => $ARGS{'TimeEstimated'},          TimeWorked      => $ARGS{'TimeWorked'}, -        Requestor       => \@Requestors, -        Cc              => \@Cc, -        AdminCc         => \@AdminCc,          Subject         => $ARGS{'Subject'},          Status          => $ARGS{'Status'}, -        Due             => $due->ISO, -        Starts          => $starts->ISO, +        Due             => $due ? $due->ISO : undef, +        Starts          => $starts ? $starts->ISO : undef,          MIMEObj         => $MIMEObj      ); -  foreach my $arg (%ARGS) { -        if ($arg =~ /^CustomField-(\d+)(.*?)$/) { -            next if ($arg =~ /-Magic$/); -            $create_args{"CustomField-".$1} = $ARGS{"$arg"}; -        } + +    my @temp_squelch; +    foreach my $type (qw(Requestor Cc AdminCc)) { +        push @temp_squelch, map $_->address, Email::Address->parse( $create_args{$type} ) +            if grep $_ eq $type || $_ eq ( $type . 's' ), @{ $ARGS{'SkipNotification'} || [] }; +      } -    my ( $id, $Trans, $ErrMsg ) = $Ticket->Create(%create_args); -    unless ( $id && $Trans ) { -        Abort($ErrMsg); + +    if (@temp_squelch) { +        require RT::Action::SendEmail; +        RT::Action::SendEmail->SquelchMailTo( RT::Action::SendEmail->SquelchMailTo, @temp_squelch );      } -    my @linktypes = qw( DependsOn MemberOf RefersTo ); -    foreach my $linktype (@linktypes) { -        foreach my $luri ( split ( / /, $ARGS{"new-$linktype"} ) ) { -            $luri =~ s/\s*$//;    # Strip trailing whitespace -            my ( $val, $msg ) = $Ticket->AddLink( -                Target => $luri, -                Type   => $linktype -            ); -            push ( @Actions, $msg ) unless ($val); +    if ( $ARGS{'AttachTickets'} ) { +        require RT::Action::SendEmail; +        RT::Action::SendEmail->AttachTickets( RT::Action::SendEmail->AttachTickets, +            ref $ARGS{'AttachTickets'} +            ? @{ $ARGS{'AttachTickets'} } +            : ( $ARGS{'AttachTickets'} ) ); +    } + +    foreach my $arg ( keys %ARGS ) { +        next if $arg =~ /-(?:Magic|Category)$/; + +        if ( $arg =~ /^Object-RT::Transaction--CustomField-/ ) { +            $create_args{$arg} = $ARGS{$arg};          } -        foreach my $luri ( split ( / /, $ARGS{"$linktype-new"} ) ) { -            my ( $val, $msg ) = $Ticket->AddLink( -                Base => $luri, -                Type => $linktype -            ); +        # Object-RT::Ticket--CustomField-3-Values +        elsif ( $arg =~ /^Object-RT::Ticket--CustomField-(\d+)/ ) { +            my $cfid = $1; + +            my $cf = RT::CustomField->new( $session{'CurrentUser'} ); +            $cf->Load($cfid); +            unless ( $cf->id ) { +                $RT::Logger->error( "Couldn't load custom field #" . $cfid ); +                next; +            } -            push ( @Actions, $msg ) unless ($val); +            if ( $arg =~ /-Upload$/ ) { +                $create_args{"CustomField-$cfid"} = _UploadedFile($arg); +                next; +            } + +            my $type = $cf->Type; + +            my @values = (); +            if ( ref $ARGS{$arg} eq 'ARRAY' ) { +                @values = @{ $ARGS{$arg} }; +            } elsif ( $type =~ /text/i ) { +                @values = ( $ARGS{$arg} ); +            } else { +                no warnings 'uninitialized'; +                @values = split /\r*\n/, $ARGS{$arg}; +            } +            @values = grep length, map { +                s/\r+\n/\n/g; +                s/^\s+//; +                s/\s+$//; +                $_; +                } +                grep defined, @values; + +            $create_args{"CustomField-$cfid"} = \@values;          }      } -    push ( @Actions, split("\n", $ErrMsg) ); +    # turn new link lists into arrays, and pass in the proper arguments +    my %map = ( +        'new-DependsOn' => 'DependsOn', +        'DependsOn-new' => 'DependedOnBy', +        'new-MemberOf'  => 'Parents', +        'MemberOf-new'  => 'Children', +        'new-RefersTo'  => 'RefersTo', +        'RefersTo-new'  => 'ReferredToBy', +    ); +    foreach my $key ( keys %map ) { +        next unless $ARGS{$key}; +        $create_args{ $map{$key} } = [ grep $_, split ' ', $ARGS{$key} ]; + +    } + +    my ( $id, $Trans, $ErrMsg ) = $Ticket->Create(%create_args); +    unless ($id) { +        Abort($ErrMsg); +    } + +    push( @Actions, split( "\n", $ErrMsg ) );      unless ( $Ticket->CurrentUserHasRight('ShowTicket') ) { -        Abort( "No permission to view newly created ticket #" -            . $Ticket->id . "." ); +        Abort( "No permission to view newly created ticket #" . $Ticket->id . "." );      }      return ( $Ticket, @Actions ); @@ -354,370 +1088,231 @@ sub LoadTicket {  # {{{ sub ProcessUpdateMessage -sub ProcessUpdateMessage { - -    #TODO document what else this takes. -    my %args = ( -        ARGSRef   => undef, -        Actions   => undef, -        TicketObj => undef, -        @_ -    ); - -    #Make the update content have no 'weird' newlines in it -    if ( $args{ARGSRef}->{'UpdateContent'} ) { - -        if ( -            $args{ARGSRef}->{'UpdateSubject'} eq $args{'TicketObj'}->Subject() ) -        { -            $args{ARGSRef}->{'UpdateSubject'} = undef; -        } - -        my $Message = MakeMIMEEntity( -            Subject             => $args{ARGSRef}->{'UpdateSubject'}, -            Body                => $args{ARGSRef}->{'UpdateContent'}, -        ); - -        if ($args{ARGSRef}->{'UpdateAttachments'}) { -            $Message->make_multipart; -            $Message->add_part($_) foreach values %{$args{ARGSRef}->{'UpdateAttachments'}}; -        } - -        ## TODO: Implement public comments -        if ( $args{ARGSRef}->{'UpdateType'} =~ /^(private|public)$/ ) { -            my ( $Transaction, $Description ) = $args{TicketObj}->Comment( -                CcMessageTo  => $args{ARGSRef}->{'UpdateCc'}, -                BccMessageTo => $args{ARGSRef}->{'UpdateBcc'}, -                MIMEObj      => $Message, -                TimeTaken    => $args{ARGSRef}->{'UpdateTimeWorked'} -            ); -            push ( @{ $args{Actions} }, $Description ); -        } -        elsif ( $args{ARGSRef}->{'UpdateType'} eq 'response' ) { -            my ( $Transaction, $Description ) = $args{TicketObj}->Correspond( -                CcMessageTo  => $args{ARGSRef}->{'UpdateCc'}, -                BccMessageTo => $args{ARGSRef}->{'UpdateBcc'}, -                MIMEObj      => $Message, -                TimeTaken    => $args{ARGSRef}->{'UpdateTimeWorked'} -            ); -            push ( @{ $args{Actions} }, $Description ); -        } -        else { -            push ( @{ $args{'Actions'} }, -                loc("Update type was neither correspondence nor comment."). -                " ". -                loc("Update not recorded.") -            ); -        } -    } -} - -# }}} +=head2 ProcessUpdateMessage -# {{{ sub MakeMIMEEntity +Takes paramhash with fields ARGSRef, TicketObj and SkipSignatureOnly. -=head2 MakeMIMEEntity PARAMHASH - -Takes a paramhash Subject, Body and AttachmentFieldName. - -  Returns a MIME::Entity. +Don't write message if it only contains current user's signature and +SkipSignatureOnly argument is true. Function anyway adds attachments +and updates time worked field even if skips message. The default value +is true.  =cut -sub MakeMIMEEntity { +sub ProcessUpdateMessage { -    #TODO document what else this takes.      my %args = ( -        Subject             => undef, -        From                => undef, -        Cc                  => undef, -        Body                => undef, -        AttachmentFieldName => undef, -        map Encode::encode_utf8($_), @_, +        ARGSRef           => undef, +        TicketObj         => undef, +        SkipSignatureOnly => 1, +        @_      ); -    #Make the update content have no 'weird' newlines in it - -    $args{'Body'} =~ s/\r\n/\n/gs; -    my $Message; +    if ( $args{ARGSRef}->{'UpdateAttachments'} +        && !keys %{ $args{ARGSRef}->{'UpdateAttachments'} } )      { -        # MIME::Head is not happy in utf-8 domain.  This only happens -        # when processing an incoming email (so far observed). -        no utf8; -        use bytes; -        $Message = MIME::Entity->build( -            Subject => $args{'Subject'} || "", -            From    => $args{'From'}, -            Cc      => $args{'Cc'}, -            Data    => [ $args{'Body'} ] -        ); -    } - -    my $cgi_object = $m->cgi_object; - -    if (my $filehandle = $cgi_object->upload( $args{'AttachmentFieldName'} ) ) { - - - -    use File::Temp qw(tempfile tempdir); - -    #foreach my $filehandle (@filenames) { - -    my ( $fh, $temp_file ) = tempfile(); - -    binmode $fh;    #thank you, windows -    my ($buffer); -    while ( my $bytesread = read( $filehandle, $buffer, 4096 ) ) { -        print $fh $buffer; +        delete $args{ARGSRef}->{'UpdateAttachments'};      } -    my $uploadinfo = $cgi_object->uploadInfo($filehandle); - -    # Prefer the cached name first over CGI.pm stringification. -    my $filename = $RT::Mason::CGI::Filename; -    $filename = "$filehandle" unless defined($filename); -                    -    $filename =~ s#^.*[\\/]##; - -    $Message->attach( -        Path     => $temp_file, -        Filename => $filename, -        Type     => $uploadinfo->{'Content-Type'}, +    # Strip the signature +    $args{ARGSRef}->{UpdateContent} = RT::Interface::Web::StripContent( +        Content        => $args{ARGSRef}->{UpdateContent}, +        ContentType    => $args{ARGSRef}->{UpdateContentType}, +        StripSignature => $args{SkipSignatureOnly}, +        CurrentUser    => $args{'TicketObj'}->CurrentUser,      ); -    close($fh); - -    #   } +    # If, after stripping the signature, we have no message, move the +    # UpdateTimeWorked into adjusted TimeWorked, so that a later +    # ProcessBasics can deal -- then bail out. +    if (    not $args{ARGSRef}->{'UpdateAttachments'} +        and not length $args{ARGSRef}->{'UpdateContent'} ) +    { +        if ( $args{ARGSRef}->{'UpdateTimeWorked'} ) { +            $args{ARGSRef}->{TimeWorked} = $args{TicketObj}->TimeWorked + delete $args{ARGSRef}->{'UpdateTimeWorked'}; +        } +        return;      } -    $Message->make_singlepart(); -    RT::I18N::SetMIMEEntityToUTF8($Message); # convert text parts into utf-8 - -    return ($Message); - -} - -# }}} - -# {{{ sub ProcessSearchQuery - -=head2 ProcessSearchQuery - -  Takes a form such as the one filled out in webrt/Search/Elements/PickRestriction and turns it into something that RT::Tickets can understand. - -TODO Doc exactly what comes in the paramhash - - -=cut - -sub ProcessSearchQuery { -    my %args = @_; - -    ## TODO: The only parameter here is %ARGS.  Maybe it would be -    ## cleaner to load this parameter as $ARGS, and use $ARGS->{...} -    ## instead of $args{ARGS}->{...} ? :) +    if ( $args{ARGSRef}->{'UpdateSubject'} eq $args{'TicketObj'}->Subject ) { +        $args{ARGSRef}->{'UpdateSubject'} = undef; +    } -    #Searches are sticky. -    if ( defined $session{'tickets'} ) { +    my $Message = MakeMIMEEntity( +        Subject => $args{ARGSRef}->{'UpdateSubject'}, +        Body    => $args{ARGSRef}->{'UpdateContent'}, +        Type    => $args{ARGSRef}->{'UpdateContentType'}, +    ); -        # Reset the old search -        $session{'tickets'}->GotoFirstItem; +    $Message->head->add( 'Message-ID' => Encode::encode_utf8( +        RT::Interface::Email::GenMessageId( Ticket => $args{'TicketObj'} ) +    ) ); +    my $old_txn = RT::Transaction->new( $session{'CurrentUser'} ); +    if ( $args{ARGSRef}->{'QuoteTransaction'} ) { +        $old_txn->Load( $args{ARGSRef}->{'QuoteTransaction'} ); +    } else { +        $old_txn = $args{TicketObj}->Transactions->First();      } -    else { -        # Init a new search -        $session{'tickets'} = RT::Tickets->new( $session{'CurrentUser'} ); +    if ( my $msg = $old_txn->Message->First ) { +        RT::Interface::Email::SetInReplyTo( +            Message   => $Message, +            InReplyTo => $msg +        );      } -    #Import a bookmarked search if we have one -    if ( defined $args{ARGS}->{'Bookmark'} ) { -        $session{'tickets'}->ThawLimits( $args{ARGS}->{'Bookmark'} ); +    if ( $args{ARGSRef}->{'UpdateAttachments'} ) { +        $Message->make_multipart; +        $Message->add_part($_) foreach values %{ $args{ARGSRef}->{'UpdateAttachments'} };      } -    # {{{ Goto next/prev page -    if ( $args{ARGS}->{'GotoPage'} eq 'Next' ) { -        $session{'tickets'}->NextPage; -    } -    elsif ( $args{ARGS}->{'GotoPage'} eq 'Prev' ) { -        $session{'tickets'}->PrevPage; -    } -    elsif ( $args{ARGS}->{'GotoPage'} > 0 ) { -        $session{'tickets'}->GotoPage( $args{ARGS}->{GotoPage} - 1 ); +    if ( $args{ARGSRef}->{'AttachTickets'} ) { +        require RT::Action::SendEmail; +        RT::Action::SendEmail->AttachTickets( RT::Action::SendEmail->AttachTickets, +            ref $args{ARGSRef}->{'AttachTickets'} +            ? @{ $args{ARGSRef}->{'AttachTickets'} } +            : ( $args{ARGSRef}->{'AttachTickets'} ) );      } -    # }}} +    my $bcc = $args{ARGSRef}->{'UpdateBcc'}; +    my $cc  = $args{ARGSRef}->{'UpdateCc'}; -    # {{{ Deal with limiting the search +    my %message_args = ( +        CcMessageTo  => $cc, +        BccMessageTo => $bcc, +        Sign         => $args{ARGSRef}->{'Sign'}, +        Encrypt      => $args{ARGSRef}->{'Encrypt'}, +        MIMEObj      => $Message, +        TimeTaken    => $args{ARGSRef}->{'UpdateTimeWorked'} +    ); -    if ( $args{ARGS}->{'RefreshSearchInterval'} ) { -        $session{'tickets_refresh_interval'} = -          $args{ARGS}->{'RefreshSearchInterval'}; +    my @temp_squelch; +    foreach my $type (qw(Cc AdminCc)) { +        if (grep $_ eq $type || $_ eq ( $type . 's' ), @{ $args{ARGSRef}->{'SkipNotification'} || [] }) { +            push @temp_squelch, map $_->address, Email::Address->parse( $message_args{$type} ); +            push @temp_squelch, $args{TicketObj}->$type->MemberEmailAddresses; +            push @temp_squelch, $args{TicketObj}->QueueObj->$type->MemberEmailAddresses; +        }      } - -    if ( $args{ARGS}->{'TicketsSortBy'} ) { -        $session{'tickets_sort_by'}    = $args{ARGS}->{'TicketsSortBy'}; -        $session{'tickets_sort_order'} = $args{ARGS}->{'TicketsSortOrder'}; -        $session{'tickets'}->OrderBy( -            FIELD => $args{ARGS}->{'TicketsSortBy'}, -            ORDER => $args{ARGS}->{'TicketsSortOrder'} -        ); +    if (grep $_ eq 'Requestor' || $_ eq 'Requestors', @{ $args{ARGSRef}->{'SkipNotification'} || [] }) { +            push @temp_squelch, map $_->address, Email::Address->parse( $message_args{Requestor} ); +            push @temp_squelch, $args{TicketObj}->Requestors->MemberEmailAddresses;      } -    # }}} +    if (@temp_squelch) { +        require RT::Action::SendEmail; +        RT::Action::SendEmail->SquelchMailTo( RT::Action::SendEmail->SquelchMailTo, @temp_squelch ); +    } -    # {{{ Set the query limit -    if ( defined $args{ARGS}->{'RowsPerPage'} ) { -        $RT::Logger->debug( -            "limiting to " . $args{ARGS}->{'RowsPerPage'} . " rows" ); +    unless ( $args{'ARGSRef'}->{'UpdateIgnoreAddressCheckboxes'} ) { +        foreach my $key ( keys %{ $args{ARGSRef} } ) { +            next unless $key =~ /^Update(Cc|Bcc)-(.*)$/; -        $session{'tickets_rows_per_page'} = $args{ARGS}->{'RowsPerPage'}; -        $session{'tickets'}->RowsPerPage( $args{ARGS}->{'RowsPerPage'} ); +            my $var   = ucfirst($1) . 'MessageTo'; +            my $value = $2; +            if ( $message_args{$var} ) { +                $message_args{$var} .= ", $value"; +            } else { +                $message_args{$var} = $value; +            } +        }      } -    # }}} -    # {{{ Limit priority -    if ( $args{ARGS}->{'ValueOfPriority'} ne '' ) { -        $session{'tickets'}->LimitPriority( -            VALUE    => $args{ARGS}->{'ValueOfPriority'}, -            OPERATOR => $args{ARGS}->{'PriorityOp'} -        ); +    my @results; +    if ( $args{ARGSRef}->{'UpdateType'} =~ /^(private|public)$/ ) { +        my ( $Transaction, $Description, $Object ) = $args{TicketObj}->Comment(%message_args); +        push( @results, $Description ); +        $Object->UpdateCustomFields( ARGSRef => $args{ARGSRef} ) if $Object; +    } elsif ( $args{ARGSRef}->{'UpdateType'} eq 'response' ) { +        my ( $Transaction, $Description, $Object ) = $args{TicketObj}->Correspond(%message_args); +        push( @results, $Description ); +        $Object->UpdateCustomFields( ARGSRef => $args{ARGSRef} ) if $Object; +    } else { +        push( @results, +            loc("Update type was neither correspondence nor comment.") . " " . loc("Update not recorded.") );      } +    return @results; +} -    # }}} -    # {{{ Limit owner -    if ( $args{ARGS}->{'ValueOfOwner'} ne '' ) { -        $session{'tickets'}->LimitOwner( -            VALUE    => $args{ARGS}->{'ValueOfOwner'}, -            OPERATOR => $args{ARGS}->{'OwnerOp'} -        ); -    } +# }}} -    # }}} -    # {{{ Limit requestor email +# {{{ sub MakeMIMEEntity -    if ( $args{ARGS}->{'ValueOfRequestor'} ne '' ) { -        my $alias = $session{'tickets'}->LimitRequestor( -            VALUE    => $args{ARGS}->{'ValueOfRequestor'}, -            OPERATOR => $args{ARGS}->{'RequestorOp'}, -        ); +=head2 MakeMIMEEntity PARAMHASH -    } +Takes a paramhash Subject, Body and AttachmentFieldName. -    # }}} -    # {{{ Limit Queue -    if ( $args{ARGS}->{'ValueOfQueue'} ne '' ) { -        $session{'tickets'}->LimitQueue( -            VALUE    => $args{ARGS}->{'ValueOfQueue'}, -            OPERATOR => $args{ARGS}->{'QueueOp'} -        ); -    } +Also takes Form, Cc and Type as optional paramhash keys. -    # }}} -    # {{{ Limit Status -    if ( $args{ARGS}->{'ValueOfStatus'} ne '' ) { -        if ( ref( $args{ARGS}->{'ValueOfStatus'} ) ) { -            foreach my $value ( @{ $args{ARGS}->{'ValueOfStatus'} } ) { -                $session{'tickets'}->LimitStatus( -                    VALUE    => $value, -                    OPERATOR => $args{ARGS}->{'StatusOp'}, -                ); -            } -        } -        else { -            $session{'tickets'}->LimitStatus( -                VALUE    => $args{ARGS}->{'ValueOfStatus'}, -                OPERATOR => $args{ARGS}->{'StatusOp'}, -            ); -        } +  Returns a MIME::Entity. -    } +=cut -    # }}} -    # {{{ Limit Subject -    if ( $args{ARGS}->{'ValueOfSubject'} ne '' ) { -            my $val = $args{ARGS}->{'ValueOfSubject'}; -        if ($args{ARGS}->{'SubjectOp'} =~ /like/) { -            $val = "%".$val."%"; -        } -        $session{'tickets'}->LimitSubject( -            VALUE    => $val, -            OPERATOR => $args{ARGS}->{'SubjectOp'}, -        ); -    } +sub MakeMIMEEntity { + +    #TODO document what else this takes. +    my %args = ( +        Subject             => undef, +        From                => undef, +        Cc                  => undef, +        Body                => undef, +        AttachmentFieldName => undef, +        Type                => undef, +        @_, +    ); +    my $Message = MIME::Entity->build( +        Type    => 'multipart/mixed', +        map { $_ => Encode::encode_utf8( $args{ $_} ) } +            grep defined $args{$_}, qw(Subject From Cc) +    ); -    # }}}     -    # {{{ Limit Dates -    if ( $args{ARGS}->{'ValueOfDate'} ne '' ) { -        my $date = ParseDateToISO( $args{ARGS}->{'ValueOfDate'} ); -        $args{ARGS}->{'DateType'} =~ s/_Date$//; +    if ( defined $args{'Body'} && length $args{'Body'} ) { -        if ( $args{ARGS}->{'DateType'} eq 'Updated' ) { -            $session{'tickets'}->LimitTransactionDate( -                                            VALUE    => $date, -                                            OPERATOR => $args{ARGS}->{'DateOp'}, -            ); -        } -        else { -            $session{'tickets'}->LimitDate( FIELD => $args{ARGS}->{'DateType'}, -                                            VALUE => $date, -                                            OPERATOR => $args{ARGS}->{'DateOp'}, -            ); -        } -    } +        # Make the update content have no 'weird' newlines in it +        $args{'Body'} =~ s/\r\n/\n/gs; -    # }}}     -    # {{{ Limit Content -    if ( $args{ARGS}->{'ValueOfAttachmentField'} ne '' ) { -        my $val = $args{ARGS}->{'ValueOfAttachmentField'}; -        if ($args{ARGS}->{'AttachmentFieldOp'} =~ /like/) { -            $val = "%".$val."%"; -        } -        $session{'tickets'}->Limit( -            FIELD   => $args{ARGS}->{'AttachmentField'}, -            VALUE    => $val, -            OPERATOR => $args{ARGS}->{'AttachmentFieldOp'}, +        $Message->attach( +            Type    => $args{'Type'} || 'text/plain', +            Charset => 'UTF-8', +            Data    => $args{'Body'},          );      } -    # }}}    +    if ( $args{'AttachmentFieldName'} ) { - # {{{ Limit CustomFields +        my $cgi_object = $m->cgi_object; -    foreach my $arg ( keys %{ $args{ARGS} } ) { -        my $id; -        if ( $arg =~ /^CustomField(\d+)$/ ) { -            $id = $1; -        } -        else { -            next; -        } -        next unless ( $args{ARGS}->{$arg} ); - -        my $form = $args{ARGS}->{$arg}; -        my $oper = $args{ARGS}->{ "CustomFieldOp" . $id }; -        foreach my $value ( ref($form) ? @{$form} : ($form) ) { -            my $quote = 1; -            if ($oper =~ /like/i) { -                $value = "%".$value."%"; +        if ( my $filehandle = $cgi_object->upload( $args{'AttachmentFieldName'} ) ) { + +            my ( @content, $buffer ); +            while ( my $bytesread = read( $filehandle, $buffer, 4096 ) ) { +                push @content, $buffer;              } -            if ( $value =~ /^null$/i ) { -                #Don't quote the string 'null' -                $quote = 0; +            my $uploadinfo = $cgi_object->uploadInfo($filehandle); + +            # Prefer the cached name first over CGI.pm stringification. +            my $filename = $RT::Mason::CGI::Filename; +            $filename = "$filehandle" unless defined $filename; +            $filename = Encode::encode_utf8( $filename ); +            $filename =~ s{^.*[\\/]}{}; -                # Convert the operator to something apropriate for nulls -                $oper = 'IS'     if ( $oper eq '=' ); -                $oper = 'IS NOT' if ( $oper eq '!=' ); +            $Message->attach( +                Type     => $uploadinfo->{'Content-Type'}, +                Filename => $filename, +                Data     => \@content, +            ); +            if ( !$args{'Subject'} && !( defined $args{'Body'} && length $args{'Body'} ) ) { +                $Message->head->set( 'Subject' => $filename );              } -            $session{'tickets'}->LimitCustomField( CUSTOMFIELD => $id, -                                                   OPERATOR    => $oper, -                                                   QUOTEVALUE  => $quote, -                                                   VALUE       => $value );          }      } -    # }}} +    $Message->make_singlepart; + +    RT::I18N::SetMIMEEntityToUTF8($Message);    # convert text parts into utf-8 +    return ($Message);  } @@ -735,7 +1330,7 @@ Returns an ISO date and time in GMT  sub ParseDateToISO {      my $date = shift; -    my $date_obj = RT::Date->new($session{'CurrentUser'}); +    my $date_obj = RT::Date->new( $session{'CurrentUser'} );      $date_obj->Set(          Format => 'unknown',          Value  => $date @@ -745,98 +1340,56 @@ sub ParseDateToISO {  # }}} -# {{{ sub Config  -# TODO: This might eventually read the cookies, user configuration -# information from the DB, queue configuration information from the -# DB, etc. - -sub Config { -    my $args = shift; -    my $key  = shift; -    return $args->{$key} || $RT::WebOptions{$key}; -} - -# }}} -  # {{{ sub ProcessACLChanges  sub ProcessACLChanges {      my $ARGSref = shift; -    my %ARGS     = %$ARGSref; - -    my ( $ACL, @results ); - - -    foreach my $arg (keys %ARGS) { -        if ($arg =~ /GrantRight-(\d+)-(.*?)-(\d+)$/) { -            my $principal_id = $1; -            my $object_type = $2; -            my $object_id = $3; -            my $rights = $ARGS{$arg}; - -            my $principal = RT::Principal->new($session{'CurrentUser'}); -            $principal->Load($principal_id); +    #XXX: why don't we get ARGSref like in other Process* subs? -            my $obj; +    my @results; -            if ($object_type eq 'RT::Queue') { -                $obj = RT::Queue->new($session{'CurrentUser'}); -                $obj->Load($object_id);       -            } elsif ($object_type eq 'RT::Group') { -                $obj = RT::Group->new($session{'CurrentUser'}); -                $obj->Load($object_id);       +    foreach my $arg ( keys %$ARGSref ) { +        next unless ( $arg =~ /^(GrantRight|RevokeRight)-(\d+)-(.+?)-(\d+)$/ ); -            } elsif ($object_type eq 'RT::System') { -                $obj = $RT::System; -            } else { -                push (@results, loc("System Error"). -                                loc("Rights could not be granted for [_1]", $object_type)); -                next; -            } +        my ( $method, $principal_id, $object_type, $object_id ) = ( $1, $2, $3, $4 ); -            my @rights = ref($ARGS{$arg}) eq 'ARRAY' ? @{$ARGS{$arg}} : ($ARGS{$arg}); -            foreach my $right (@rights) { -                next unless ($right); -                my ($val, $msg) = $principal->GrantRight(Object => $obj, Right => $right); -                push (@results, $msg); -            } +        my @rights; +        if ( UNIVERSAL::isa( $ARGSref->{$arg}, 'ARRAY' ) ) { +            @rights = @{ $ARGSref->{$arg} }; +        } else { +            @rights = $ARGSref->{$arg};          } -       elsif ($arg =~ /RevokeRight-(\d+)-(.*?)-(\d+)-(.*?)$/) { -            my $principal_id = $1; -            my $object_type = $2; -            my $object_id = $3; -            my $right = $4; - -            my $principal = RT::Principal->new($session{'CurrentUser'}); -            $principal->Load($principal_id); -            next unless ($right); -            my $obj; - -            if ($object_type eq 'RT::Queue') { -                $obj = RT::Queue->new($session{'CurrentUser'}); -                $obj->Load($object_id);       -            } elsif ($object_type eq 'RT::Group') { -                $obj = RT::Group->new($session{'CurrentUser'}); -                $obj->Load($object_id);       - -            } elsif ($object_type eq 'RT::System') { -                $obj = $RT::System; -            } else { -                push (@results, loc("System Error"). -                                loc("Rights could not be revoked for [_1]", $object_type)); +        @rights = grep $_, @rights; +        next unless @rights; + +        my $principal = RT::Principal->new( $session{'CurrentUser'} ); +        $principal->Load($principal_id); + +        my $obj; +        if ( $object_type eq 'RT::System' ) { +            $obj = $RT::System; +        } elsif ( $RT::ACE::OBJECT_TYPES{$object_type} ) { +            $obj = $object_type->new( $session{'CurrentUser'} ); +            $obj->Load($object_id); +            unless ( $obj->id ) { +                $RT::Logger->error("couldn't load $object_type #$object_id");                  next;              } -            my ($val, $msg) = $principal->RevokeRight(Object => $obj, Right => $right); -            push (@results, $msg); +        } else { +            $RT::Logger->error("object type '$object_type' is incorrect"); +            push( @results, loc("System Error") . ': ' . loc( "Rights could not be granted for [_1]", $object_type ) ); +            next;          } - +        foreach my $right (@rights) { +            my ( $val, $msg ) = $principal->$method( Object => $obj, Right => $right ); +            push( @results, $msg ); +        }      }      return (@results); - -    } +}  # }}} @@ -852,59 +1405,20 @@ Returns an array of success/failure messages  sub UpdateRecordObject {      my %args = ( -        ARGSRef       => undef, -        AttributesRef => undef, -        Object        => undef, +        ARGSRef         => undef, +        AttributesRef   => undef, +        Object          => undef,          AttributePrefix => undef,          @_      ); -    my (@results); - -    my $object     = $args{'Object'}; -    my $attributes = $args{'AttributesRef'}; -    my $ARGSRef    = $args{'ARGSRef'}; -    foreach my $attribute (@$attributes) { -        my $value; -        if ( defined $ARGSRef->{$attribute} ) { -            $value = $ARGSRef->{$attribute}; -        } -        elsif ( -              defined( $args{'AttributePrefix'} ) -              && defined( -                  $ARGSRef->{ $args{'AttributePrefix'} . "-" . $attribute } -              ) -          ) { -            $value = $ARGSRef->{ $args{'AttributePrefix'} . "-" . $attribute }; - -        } else { -                next; -        } +    my $Object  = $args{'Object'}; +    my @results = $Object->Update( +        AttributesRef   => $args{'AttributesRef'}, +        ARGSRef         => $args{'ARGSRef'}, +        AttributePrefix => $args{'AttributePrefix'}, +    ); -            $value =~ s/\r\n/\n/gs; - -        if ($value ne $object->$attribute()){ - -              my $method = "Set$attribute"; -              my ( $code, $msg ) = $object->$method($value); - -              push @results, loc($attribute) . ': ' . loc_fuzzy($msg); -=for loc -                                   "[_1] could not be set to [_2].",       # loc -                                   "That is already the current value",    # loc -                                   "No value sent to _Set!\n",             # loc -                                   "Illegal value for [_1]",               # loc -                                   "The new value has been set.",          # loc -                                   "No column specified",                  # loc -                                   "Immutable field",                      # loc -                                   "Nonexistant field?",                   # loc -                                   "Invalid data",                         # loc -                                   "Couldn't find row",                    # loc -                                   "Missing a primary key?: [_1]",         # loc -                                   "Found Object",                         # loc -=cut -          }; -    }      return (@results);  } @@ -922,37 +1436,44 @@ sub ProcessCustomFieldUpdates {      my $Object  = $args{'CustomFieldObj'};      my $ARGSRef = $args{'ARGSRef'}; -    my @attribs = qw( Name Type Description Queue SortOrder); +    my @attribs = qw(Name Type Description Queue SortOrder);      my @results = UpdateRecordObject(          AttributesRef => \@attribs,          Object        => $Object,          ARGSRef       => $ARGSRef      ); -    if ( $ARGSRef->{ "CustomField-" . $Object->Id . "-AddValue-Name" } ) { - +    my $prefix = "CustomField-" . $Object->Id; +    if ( $ARGSRef->{"$prefix-AddValue-Name"} ) {          my ( $addval, $addmsg ) = $Object->AddValue( -            Name => -              $ARGSRef->{ "CustomField-" . $Object->Id . "-AddValue-Name" }, -            Description => $ARGSRef->{ "CustomField-" -                  . $Object->Id -                  . "-AddValue-Description" }, -            SortOrder => $ARGSRef->{ "CustomField-" -                  . $Object->Id -                  . "-AddValue-SortOrder" }, +            Name        => $ARGSRef->{"$prefix-AddValue-Name"}, +            Description => $ARGSRef->{"$prefix-AddValue-Description"}, +            SortOrder   => $ARGSRef->{"$prefix-AddValue-SortOrder"},          ); -        push ( @results, $addmsg ); +        push( @results, $addmsg );      } -    my @delete_values = ( -        ref $ARGSRef->{ 'CustomField-' . $Object->Id . '-DeleteValue' } eq -          'ARRAY' ) -      ? @{ $ARGSRef->{ 'CustomField-' . $Object->Id . '-DeleteValue' } } -      : ( $ARGSRef->{ 'CustomField-' . $Object->Id . '-DeleteValue' } ); + +    my @delete_values +        = ( ref $ARGSRef->{"$prefix-DeleteValue"} eq 'ARRAY' ) +        ? @{ $ARGSRef->{"$prefix-DeleteValue"} } +        : ( $ARGSRef->{"$prefix-DeleteValue"} ); +      foreach my $id (@delete_values) {          next unless defined $id;          my ( $err, $msg ) = $Object->DeleteValue($id); -        push ( @results, $msg ); +        push( @results, $msg ); +    } + +    my $vals = $Object->Values(); +    while ( my $cfv = $vals->Next() ) { +        if ( my $so = $ARGSRef->{ "$prefix-SortOrder" . $cfv->Id } ) { +            if ( $cfv->SortOrder != $so ) { +                my ( $err, $msg ) = $cfv->SetSortOrder($so); +                push( @results, $msg ); +            } +        }      } +      return (@results);  } @@ -977,30 +1498,35 @@ sub ProcessTicketBasics {      my $TicketObj = $args{'TicketObj'};      my $ARGSRef   = $args{'ARGSRef'}; -    # {{{ Set basic fields  +    # {{{ Set basic fields      my @attribs = qw( -      Subject -      FinalPriority -      Priority -      TimeEstimated -      TimeWorked -      TimeLeft -      Status -      Queue +        Subject +        FinalPriority +        Priority +        TimeEstimated +        TimeWorked +        TimeLeft +        Type +        Status +        Queue      );      if ( $ARGSRef->{'Queue'} and ( $ARGSRef->{'Queue'} !~ /^(\d+)$/ ) ) {          my $tempqueue = RT::Queue->new($RT::SystemUser);          $tempqueue->Load( $ARGSRef->{'Queue'} );          if ( $tempqueue->id ) { -            $ARGSRef->{'Queue'} = $tempqueue->Id(); +            $ARGSRef->{'Queue'} = $tempqueue->id;          }      } +    # Status isn't a field that can be set to a null value. +    # RT core complains if you try +    delete $ARGSRef->{'Status'} unless $ARGSRef->{'Status'}; +      my @results = UpdateRecordObject(          AttributesRef => \@attribs,          Object        => $TicketObj, -        ARGSRef       => $ARGSRef +        ARGSRef       => $ARGSRef,      );      # We special case owner changing, so we can use ForceOwnerChange @@ -1008,14 +1534,12 @@ sub ProcessTicketBasics {          my ($ChownType);          if ( $ARGSRef->{'ForceOwnerChange'} ) {              $ChownType = "Force"; -        } -        else { +        } else {              $ChownType = "Give";          } -        my ( $val, $msg ) = -          $TicketObj->SetOwner( $ARGSRef->{'Owner'}, $ChownType ); -        push ( @results, $msg ); +        my ( $val, $msg ) = $TicketObj->SetOwner( $ARGSRef->{'Owner'}, $ChownType ); +        push( @results, $msg );      }      # }}} @@ -1025,142 +1549,220 @@ sub ProcessTicketBasics {  # }}} -# {{{ Sub ProcessTicketCustomFieldUpdates -  sub ProcessTicketCustomFieldUpdates { -    my %args = ( -        ARGSRef => undef, -        @_ -    ); +    my %args = @_; +    $args{'Object'} = delete $args{'TicketObj'}; +    my $ARGSRef = { %{ $args{'ARGSRef'} } }; -    my @results; +    # Build up a list of objects that we want to work with +    my %custom_fields_to_mod; +    foreach my $arg ( keys %$ARGSRef ) { +        if ( $arg =~ /^Ticket-(\d+-.*)/ ) { +            $ARGSRef->{"Object-RT::Ticket-$1"} = delete $ARGSRef->{$arg}; +        } elsif ( $arg =~ /^CustomField-(\d+-.*)/ ) { +            $ARGSRef->{"Object-RT::Ticket--$1"} = delete $ARGSRef->{$arg}; +        } +    } + +    return ProcessObjectCustomFieldUpdates( %args, ARGSRef => $ARGSRef ); +} +sub ProcessObjectCustomFieldUpdates { +    my %args    = @_;      my $ARGSRef = $args{'ARGSRef'}; +    my @results; -    # Build up a list of tickets that we want to work with -    my %tickets_to_mod; +    # Build up a list of objects that we want to work with      my %custom_fields_to_mod; -    foreach my $arg ( keys %{$ARGSRef} ) { -        if ( $arg =~ /^Ticket-(\d+)-CustomField-(\d+)-/ ) { +    foreach my $arg ( keys %$ARGSRef ) { + +        # format: Object-<object class>-<object id>-CustomField-<CF id>-<commands> +        next unless $arg =~ /^Object-([\w:]+)-(\d*)-CustomField-(\d+)-(.*)$/; + +        # For each of those objects, find out what custom fields we want to work with. +        $custom_fields_to_mod{$1}{ $2 || 0 }{$3}{$4} = $ARGSRef->{$arg}; +    } + +    # For each of those objects +    foreach my $class ( keys %custom_fields_to_mod ) { +        foreach my $id ( keys %{ $custom_fields_to_mod{$class} } ) { +            my $Object = $args{'Object'}; +            $Object = $class->new( $session{'CurrentUser'} ) +                unless $Object && ref $Object eq $class; + +            $Object->Load($id) unless ( $Object->id || 0 ) == $id; +            unless ( $Object->id ) { +                $RT::Logger->warning("Couldn't load object $class #$id"); +                next; +            } -            # For each of those tickets, find out what custom fields we want to work with. -            $custom_fields_to_mod{$1}{$2} = 1; +            foreach my $cf ( keys %{ $custom_fields_to_mod{$class}{$id} } ) { +                my $CustomFieldObj = RT::CustomField->new( $session{'CurrentUser'} ); +                $CustomFieldObj->LoadById($cf); +                unless ( $CustomFieldObj->id ) { +                    $RT::Logger->warning("Couldn't load custom field #$cf"); +                    next; +                } +                push @results, +                    _ProcessObjectCustomFieldUpdates( +                    Prefix      => "Object-$class-$id-CustomField-$cf-", +                    Object      => $Object, +                    CustomField => $CustomFieldObj, +                    ARGS        => $custom_fields_to_mod{$class}{$id}{$cf}, +                    ); +            }          }      } +    return @results; +} + +sub _ProcessObjectCustomFieldUpdates { +    my %args    = @_; +    my $cf      = $args{'CustomField'}; +    my $cf_type = $cf->Type; + +    # Remove blank Values since the magic field will take care of this. Sometimes +    # the browser gives you a blank value which causes CFs to be processed twice +    if (   defined $args{'ARGS'}->{'Values'} +        && !length $args{'ARGS'}->{'Values'} +        && $args{'ARGS'}->{'Values-Magic'} ) +    { +        delete $args{'ARGS'}->{'Values'}; +    } -    # For each of those tickets -    foreach my $tick ( keys %custom_fields_to_mod ) { -        my $Ticket = RT::Ticket->new( $session{'CurrentUser'} ); -        $Ticket->Load($tick); +    my @results; +    foreach my $arg ( keys %{ $args{'ARGS'} } ) { -        # For each custom field   -        foreach my $cf ( keys %{ $custom_fields_to_mod{$tick} } ) { +        # skip category argument +        next if $arg eq 'Category'; -	    my $CustomFieldObj = RT::CustomField->new($session{'CurrentUser'}); -	    $CustomFieldObj->LoadById($cf); +        # since http won't pass in a form element with a null value, we need +        # to fake it +        if ( $arg eq 'Values-Magic' ) { -            foreach my $arg ( keys %{$ARGSRef} ) { -                # since http won't pass in a form element with a null value, we need -                # to fake it -                if ($arg =~ /^(.*?)-Values-Magic$/ ) { -                    # We don't care about the magic, if there's really a values element; -                    next if (exists $ARGSRef->{$1.'-Values'}) ; +            # We don't care about the magic, if there's really a values element; +            next if defined $args{'ARGS'}->{'Value'}  && length $args{'ARGS'}->{'Value'}; +            next if defined $args{'ARGS'}->{'Values'} && length $args{'ARGS'}->{'Values'}; -                    $arg = $1."-Values"; -                    $ARGSRef->{$1."-Values"} = undef; -                 -                } -                next unless ( $arg =~ /^Ticket-$tick-CustomField-$cf-/ ); -                my @values = -                  ( ref( $ARGSRef->{$arg} ) eq 'ARRAY' )  -                  ? @{ $ARGSRef->{$arg} } -                  : ( $ARGSRef->{$arg} ); -                if ( ( $arg =~ /-AddValue$/ ) || ( $arg =~ /-Value$/ ) ) { -                    foreach my $value (@values) { -                        next unless ($value); -                        my ( $val, $msg ) = $Ticket->AddCustomFieldValue( -                            Field => $cf, -                            Value => $value -                        ); -                        push ( @results, $msg ); -                    } -                } -                elsif ( $arg =~ /-DeleteValues$/ ) { -                    foreach my $value (@values) { -                        next unless ($value); -                        my ( $val, $msg ) = $Ticket->DeleteCustomFieldValue( -                            Field => $cf, -                            Value => $value -                        ); -                        push ( @results, $msg ); -                    } -                } -                elsif ( $arg =~ /-Values$/ and $CustomFieldObj->Type !~ /Entry/) { -                    my $cf_values = $Ticket->CustomFieldValues($cf); - -                    my %values_hash; -                    foreach my $value (@values) { -                        next unless ($value); - -                        # build up a hash of values that the new set has -                        $values_hash{$value} = 1; - -                        unless ( $cf_values->HasEntry($value) ) { -                            my ( $val, $msg ) = $Ticket->AddCustomFieldValue( -                                Field => $cf, -                                Value => $value -                            ); -                            push ( @results, $msg ); -                        } - -                    } -                    while ( my $cf_value = $cf_values->Next ) { -                        unless ( $values_hash{ $cf_value->Content } == 1 ) { -                            my ( $val, $msg ) = $Ticket->DeleteCustomFieldValue( -                                Field => $cf, -                                Value => $cf_value->Content -                            ); -                            push ( @results, $msg); - -                        } - -                    } +            # "Empty" values does not mean anything for Image and Binary fields +            next if $cf_type =~ /^(?:Image|Binary)$/; + +            $arg = 'Values'; +            $args{'ARGS'}->{'Values'} = undef; +        } + +        my @values = (); +        if ( ref $args{'ARGS'}->{$arg} eq 'ARRAY' ) { +            @values = @{ $args{'ARGS'}->{$arg} }; +        } elsif ( $cf_type =~ /text/i ) {    # Both Text and Wikitext +            @values = ( $args{'ARGS'}->{$arg} ); +        } else { +            @values = split /\r*\n/, $args{'ARGS'}->{$arg} +                if defined $args{'ARGS'}->{$arg}; +        } +        @values = grep length, map { +            s/\r+\n/\n/g; +            s/^\s+//; +            s/\s+$//; +            $_; +            } +            grep defined, @values; + +        if ( $arg eq 'AddValue' || $arg eq 'Value' ) { +            foreach my $value (@values) { +                my ( $val, $msg ) = $args{'Object'}->AddCustomFieldValue( +                    Field => $cf->id, +                    Value => $value +                ); +                push( @results, $msg ); +            } +        } elsif ( $arg eq 'Upload' ) { +            my $value_hash = _UploadedFile( $args{'Prefix'} . $arg ) or next; +            my ( $val, $msg ) = $args{'Object'}->AddCustomFieldValue( %$value_hash, Field => $cf, ); +            push( @results, $msg ); +        } elsif ( $arg eq 'DeleteValues' ) { +            foreach my $value (@values) { +                my ( $val, $msg ) = $args{'Object'}->DeleteCustomFieldValue( +                    Field => $cf, +                    Value => $value, +                ); +                push( @results, $msg ); +            } +        } elsif ( $arg eq 'DeleteValueIds' ) { +            foreach my $value (@values) { +                my ( $val, $msg ) = $args{'Object'}->DeleteCustomFieldValue( +                    Field   => $cf, +                    ValueId => $value, +                ); +                push( @results, $msg ); +            } +        } elsif ( $arg eq 'Values' && !$cf->Repeated ) { +            my $cf_values = $args{'Object'}->CustomFieldValues( $cf->id ); + +            my %values_hash; +            foreach my $value (@values) { +                if ( my $entry = $cf_values->HasEntry($value) ) { +                    $values_hash{ $entry->id } = 1; +                    next;                  } -                elsif ( $arg =~ /-Values$/ ) { -                    my $cf_values = $Ticket->CustomFieldValues($cf); - -		    # keep everything up to the point of difference, delete the rest -		    my $delete_flag; -		    foreach my $old_cf (@{$cf_values->ItemsArrayRef}) { -			if (!$delete_flag and @values and $old_cf->Content eq $values[0]) { -			    shift @values; -			    next; -			} - -			$delete_flag ||= 1; -			$old_cf->Delete; -		    } - -		    # now add/replace extra things, if any -		    foreach my $value (@values) { -			my ( $val, $msg ) = $Ticket->AddCustomFieldValue( -			    Field => $cf, -			    Value => $value -			); -			push ( @results, $msg ); -		    } -		} -                else { -                    push ( @results, "User asked for an unknown update type for custom field " . $cf->Name . " for ticket " . $Ticket->id ); + +                my ( $val, $msg ) = $args{'Object'}->AddCustomFieldValue( +                    Field => $cf, +                    Value => $value +                ); +                push( @results, $msg ); +                $values_hash{$val} = 1 if $val; +            } + +            # For Date Cfs, @values is empty when there is no changes (no datas in form input) +            return @results if ( $cf->Type eq 'Date' && ! @values ); + +            $cf_values->RedoSearch; +            while ( my $cf_value = $cf_values->Next ) { +                next if $values_hash{ $cf_value->id }; + +                my ( $val, $msg ) = $args{'Object'}->DeleteCustomFieldValue( +                    Field   => $cf, +                    ValueId => $cf_value->id +                ); +                push( @results, $msg ); +            } +        } elsif ( $arg eq 'Values' ) { +            my $cf_values = $args{'Object'}->CustomFieldValues( $cf->id ); + +            # keep everything up to the point of difference, delete the rest +            my $delete_flag; +            foreach my $old_cf ( @{ $cf_values->ItemsArrayRef } ) { +                if ( !$delete_flag and @values and $old_cf->Content eq $values[0] ) { +                    shift @values; +                    next;                  } + +                $delete_flag ||= 1; +                $old_cf->Delete; +            } + +            # now add/replace extra things, if any +            foreach my $value (@values) { +                my ( $val, $msg ) = $args{'Object'}->AddCustomFieldValue( +                    Field => $cf, +                    Value => $value +                ); +                push( @results, $msg );              } +        } else { +            push( +                @results, +                loc("User asked for an unknown update type for custom field [_1] for [_2] object #[_3]", +                    $cf->Name, ref $args{'Object'}, +                    $args{'Object'}->id +                ) +            );          } -        return (@results);      } +    return @results;  } -# }}} -  # {{{ sub ProcessTicketWatchers  =head2 ProcessTicketWatchers ( TicketObj => $Ticket, ARGSRef => \%ARGS ); @@ -1180,29 +1782,31 @@ sub ProcessTicketWatchers {      my $Ticket  = $args{'TicketObj'};      my $ARGSRef = $args{'ARGSRef'}; -    # {{{ Munge watchers +    # Munge watchers      foreach my $key ( keys %$ARGSRef ) { -        # {{{ Delete deletable watchers -        if ( ( $key =~ /^Ticket-DelWatcher-Type-(.*)-Principal-(\d+)$/ )  ) { -            my ( $code, $msg ) =  -                $Ticket->DeleteWatcher(PrincipalId => $2, -                                       Type => $1); +        # Delete deletable watchers +        if ( $key =~ /^Ticket-DeleteWatcher-Type-(.*)-Principal-(\d+)$/ ) { +            my ( $code, $msg ) = $Ticket->DeleteWatcher( +                PrincipalId => $2, +                Type        => $1 +            );              push @results, $msg;          }          # Delete watchers in the simple style demanded by the bulk manipulator          elsif ( $key =~ /^Delete(Requestor|Cc|AdminCc)$/ ) { -            my ( $code, $msg ) = $Ticket->DeleteWatcher( Type => $ARGSRef->{$key}, PrincipalId => $1 ); +            my ( $code, $msg ) = $Ticket->DeleteWatcher( +                Email => $ARGSRef->{$key}, +                Type  => $1 +            );              push @results, $msg;          } -        # }}} - -        # Add new wathchers by email address       -        elsif ( ( $ARGSRef->{$key} =~ /^(AdminCc|Cc|Requestor)$/ ) -            and ( $key =~ /^WatcherTypeEmail(\d*)$/ ) ) +        # Add new wathchers by email address +        elsif ( ( $ARGSRef->{$key} || '' ) =~ /^(?:AdminCc|Cc|Requestor)$/ +            and $key =~ /^WatcherTypeEmail(\d*)$/ )          {              #They're in this order because otherwise $1 gets clobbered :/ @@ -1223,18 +1827,21 @@ sub ProcessTicketWatchers {          }          # Add new  watchers by owner -        elsif ( ( $ARGSRef->{$key} =~ /^(AdminCc|Cc|Requestor)$/ ) -            and ( $key =~ /^Ticket-AddWatcher-Principal-(\d*)$/ ) ) { +        elsif ( $key =~ /^Ticket-AddWatcher-Principal-(\d*)$/ ) { +            my $principal_id = $1; +            my $form         = $ARGSRef->{$key}; +            foreach my $value ( ref($form) ? @{$form} : ($form) ) { +                next unless $value =~ /^(?:AdminCc|Cc|Requestor)$/i; -            #They're in this order because otherwise $1 gets clobbered :/ -            my ( $code, $msg ) = -              $Ticket->AddWatcher( Type => $ARGSRef->{$key}, PrincipalId => $1 ); -            push @results, $msg; +                my ( $code, $msg ) = $Ticket->AddWatcher( +                    Type        => $value, +                    PrincipalId => $principal_id +                ); +                push @results, $msg; +            }          } -    } - -    # }}} +    }      return (@results);  } @@ -1262,33 +1869,33 @@ sub ProcessTicketDates {      # {{{ Set date fields      my @date_fields = qw( -      Told -      Resolved -      Starts -      Started -      Due +        Told +        Resolved +        Starts +        Started +        Due      );      #Run through each field in this list. update the value if apropriate      foreach my $field (@date_fields) { +        next unless exists $ARGSRef->{ $field . '_Date' }; +        next if $ARGSRef->{ $field . '_Date' } eq ''; +          my ( $code, $msg );          my $DateObj = RT::Date->new( $session{'CurrentUser'} ); +        $DateObj->Set( +            Format => 'unknown', +            Value  => $ARGSRef->{ $field . '_Date' } +        ); -        #If it's something other than just whitespace -        if ( $ARGSRef->{ $field . '_Date' } ne '' ) { -            $DateObj->Set( -                Format => 'unknown', -                Value  => $ARGSRef->{ $field . '_Date' } -            ); -            my $obj = $field . "Obj"; -            if ( ( defined $DateObj->Unix ) -                and ( $DateObj->Unix ne $Ticket->$obj()->Unix() ) ) -            { -                my $method = "Set$field"; -                my ( $code, $msg ) = $Ticket->$method( $DateObj->ISO ); -                push @results, "$msg"; -            } +        my $obj = $field . "Obj"; +        if (    ( defined $DateObj->Unix ) +            and ( $DateObj->Unix != $Ticket->$obj()->Unix() ) ) +        { +            my $method = "Set$field"; +            my ( $code, $msg ) = $Ticket->$method( $DateObj->ISO ); +            push @results, "$msg";          }      } @@ -1307,13 +1914,39 @@ Returns an array of results messages.  =cut  sub ProcessTicketLinks { -    my %args = ( TicketObj => undef, -                 ARGSRef   => undef, -                 @_ ); +    my %args = ( +        TicketObj => undef, +        ARGSRef   => undef, +        @_ +    );      my $Ticket  = $args{'TicketObj'};      my $ARGSRef = $args{'ARGSRef'}; +    my (@results) = ProcessRecordLinks( RecordObj => $Ticket, ARGSRef => $ARGSRef ); + +    #Merge if we need to +    if ( $ARGSRef->{ $Ticket->Id . "-MergeInto" } ) { +        $ARGSRef->{ $Ticket->Id . "-MergeInto" } =~ s/\s+//g; +        my ( $val, $msg ) = $Ticket->MergeInto( $ARGSRef->{ $Ticket->Id . "-MergeInto" } ); +        push @results, $msg; +    } + +    return (@results); +} + +# }}} + +sub ProcessRecordLinks { +    my %args = ( +        RecordObj => undef, +        ARGSRef   => undef, +        @_ +    ); + +    my $Record  = $args{'RecordObj'}; +    my $ARGSRef = $args{'ARGSRef'}; +      my (@results);      # Delete links that are gone gone gone. @@ -1323,11 +1956,11 @@ sub ProcessTicketLinks {              my $type   = $2;              my $target = $3; -            push @results, -              "Trying to delete: Base: $base Target: $target  Type $type"; -            my ( $val, $msg ) = $Ticket->DeleteLink( Base   => $base, -                                                     Type   => $type, -                                                     Target => $target ); +            my ( $val, $msg ) = $Record->DeleteLink( +                Base   => $base, +                Type   => $type, +                Target => $target +            );              push @results, $msg; @@ -1338,40 +1971,138 @@ sub ProcessTicketLinks {      my @linktypes = qw( DependsOn MemberOf RefersTo );      foreach my $linktype (@linktypes) { -        if ( $ARGSRef->{ $Ticket->Id . "-$linktype" } ) { -            for my $luri ( split ( / /, $ARGSRef->{ $Ticket->Id . "-$linktype" } ) ) { -                $luri =~ s/\s*$//;    # Strip trailing whitespace -                my ( $val, $msg ) = $Ticket->AddLink( Target => $luri, -                                                      Type   => $linktype ); +        if ( $ARGSRef->{ $Record->Id . "-$linktype" } ) { +            $ARGSRef->{ $Record->Id . "-$linktype" } = join( ' ', @{ $ARGSRef->{ $Record->Id . "-$linktype" } } ) +                if ref( $ARGSRef->{ $Record->Id . "-$linktype" } ); + +            for my $luri ( split( / /, $ARGSRef->{ $Record->Id . "-$linktype" } ) ) { +                next unless $luri; +                $luri =~ s/\s+$//;    # Strip trailing whitespace +                my ( $val, $msg ) = $Record->AddLink( +                    Target => $luri, +                    Type   => $linktype +                );                  push @results, $msg;              }          } -        if ( $ARGSRef->{ "$linktype-" . $Ticket->Id } ) { - -            for my $luri ( split ( / /, $ARGSRef->{ "$linktype-" . $Ticket->Id } ) ) { -                my ( $val, $msg ) = $Ticket->AddLink( Base => $luri, -                                                      Type => $linktype ); +        if ( $ARGSRef->{ "$linktype-" . $Record->Id } ) { +            $ARGSRef->{ "$linktype-" . $Record->Id } = join( ' ', @{ $ARGSRef->{ "$linktype-" . $Record->Id } } ) +                if ref( $ARGSRef->{ "$linktype-" . $Record->Id } ); + +            for my $luri ( split( / /, $ARGSRef->{ "$linktype-" . $Record->Id } ) ) { +                next unless $luri; +                my ( $val, $msg ) = $Record->AddLink( +                    Base => $luri, +                    Type => $linktype +                );                  push @results, $msg;              } -        }  +        }      } -    #Merge if we need to -    if ( $ARGSRef->{ $Ticket->Id . "-MergeInto" } ) { -        my ( $val, $msg ) = -          $Ticket->MergeInto( $ARGSRef->{ $Ticket->Id . "-MergeInto" } ); -        push @results, $msg; +    return (@results); +} + +=head2 _UploadedFile ( $arg ); + +Takes a CGI parameter name; if a file is uploaded under that name, +return a hash reference suitable for AddCustomFieldValue's use: +C<( Value => $filename, LargeContent => $content, ContentType => $type )>. + +Returns C<undef> if no files were uploaded in the C<$arg> field. + +=cut + +sub _UploadedFile { +    my $arg         = shift; +    my $cgi_object  = $m->cgi_object; +    my $fh          = $cgi_object->upload($arg) or return undef; +    my $upload_info = $cgi_object->uploadInfo($fh); + +    my $filename = "$fh"; +    $filename =~ s#^.*[\\/]##; +    binmode($fh); + +    return { +        Value        => $filename, +        LargeContent => do { local $/; scalar <$fh> }, +        ContentType  => $upload_info->{'Content-Type'}, +    }; +} + +sub GetColumnMapEntry { +    my %args = ( Map => {}, Name => '', Attribute => undef, @_ ); + +    # deal with the simplest thing first +    if ( $args{'Map'}{ $args{'Name'} } ) { +        return $args{'Map'}{ $args{'Name'} }{ $args{'Attribute'} };      } -    return (@results); +    # complex things +    elsif ( my ( $mainkey, $subkey ) = $args{'Name'} =~ /^(.*?)\.{(.+)}$/ ) { +        return undef unless $args{'Map'}->{$mainkey}; +        return $args{'Map'}{$mainkey}{ $args{'Attribute'} } +            unless ref $args{'Map'}{$mainkey}{ $args{'Attribute'} } eq 'CODE'; + +        return sub { $args{'Map'}{$mainkey}{ $args{'Attribute'} }->( @_, $subkey ) }; +    } +    return undef;  } -# }}} +sub ProcessColumnMapValue { +    my $value = shift; +    my %args = ( Arguments => [], Escape => 1, @_ ); + +    if ( ref $value ) { +        if ( UNIVERSAL::isa( $value, 'CODE' ) ) { +            my @tmp = $value->( @{ $args{'Arguments'} } ); +            return ProcessColumnMapValue( ( @tmp > 1 ? \@tmp : $tmp[0] ), %args ); +        } elsif ( UNIVERSAL::isa( $value, 'ARRAY' ) ) { +            return join '', map ProcessColumnMapValue( $_, %args ), @$value; +        } elsif ( UNIVERSAL::isa( $value, 'SCALAR' ) ) { +            return $$value; +        } +    } + +    return $m->interp->apply_escapes( $value, 'h' ) if $args{'Escape'}; +    return $value; +} + +=head2 _load_container_object ( $type, $id ); + +Instantiate container object for saving searches. + +=cut + +sub _load_container_object { +    my ( $obj_type, $obj_id ) = @_; +    return RT::SavedSearch->new( $session{'CurrentUser'} )->_load_privacy_object( $obj_type, $obj_id ); +} + +=head2 _parse_saved_search ( $arg ); + +Given a serialization string for saved search, and returns the +container object and the search id. + +=cut + +sub _parse_saved_search { +    my $spec = shift; +    return unless $spec; +    if ( $spec !~ /^(.*?)-(\d+)-SavedSearch-(\d+)$/ ) { +        return; +    } +    my $obj_type  = $1; +    my $obj_id    = $2; +    my $search_id = $3; + +    return ( _load_container_object( $obj_type, $obj_id ), $search_id ); +}  eval "require RT::Interface::Web_Vendor"; -die $@ if ($@ && $@ !~ qr{^Can't locate RT/Interface/Web_Vendor.pm}); +die $@ if ( $@ && $@ !~ qr{^Can't locate RT/Interface/Web_Vendor.pm} );  eval "require RT::Interface::Web_Local"; -die $@ if ($@ && $@ !~ qr{^Can't locate RT/Interface/Web_Local.pm}); +die $@ if ( $@ && $@ !~ qr{^Can't locate RT/Interface/Web_Local.pm} );  1; diff --git a/rt/lib/RT/Record.pm b/rt/lib/RT/Record.pm index bee94420d..78bbe915d 100755 --- a/rt/lib/RT/Record.pm +++ b/rt/lib/RT/Record.pm @@ -1744,6 +1744,25 @@ sub _AddCustomFieldValue {          }          my $new_content = $new_value->Content; + +        # For date, we need to display them in "human" format in result message +        if ($cf->Type eq 'Date') { +            my $DateObj = new RT::Date( $self->CurrentUser ); +            $DateObj->Set( +                Format => 'ISO', +                Value  => $new_content, +            ); +            $new_content = $DateObj->AsString; + +            if ( defined $old_content && length $old_content ) { +                $DateObj->Set( +                    Format => 'ISO', +                    Value  => $old_content, +                ); +                $old_content = $DateObj->AsString; +            } +        } +          unless ( defined $old_content && length $old_content ) {              return ( $new_value_id, $self->loc( "[_1] [_2] added", $cf->Name, $new_content ));          } @@ -1832,11 +1851,21 @@ sub DeleteCustomFieldValue {          return ( 0, $self->loc( "Couldn't create a transaction: [_1]", $Msg ) );      } +    my $old_value = $TransactionObj->OldValue; +    # For date, we need to display them in "human" format in result message +    if ( $cf->Type eq 'Date' ) { +        my $DateObj = new RT::Date( $self->CurrentUser ); +        $DateObj->Set( +            Format => 'ISO', +            Value  => $old_value, +        ); +        $old_value = $DateObj->AsString; +    }      return (          $TransactionId,          $self->loc(              "[_1] is no longer a value for custom field [_2]", -            $TransactionObj->OldValue, $cf->Name +            $old_value, $cf->Name          )      );  } diff --git a/rt/lib/RT/Tickets_Overlay.pm b/rt/lib/RT/Tickets_Overlay.pm index e8d350dea..0d3264c4b 100644 --- a/rt/lib/RT/Tickets_Overlay.pm +++ b/rt/lib/RT/Tickets_Overlay.pm @@ -136,6 +136,7 @@ our %FIELD_METADATA = (      QueueAdminCc     => [ 'WATCHERFIELD'    => 'AdminCc' => 'Queue', ], #loc_left_pair      QueueWatcher     => [ 'WATCHERFIELD'    => undef     => 'Queue', ], #loc_left_pair      CustomFieldValue => [ 'CUSTOMFIELD', ], #loc_left_pair +    DateCustomFieldValue => [ 'DATECUSTOMFIELD', ],      CustomField      => [ 'CUSTOMFIELD', ], #loc_left_pair      CF               => [ 'CUSTOMFIELD', ], #loc_left_pair      Updated          => [ 'TRANSDATE', ], #loc_left_pair @@ -160,6 +161,7 @@ our %dispatch = (      WATCHERFIELD    => \&_WatcherLimit,      MEMBERSHIPFIELD => \&_WatcherMembershipLimit,      CUSTOMFIELD     => \&_CustomFieldLimit, +    DATECUSTOMFIELD => \&_DateCustomFieldLimit,      HASATTRIBUTE    => \&_HasAttributeLimit,  );  our %can_bundle = ();# WATCHERFIELD => "yes", ); @@ -1340,6 +1342,101 @@ sub _CustomFieldJoin {      return ($TicketCFs, $CFs);  } +=head2 _DateCustomFieldLimit + +Limit based on CustomFields of type Date + +Meta Data: +  none + +=cut + +sub _DateCustomFieldLimit { +    my ( $self, $_field, $op, $value, %rest ) = @_; + +    my $field = $rest{'SUBKEY'} || die "No field specified"; + +    # For our sanity, we can only limit on one queue at a time + +    my ($queue, $cfid, $column); +    ($queue, $field, $cfid, $column) = $self->_CustomFieldDecipher( $field ); + +# If we're trying to find custom fields that don't match something, we +# want tickets where the custom field has no value at all.  Note that +# we explicitly don't include the "IS NULL" case, since we would +# otherwise end up with a redundant clause. + +    my $null_columns_ok; +    if ( ( $op =~ /^NOT LIKE$/i ) or ( $op eq '!=' ) ) { +        $null_columns_ok = 1; +    } + +    my $cfkey = $cfid ? $cfid : "$queue.$field"; +    my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field ); + +    $self->_OpenParen; + +    if ( $CFs && !$cfid ) { +        $self->SUPER::Limit( +            ALIAS           => $CFs, +            FIELD           => 'Name', +            VALUE           => $field, +            ENTRYAGGREGATOR => 'AND', +        ); +    } + +    $self->_OpenParen if $null_columns_ok; + +    my $date = RT::Date->new( $self->CurrentUser ); +    $date->Set( Format => 'unknown', Value => $value ); + +    if ( $op eq "=" ) { + +        # if we're specifying =, that means we want everything on a +        # particular single day.  in the database, we need to check for > +        # and < the edges of that day. + +        $date->SetToMidnight( Timezone => 'server' ); +        my $daystart = $date->ISO; +        $date->AddDay; +        my $dayend = $date->ISO; + +        $self->_OpenParen; + +        $self->_SQLLimit( +            ALIAS    => $TicketCFs, +            FIELD    => 'Content', +            OPERATOR => ">=", +            VALUE    => $daystart, +            %rest, +        ); + +        $self->_SQLLimit( +            ALIAS    => $TicketCFs, +            FIELD    => 'Content', +            OPERATOR => "<=", +            VALUE    => $dayend, +            %rest, +            ENTRYAGGREGATOR => 'AND', +        ); + +        $self->_CloseParen; + +    } +    else { +        $self->_SQLLimit( +            ALIAS    => $TicketCFs, +            FIELD    => 'Content', +            OPERATOR => $op, +            VALUE    => $date->ISO, +            %rest, +        ); +    } + +    $self->_CloseParen; + +} +  =head2 _CustomFieldLimit  Limit based on CustomFields @@ -2667,6 +2764,11 @@ sub LimitCustomField {          $args{CUSTOMFIELD} = $CF->Id;      } +    # Handle special customfields types +    if ($CF->Type eq 'Date') { +        $args{FIELD} = 'DateCustomFieldValue'; +    } +      #If we are looking to compare with a null value.      if ( $args{'OPERATOR'} =~ /^is$/i ) {          $args{'DESCRIPTION'} diff --git a/rt/share/html/Elements/EditCustomFieldDate b/rt/share/html/Elements/EditCustomFieldDate new file mode 100644 index 000000000..9df469f9d --- /dev/null +++ b/rt/share/html/Elements/EditCustomFieldDate @@ -0,0 +1,62 @@ +%# BEGIN BPS TAGGED BLOCK {{{ +%#  +%# COPYRIGHT: +%#   +%# This software is Copyright (c) 1996-2008 Best Practical Solutions, LLC  +%#                                          <jesse@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/copyleft/gpl.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 }}} +% my $name = $NamePrefix.$CustomField->Id.'-Values'; +<& /Elements/SelectDate, Name => "$name", current => 0 &> (<%$DateObj->AsString%>) + +<%INIT> +my $DateObj = new RT::Date ( $session{'CurrentUser'} ); +$DateObj->Set( Format => 'ISO', Value => $Default ); +</%INIT> +<%ARGS> +$Object => undef +$CustomField => undef +$NamePrefix => undef +$Default => undef +$Values => undef +$MaxValues => 1 +</%ARGS> diff --git a/rt/share/html/Elements/ShowCustomFieldDate b/rt/share/html/Elements/ShowCustomFieldDate new file mode 100644 index 000000000..4e8ad676c --- /dev/null +++ b/rt/share/html/Elements/ShowCustomFieldDate @@ -0,0 +1,57 @@ +%# BEGIN BPS TAGGED BLOCK {{{ +%#  +%# COPYRIGHT: +%#   +%# This software is Copyright (c) 1996-2008 Best Practical Solutions, LLC  +%#                                          <jesse@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 }}} +<%INIT> + my $content = $Object->Content; + my $DateObj = new RT::Date ( $session{'CurrentUser'} ); + $DateObj->Set( Format => 'ISO', Value => $content ); + $content = $DateObj->AsString; +</%INIT> +<%$content|n%> +<%ARGS> +$Object +</%ARGS> diff --git a/rt/share/html/Search/Build.html b/rt/share/html/Search/Build.html index c5067d5d1..f347acd79 100644 --- a/rt/share/html/Search/Build.html +++ b/rt/share/html/Search/Build.html @@ -188,7 +188,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.{.*?})$/                  && ( ref $ARGS{$arg} eq "ARRAY"                       ? grep $_ ne '', @{ $ARGS{$arg} }                       : $ARGS{$arg} ne '' ); diff --git a/rt/share/html/Search/Elements/PickCFs b/rt/share/html/Search/Elements/PickCFs index ba25cdeda..3f6d188b8 100644 --- a/rt/share/html/Search/Elements/PickCFs +++ b/rt/share/html/Search/Elements/PickCFs @@ -76,22 +76,43 @@ $m->callback(  my @lines;  while ( my $CustomField = $CustomFields->Next ) {      my %line; -    $line{'Name'} = "'CF.{" . $CustomField->Name . "}'"; +    $line{'Name'} = "CF.{" . $CustomField->Name . "}";      $line{'Field'} = $CustomField->Name; -    $line{'Op'} = { -        Type => 'component', -        Path => '/Elements/SelectCustomFieldOperator', -        Arguments => { True => loc("is"), -                       False => loc("isn't"), -                       TrueVal=> '=', -                       FalseVal => '!=', -                     }, -    }; -    $line{'Value'} = { -        Type => 'component', -        Path => '/Elements/SelectCustomFieldValue', -        Arguments => { CustomField => $CustomField }, -    }; + +    # Op +    if ($CustomField->Type eq 'Date') { +        $line{'Op'} = { +            Type => 'component', +            Path => '/Elements/SelectDateRelation', +            Arguments => {}, +        }; +    } else { +        $line{'Op'} = { +            Type => 'component', +            Path => '/Elements/SelectCustomFieldOperator', +            Arguments => { True => loc("is"), +                           False => loc("isn't"), +                           TrueVal=> '=', +                           FalseVal => '!=', +                         }, +        }; +    } + +    # Value +    if ($CustomField->Type eq 'Date') { +        $line{'Value'} = { +            Type => 'component', +            Path => '/Elements/SelectDate', +            Arguments => {}, +        }; +    } else { +        $line{'Value'} = { +            Type => 'component', +            Path => '/Elements/SelectCustomFieldValue', +            Arguments => { CustomField => $CustomField }, +        }; +    } +      push @lines, \%line;  }  | 
