if (@ARGV == 3) {
my ($from, $rel, $to) = @ARGV;
- if ($from !~ /^\d+$/ || $to !~ /^\d+$/) {
- my $bad = $from =~ /^\d+$/ ? $to : $from;
- whine "Invalid $type ID '$bad' specified.";
- $bad = 1;
- }
if (($type eq "ticket") && ( ! exists $ltypes{lc $rel})) {
whine "Invalid link '$rel' for type $type specified.";
$bad = 1;
}
print "$k->{Content}\n" if exists $k->{Content} and
$k->{Content} !~ /to have no content$/ and
- $k->{Type} ne 'EmailRecord';
+ ($k->{Type}||'') ne 'EmailRecord';
print "$k->{Attachments}\n" if exists $k->{Attachments} and
$k->{Attachments};
}
use strict;
use Carp;
-use lib '/www/data/rt/rt-perl/current-perl10/share/perl5';
-use lib '/www/data/rt/rt-perl/current-perl10/lib/perl5';
-use lib '/www/data/rt/rt-perl/current-perl10/lib64/perl5';
-
# fix lib paths, some may be relative
BEGIN {
require File::Spec;
my $r = shift;
return if $r->is_success;
- # This ordinarily oughtn't to be able to happen, suggests a bug in RT.
- # So only load these heavy modules when they're needed.
- require HTML::TreeBuilder;
- require HTML::FormatText;
-
- my $error = $r->error_as_HTML;
- my $tree = HTML::TreeBuilder->new->parse($error);
- $tree->eof;
-
- # It'll be a cold day in hell before RT sends out bounces in HTML
- my $formatter =
- HTML::FormatText->new( leftmargin => 0,
- rightmargin => 50, );
- print STDERR $formatter->format($tree);
+ # XXX TODO 4.2: Remove the multi-line error strings in favor of something more concise
+ print STDERR <<" ERROR";
+An Error Occurred
+=================
+
+@{[ $r->status_line ]}
+ ERROR
print STDERR "\n$0: undefined server error\n" if $opts->{'debug'};
return $self->tempfail();
}
database level.
*******
+
+UPGRADING FROM 4.0.5 and earlier - Changes:
+
+The fix for an attribute truncation bug on MySQL requires a small ALTER TABLE.
+Be sure you run `make upgrade-database` to apply this change automatically.
+The bug primarily manifested when uploading large logos in the theme editor on
+MySQL. Refer to etc/upgrade/4.0.6/schema.mysql for the actual ALTER TABLE that
+will be run.
+
+*******
+The web-based query builder now uses Queue limits to restrict the set of
+displayed statuses and owners. As part of this change, the %cfqueues
+parameter was renamed to %Queues; if you have local modifications to any
+of the following Mason templates, this feature will not function
+correctly:
+
+ share/html/Elements/SelectOwner
+ share/html/Elements/SelectStatus
+ share/html/Prefs/Search.html
+ share/html/Search/Build.html
+ share/html/Search/Elements/BuildFormatString
+ share/html/Search/Elements/PickCFs
+ share/html/Search/Elements/PickCriteria
make test-parallel
-The C<*-trunk> and C<master> branches are expected to be passing always
-be passing all tests. While it is acceptable to break tests in an
-intermediate commit, a branch which does not pass tests will not be
-merged. Ideally, commits which fix a bug should also include a testcase
-which fails before the fix and succeeds after.
+The C<*-trunk> and C<master> branches are expected to always be passing
+all tests. While it is acceptable to break tests in an intermediate
+commit, a branch which does not pass tests will not be merged. Ideally,
+commits which fix a bug should also include a testcase which fails
+before the fix and succeeds after.
More information is available at L<http://bestpractical.com/security/>.
+
+=head2 RT's security process
+
+After a security vulnerability is reported to Best Practical and
+verified, we attempt to resolve it in as timely a fashion as possible.
+Best Practical support customers will be notified before we disclose the
+information to the public. All security announcements will be sent to
+C<rt-announce@bestpractical.com>, which includes
+C<rt-users@bestpractical.com> and C<rt-devel@bestpractical.com>.
+
+As the tests for security vulnerabilities are often nearly identical to
+working exploits, sensitive tests will be embargoed for a period of six
+months before being added to the public RT repository.
+
+
=head2 Security tips for running RT
=over
=head3 mod_fcgid
+B<WARNING>: Before mod_fcgid 2.3.6, the maximum request size was 1GB.
+Starting in 2.3.6, this is now 128Kb. This is unlikely to be large
+enough for any RT install that handles attachments. You can read more
+about FcgidMaxRequestLen at
+L<http://httpd.apache.org/mod_fcgid/mod/mod_fcgid.html#fcgidmaxrequestlen>
+
+Most distributions will have a mod_fcgid.conf or similar file with
+mod_fcgid configurations and you should add:
+
+ FcgidMaxRequestLen 1073741824
+
+to return to the old default.
+
<VirtualHost rt.example.com>
### Optional apache logs for RT
# Ensure that your log rotation scripts know about these files
#
# RT was configured with:
#
-# $ ./configure --prefix=/www/data/rt/4.0.5/ --with-web-user=httpd --with-web-group=httpd --with-rt-group=uio-rt --with-apachectl=/www/sbin/apachectl --with-db-type=Pg --with-db-dba=postgres --disable-gpg
+# $ ./configure --prefix=/www/var/rt/ --with-web-user=httpd --with-web-group=httpd --with-rt-group=uio-rt --with-apachectl=/www/sbin/apachectl --with-db-type=Pg --with-db-dba=postgres --disable-gpg
#
package RT;
C<$MaxAttachmentSize> sets the maximum size (in bytes) of attachments
stored in the database.
-For MySQL and Oracle, we set this size to 10 megabytes. If you're
-running a PostgreSQL version earlier than 7.1, you will need to drop
-this to 8192. (8k)
-
=cut
-
Set($MaxAttachmentSize, 10_000_000);
=item C<$TruncateLongAttachments>
A list of JavaScript files to be included in head. Removing any of
the default entries is not suggested.
+If you're a plugin author, refer to RT->AddJavaScript.
+
=cut
Set(@JSFiles, qw/
A list of additional CSS files to be included in head.
+If you're a plugin author, refer to RT->AddStyleSheets.
+
=cut
Set(@CSSFiles, qw//);
If set to a false value, RT will allow the user to log in from any link
or request, merely by passing in C<user> and C<pass> parameters; setting
it to a true value forces all logins to come from the login box, so the
-user us aware that they are being logged in. The default is off, for
+user is aware that they are being logged in. The default is off, for
backwards compatability.
=cut
Set($RestrictLoginReferrer, 0);
+=item C<$ReferrerWhitelist>
+
+This is a list of hostname:port combinations that RT will treat as being
+part of RT's domain. This is particularly useful if you access RT as
+multiple hostnames or have an external auth system that needs to
+redirect back to RT once authentication is complete.
+
+ Set(@ReferrerWhitelist, qw(www.example.com:443 www3.example.com:80));
+
+If the "RT has detected a possible cross-site request forgery" error is triggered
+by a host:port sent by your browser that you believe should be valid, you can copy
+the host:port from the error message into this list.
+
+=cut
+
+Set(@ReferrerWhitelist, qw());
+
=back
=item C<$WebSessionClass>
-C<$WebSessionClass> is the class you wish to use for managing
-Sessions. It defaults to use your SQL database, but if you are using
-MySQL 3.x and plans to use non-ascii Queue names, uncomment and add
-this line to F<RT_SiteConfig.pm> to prevent session corruption.
+C<$WebSessionClass> is the class you wish to use for managing sessions.
+It defaults to use your SQL database, except on Oracle, where it
+defaults to files on disk.
=cut
When an approval is denied, the status of depending tickets will
be changed to this value.
+=item reminder_on_open
+
+When a reminder is opened, the status will be changed to this value.
+
+=item reminder_on_resolve
+
+When a reminder is resolved, the status will be changed to this value.
+
=back
=head2 Transitions between statuses and UI actions
on_merge => 'resolved',
approved => 'open',
denied => 'rejected',
+ reminder_on_open => 'open',
+ reminder_on_resolve => 'resolved',
},
transitions => {
defaults => {
on_create => 'new',
on_merge => 'resolved',
+ reminder_on_open => 'open',
+ reminder_on_resolve => 'resolved',
},
transitions => {
Queues =>
q{'<a href="__WebPath__/Admin/Queues/Modify.html?id=__id__">__id__</a>/TITLE:#'}
.q{,'<a href="__WebPath__/Admin/Queues/Modify.html?id=__id__">__Name__</a>/TITLE:Name'}
- .q{,__Description__,__Address__,__Priority__,__DefaultDueIn__,__Disabled__},
+ .q{,__Description__,__Address__,__Priority__,__DefaultDueIn__,'__Disabled__,__Lifecycle__},
Groups =>
q{'<a href="__WebPath__/Admin/Groups/Modify.html?id=__id__">__id__</a>/TITLE:#'}
Set($Framebusting, 1);
+=item C<$RestrictReferrer>
+
+If set to a false value, the HTTP C<Referer> (sic) header will not be
+checked to ensure that requests come from RT's own domain. As RT allows
+for GET requests to alter state, disabling this opens RT up to
+cross-site request forgery (CSRF) attacks.
+
+=cut
+
+Set($RestrictReferrer, 1);
+
+=item C<$RestrictLoginReferrer>
+
+If set to a false value, RT will allow the user to log in from any link
+or request, merely by passing in C<user> and C<pass> parameters; setting
+it to a true value forces all logins to come from the login box, so the
+user us aware that they are being logged in. The default is off, for
+backwards compatability.
+
+=cut
+
+Set($RestrictLoginReferrer, 0);
+
=back
+
+
=head1 Authorization and user configuration
=over 4
id INTEGER NOT NULL AUTO_INCREMENT,
Name varchar(255) NULL ,
Description varchar(255) NULL ,
- Content BLOB,
+ Content LONGBLOB,
ContentType varchar(16) CHARACTER SET ascii,
ObjectType varchar(64) CHARACTER SET ascii,
ObjectId integer, # foreign key to anything
require RT::Approval;
require RT::Lifecycle;
require RT::Link;
+ require RT::Links;
require RT::Article;
require RT::Articles;
require RT::Class;
=head2 AddJavaScript
helper method to add js files to C<JSFiles> config.
-to add extra css files, you can add the following line
+to add extra js files, you can add the following line
in the plugin's main file:
RT->AddJavaScript( 'foo.js', 'bar.js' );
$MIMEObj->head->delete('RT-Attach-Message');
- my $attachments = RT::Attachments->new( $self->TransactionObj->CreatorObj );
+ my $attachments = RT::Attachments->new( RT->SystemUser );
$attachments->Limit(
FIELD => 'TransactionId',
VALUE => $self->TransactionObj->Id
}
}
},
+ ReferrerWhitelist => { Type => 'ARRAY' },
ResolveDefaultUpdateType => {
PostLoadCheck => sub {
my $self = shift;
$content = HTML::RewriteAttributes::Links->rewrite(
$content,
- RT->Config->Get('WebURL') . '/Dashboards/Render.html',
+ RT->Config->Get('WebURL') . 'Dashboards/Render.html',
);
$self->EmailDashboard(
use warnings;
use strict;
-our $VERSION = '4.0.5';
+our $VERSION = '4.0.6';
my $self = shift;
my $datafile = shift;
my $root_password = shift;
+ my %args = (
+ disconnect_after => 1,
+ @_
+ );
# Slurp in stuff to insert from the datafile. Possible things to go in here:-
our (@Groups, @Users, @ACL, @Queues, @ScripActions, @ScripConditions,
$RT::Logger->debug("done.");
}
- my $db_type = RT->Config->Get('DatabaseType');
- $RT::Handle->Disconnect() unless $db_type eq 'SQLite';
+ # XXX: This disconnect doesn't really belong here; it's a relict from when
+ # this method was extracted from rt-setup-database. However, too much
+ # depends on it to change without significant testing. At the very least,
+ # we can provide a way to skip the side-effect.
+ if ( $args{disconnect_after} ) {
+ my $db_type = RT->Config->Get('DatabaseType');
+ $RT::Handle->Disconnect() unless $db_type eq 'SQLite';
+ }
$RT::Logger->debug("Done setting up database content.");
my $head = $entity->head;
- # convert at least MIME word encoded attachment filename
- foreach my $attr (qw(content-type.name content-disposition.filename)) {
- if ( my $name = $head->mime_attr($attr) and !$preserve_words ) {
- $head->mime_attr( $attr => DecodeMIMEWordsToUTF8($name) );
- }
- }
-
# If this is a textual entity, we'd need to preserve its original encoding
$head->replace( "X-RT-Original-Encoding" => $charset )
if $head->mime_attr('content-type.charset') or IsTextualContentType($head->mime_type);
my $to_charset = _CanonicalizeCharset(shift);
my $field = shift || '';
- my @list = $str =~ m/(.*?)=\?([^?]+)\?([QqBb])\?([^?]+)\?=([^=]*)/gcs;
+ # handle filename*=ISO-8859-1''%74%E9%73%74%2E%74%78%74, parameter value
+ # continuations, and similar syntax from RFC 2231
+ if ($field =~ /^Content-(Type|Disposition)/i) {
+ # This concatenates continued parameters and normalizes encoded params
+ # to QB encoded-words which we handle below
+ $str = MIME::Field::ParamVal->parse($str)->stringify;
+ }
+
+ # XXX TODO: use decode('MIME-Header', ...) and Encode::Alias to replace our
+ # custom MIME word decoding and charset canonicalization. We can't do this
+ # until we parse before decode, instead of the other way around.
+ my @list = $str =~ m/(.*?) # prefix
+ =\? # =?
+ ([^?]+?) # charset
+ (?:\*[^?]+)? # optional '*language'
+ \? # ?
+ ([QqBb]) # encoding
+ \? # ?
+ ([^?]+) # encoded string
+ \?= # ?=
+ ([^=]*) # trailing
+ /xgcs;
if ( @list ) {
# add everything that hasn't matched to the end of the latest
}
}
-# handle filename*=ISO-8859-1''%74%E9%73%74%2E%74%78%74, see also rfc 2231
- @list = $str =~ m/(.*?\*=)([^']*?)'([^']*?)'(\S+)(.*?)(?=(?:\*=|$))/gcs;
- if (@list) {
- $str = '';
- while (@list) {
- my ( $prefix, $charset, $language, $enc_str, $trailing ) =
- splice @list, 0, 5;
- $prefix =~ s/\*=$/=/; # remove the *
- $charset = _CanonicalizeCharset($charset);
- $enc_str =~ s/%(\w{2})/chr hex $1/eg;
- unless ( $charset eq $to_charset ) {
- Encode::from_to( $enc_str, $charset, $to_charset );
- }
- $enc_str = qq{"$enc_str"}
- if $enc_str =~ /[,;]/
- and $enc_str !~ /^".*"$/
- and (!$field || $field =~ /^(?:To$|From$|B?Cc$|Content-)/i);
- $str .= $prefix . $enc_str . $trailing;
- }
- }
-
# We might have \n without trailing whitespace, which will result in
# invalid headers.
$str =~ s/\n//g;
sub ConfigFile {
require File::Spec;
- return File::Spec->catfile( $RT::EtcPath, 'RT_SiteConfig.pm' );
+ return $ENV{RT_SITE_CONFIG} || File::Spec->catfile( $RT::EtcPath, 'RT_SiteConfig.pm' );
}
sub SaveConfig {
$mail->add_part( $entity );
my $from;
- my $subject = '';
- $subject = $txn->Subject if $txn;
- $subject ||= $ticket->Subject if $ticket;
+ unless (defined $mail->head->get('Subject')) {
+ my $subject = '';
+ $subject = $txn->Subject if $txn;
+ $subject ||= $ticket->Subject if $ticket;
+
+ unless ( RT->Config->Get('ForwardFromUser') ) {
+ # XXX: what if want to forward txn of other object than ticket?
+ $subject = AddSubjectTag( $subject, $ticket );
+ }
- unless ( RT->Config->Get('ForwardFromUser') ) {
- # XXX: what if want to forward txn of other object than ticket?
- $subject = AddSubjectTag( $subject, $ticket );
+ $mail->head->set( Subject => EncodeToMIME( String => "Fwd: $subject" ) );
}
- $mail->head->set( Subject => EncodeToMIME( String => "Fwd: $subject" ) );
$mail->head->set(
From => EncodeToMIME(
String => GetForwardFrom( Transaction => $txn, Ticket => $ticket )
ValidateWebConfig();
DecodeARGS($ARGS);
+ local $HTML::Mason::Commands::DECODED_ARGS = $ARGS;
PreprocessTimeUpdates($ARGS);
InitializeMenu();
my $m = $HTML::Mason::Commands::m;
+ # Ensure that the cookie that we send is up-to-date, in case the
+ # session-id has been modified in any way
+ SendSessionCookie();
+
# precache all system level rights for the current user
$HTML::Mason::Commands::session{CurrentUser}->PrincipalObj->HasRights( Object => RT->System );
InstantiateNewSession();
$HTML::Mason::Commands::session{'CurrentUser'} = $user_obj;
- SendSessionCookie();
$m->callback( %$ARGS, CallbackName => 'SuccessfulLogin', CallbackPage => '/autohandler' );
sub InstantiateNewSession {
tied(%HTML::Mason::Commands::session)->delete if tied(%HTML::Mason::Commands::session);
tie %HTML::Mason::Commands::session, 'RT::Interface::Web::Session', undef;
+ SendSessionCookie();
}
sub SendSessionCookie {
return @roots;
}
-my %is_whitelisted_path = (
+our %is_whitelisted_component = (
# The RSS feed embeds an auth token in the path, but query
# information for the search. Because it's a straight-up read, in
# addition to embedding its own auth, it's fine.
my $comp = shift;
my $ARGS = shift;
- return 1 if $is_whitelisted_path{$comp};
+ return 1 if $is_whitelisted_component{$comp};
my %args = %{ $ARGS };
delete $args{results} if $args{results}
and $HTML::Mason::Commands::session{"Actions"}->{$args{results}};
+ # The homepage refresh, which uses the Refresh header, doesn't send
+ # a referer in most browsers; whitelist the one parameter it reloads
+ # with, HomeRefreshInterval, which is safe
+ delete $args{HomeRefreshInterval};
+
# If there are no arguments, then it's likely to be an idempotent
# request, which are not susceptible to CSRF
return 1 if !%args;
sub IsRefererCSRFWhitelisted {
my $referer = _NormalizeHost(shift);
- my $config = _NormalizeHost(RT->Config->Get('WebBaseURL'));
+ my $base_url = _NormalizeHost(RT->Config->Get('WebBaseURL'));
+ $base_url = $base_url->host_port;
- return (1,$referer,$config) if $referer->host_port eq $config->host_port;
+ my $configs;
+ for my $config ( $base_url, RT->Config->Get('ReferrerWhitelist') ) {
+ push @$configs,$config;
+ return 1 if $referer->host_port eq $config;
+ }
- return (0,$referer,$config);
+ return (0,$referer,$configs);
}
=head3 _NormalizeHost
"your browser did not supply a Referrer header", # loc
) if !$ENV{HTTP_REFERER};
- my ($whitelisted, $browser, $config) = IsRefererCSRFWhitelisted($ENV{HTTP_REFERER});
+ my ($whitelisted, $browser, $configs) = IsRefererCSRFWhitelisted($ENV{HTTP_REFERER});
return 0 if $whitelisted;
+ if ( @$configs > 1 ) {
+ return (1,
+ "the Referrer header supplied by your browser ([_1]) is not allowed by RT's configured hostname ([_2]) or whitelisted hosts ([_3])", # loc
+ $browser->host_port,
+ shift @$configs,
+ join(', ', @$configs) );
+ }
+
return (1,
"the Referrer header supplied by your browser ([_1]) is not allowed by RT's configured hostname ([_2])", # loc
- $browser->host_port, $config->host_port);
+ $browser->host_port,
+ $configs->[0]);
}
sub ExpandCSRFToken {
return unless $user->ValidateAuthString( $data->{auth}, $token );
%{$ARGS} = %{$data->{args}};
+ $HTML::Mason::Commands::DECODED_ARGS = $ARGS;
# We explicitly stored file attachments with the request, but not in
# the session yet, as that would itself be an attack. Put them into
return 1;
}
-sub MaybeShowInterstitialCSRFPage {
+sub StoreRequestToken {
my $ARGS = shift;
- return unless RT->Config->Get('RestrictReferrer');
-
- # Deal with the form token provided by the interstitial, which lets
- # browsers which never set referer headers still use RT, if
- # painfully. This blows values into ARGS
- return if ExpandCSRFToken($ARGS);
-
- my ($is_csrf, $msg, @loc) = IsPossibleCSRF($ARGS);
- return if !$is_csrf;
-
- $RT::Logger->notice("Possible CSRF: ".RT::CurrentUser->new->loc($msg, @loc));
-
my $token = Digest::MD5::md5_hex(time . {} . $$ . rand(1024));
my $user = $HTML::Mason::Commands::session{'CurrentUser'}->UserObj;
my $data = {
$HTML::Mason::Commands::session{'CSRF'}->{$token} = $data;
$HTML::Mason::Commands::session{'i'}++;
+ return $token;
+}
+
+sub MaybeShowInterstitialCSRFPage {
+ my $ARGS = shift;
+
+ return unless RT->Config->Get('RestrictReferrer');
+
+ # Deal with the form token provided by the interstitial, which lets
+ # browsers which never set referer headers still use RT, if
+ # painfully. This blows values into ARGS
+ return if ExpandCSRFToken($ARGS);
+
+ my ($is_csrf, $msg, @loc) = IsPossibleCSRF($ARGS);
+ return if !$is_csrf;
+
+ $RT::Logger->notice("Possible CSRF: ".RT::CurrentUser->new->loc($msg, @loc));
+ my $token = StoreRequestToken($ARGS);
$HTML::Mason::Commands::m->comp(
'/Elements/CSRF',
- OriginalURL => $HTML::Mason::Commands::r->path_info,
+ OriginalURL => RT->Config->Get('WebPath') . $HTML::Mason::Commands::r->path_info,
Reason => HTML::Mason::Commands::loc( $msg, @loc ),
Token => $token,
);
if ( $args->{'update-reminders'} ) {
while ( my $reminder = $reminder_collection->Next ) {
- if ( $reminder->Status ne 'resolved' && $args->{ 'Complete-Reminder-' . $reminder->id } ) {
+ my $resolve_status = $reminder->QueueObj->Lifecycle->ReminderStatusOnResolve;
+ if ( $reminder->Status ne $resolve_status && $args->{ 'Complete-Reminder-' . $reminder->id } ) {
$Ticket->Reminders->Resolve($reminder);
}
- elsif ( $reminder->Status eq 'resolved' && !$args->{ 'Complete-Reminder-' . $reminder->id } ) {
+ elsif ( $reminder->Status eq $resolve_status && !$args->{ 'Complete-Reminder-' . $reminder->id } ) {
$Ticket->Reminders->Open($reminder);
}
our @SCRUBBER_ALLOWED_TAGS = qw(
A B U P BR I HR BR SMALL EM FONT SPAN STRONG SUB SUP STRIKE H1 H2 H3 H4 H5
- H6 DIV UL OL LI DL DT DD PRE BLOCKQUOTE
+ H6 DIV UL OL LI DL DT DD PRE BLOCKQUOTE BDO
);
our %SCRUBBER_ALLOWED_ATTRIBUTES = (
)\s* ;? \s*)
+$ # one or more of these allowed properties from here 'till sunset
}ix,
+ dir => qr/^(rtl|ltr)$/i,
+ lang => qr/^\w+(-\w+)?$/,
);
our %SCRUBBER_RULES = ();
],
default_escape_flags => 'h',
data_dir => "$RT::MasonDataDir",
- allow_globals => [qw(%session)],
+ allow_globals => [qw(%session $DECODED_ARGS)],
# Turn off static source if we're in developer mode.
static_source => (RT->Config->Get('DevelMode') ? '0' : '1'),
use_object_files => (RT->Config->Get('DevelMode') ? '0' : '1'),
=head2 GetReferencedQueues
-Returns a hash reference with keys each queue name referenced in a clause in
-the key (even if it's "Queue != 'Foo'"), and values all 1.
+Returns a hash reference; each queue referenced with an '=' operation
+will appear as a key whose value is 1.
=cut
return unless $node->isLeaf;
my $clause = $node->getNodeValue();
+ return unless $clause->{Key} eq 'Queue';
+ return unless $clause->{Op} eq '=';
- if ( $clause->{Key} eq 'Queue' ) {
- $queues->{ $clause->{Value} } = 1;
- };
+ my $value = $clause->{Value};
+ $value =~ s/\\(.)/$1/g if $value =~ s/^'(.*)'$/$1/;
+ $queues->{ $value } = 1;
}
);
return $self->DefaultStatus('on_merge');
}
+=head3 ReminderStatusOnOpen
+
+Returns the status that should be used when reminders are opened.
+
+=cut
+
+sub ReminderStatusOnOpen {
+ my $self = shift;
+ return $self->DefaultStatus('reminder_on_open') || 'open';
+}
+
+=head3 ReminderStatusOnResolve
+
+Returns the status that should be used when reminders are resolved.
+
+=cut
+
+sub ReminderStatusOnResolve {
+ my $self = shift;
+ return $self->DefaultStatus('reminder_on_resolve') || 'resolved';
+}
+
=head2 Transitions, rights, labels and actions.
=head3 Transitions
if ( ($args{'FIELD'} eq 'Target') or
($args{'FIELD'} eq 'LocalTarget') ) {
- $self->OrderBy (ALIAS => 'main',
- FIELD => 'Base',
- ORDER => 'ASC');
+ $self->OrderByCols(
+ { ALIAS => 'main', FIELD => 'LocalBase', ORDER => 'ASC' },
+ { ALIAS => 'main', FIELD => 'Base', ORDER => 'ASC' },
+ );
}
elsif ( ($args{'FIELD'} eq 'Base') or
($args{'FIELD'} eq 'LocalBase') ) {
- $self->OrderBy (ALIAS => 'main',
- FIELD => 'Target',
- ORDER => 'ASC');
+ $self->OrderByCols(
+ { ALIAS => 'main', FIELD => 'LocalTarget', ORDER => 'ASC' },
+ { ALIAS => 'main', FIELD => 'Target', ORDER => 'ASC' },
+ );
}
my $self = shift;
my $reminder = shift;
- my ( $status, $msg ) = $reminder->SetStatus('open');
+ my ( $status, $msg ) =
+ $reminder->SetStatus( $reminder->QueueObj->Lifecycle->ReminderStatusOnOpen );
$self->TicketObj->_NewTransaction(
Type => 'OpenReminder',
Field => 'RT::Ticket',
sub Resolve {
my $self = shift;
my $reminder = shift;
- my ( $status, $msg ) = $reminder->SetStatus('resolved');
+ my ( $status, $msg ) =
+ $reminder->SetStatus( $reminder->QueueObj->Lifecycle->ReminderStatusOnResolve );
$self->TicketObj->_NewTransaction(
Type => 'ResolveReminder',
Field => 'RT::Ticket',
foreach my $id (keys %$queues) {
my $queue = RT::Queue->new( $self->CurrentUser );
$queue->Load($id);
- unless ($queue->id) {
- # XXX TODO: This ancient code dates from a former developer
- # we have no idea what it means or why cfqueues are so encoded.
- $id =~ s/^.'*(.*).'*$/$1/;
- $queue->Load($id);
- }
- $CustomFields->LimitToQueue($queue->Id);
+ $CustomFields->LimitToQueue($queue->Id) if $queue->Id;
}
$CustomFields->LimitToGlobal;
while ( my $CustomField = $CustomFields->Next ) {
return $value;
}
+sub ObjectType {
+ return 'RT::Ticket';
+}
+
RT::Base->_ImportOverlays();
1;
TABLE2 => 'Transactions',
FIELD2 => 'ObjectId',
);
+
+ my $item = $self->NewItem;
+ my $object_type = $item->can('ObjectType') ? $item->ObjectType : ref $item;
+
$self->RT::SearchBuilder::Limit(
LEFTJOIN => $alias,
FIELD => 'ObjectType',
- VALUE => ref $self->NewItem,
+ VALUE => $object_type,
);
$self->{'_sql_aliases'}{'transactions'} = $alias
unless $args{'New'};
$args{$forceopt}=1;
}
- return if $args{nodb};
+ # Short-circuit the rest of ourselves if we don't want a db
+ if ($args{nodb}) {
+ __drop_database();
+ return;
+ }
my $db_type = RT->Config->Get('DatabaseType');
__create_database();
RT::Handle->SystemDSN,
$ENV{RT_DBA_USER}, $ENV{RT_DBA_PASSWORD}
);
+
+ # We ignore errors intentionally by not checking the return value of
+ # DropDatabase below, so let's also suppress DBI's printing of errors when
+ # we overzealously drop.
+ local $dbh->{PrintError} = 0;
+ local $dbh->{PrintWarn} = 0;
+
RT::Handle->DropDatabase( $dbh );
$dbh->disconnect if $my_dbh;
}
require RT::Test::Web;
- if ($rttest_opt{nodb}) {
- die "you are trying to use a test web server without db, try use noinitialdata => 1 instead";
+ if ($rttest_opt{nodb} and not $rttest_opt{server_ok}) {
+ die "You are trying to use a test web server without a database. "
+ ."You may want noinitialdata => 1 instead. "
+ ."Pass server_ok => 1 if you know what you're doing.";
}
my $self = shift;
my %server_opt = @_;
- require RT::Interface::Web::Handler;
- my $app = RT::Interface::Web::Handler->PSGIApp;
+ my $app;
+
+ my $warnings = "";
+ open( my $warn_fh, ">", \$warnings );
+ local *STDERR = $warn_fh;
+
+ if ($server_opt{variant} and $server_opt{variant} eq 'rt-server') {
+ $app = do {
+ my $file = "$RT::SbinPath/rt-server";
+ my $psgi = do $file;
+ unless ($psgi) {
+ die "Couldn't parse $file: $@" if $@;
+ die "Couldn't do $file: $!" unless defined $psgi;
+ die "Couldn't run $file" unless $psgi;
+ }
+ $psgi;
+ };
+ } else {
+ require RT::Interface::Web::Handler;
+ $app = RT::Interface::Web::Handler->PSGIApp;
+ }
require Plack::Middleware::Test::StashWarnings;
- $app = Plack::Middleware::Test::StashWarnings->wrap($app);
+ my $stashwarnings = Plack::Middleware::Test::StashWarnings->new;
+ $app = $stashwarnings->wrap($app);
if ($server_opt{basic_auth}) {
require Plack::Middleware::Auth::Basic;
}
);
}
+
+ close $warn_fh;
+ $stashwarnings->add_warning( $warnings ) if $warnings;
+
return $app;
}
my $Tester = Test::Builder->new;
$Tester->ok(1, "started plack server ok");
- __reconnect_rt();
+ __reconnect_rt()
+ unless $rttest_opt{nodb};
return ("http://localhost:$port", RT::Test::Web->new);
}
use warnings;
use base qw(Test::WWW::Mechanize);
+use Scalar::Util qw(weaken);
-require RT::Test;
+BEGIN { require RT::Test; }
require Test::More;
+my $instance;
+
sub new {
my ($class, @args) = @_;
push @args, app => $RT::Test::TEST_APP if $RT::Test::TEST_APP;
- my $self = $class->SUPER::new(@args);
+ my $self = $instance = $class->SUPER::new(@args);
+ weaken $instance;
$self->cookie_jar(HTTP::Cookies->new);
return $self;
Test::More::diag("error: page has no Logout");
return 0;
}
+ RT::Interface::Web::EscapeUTF8(\$user);
unless ( $self->content =~ m{<span class="current-user">\Q$user\E</span>}i ) {
Test::More::diag("Page has no user name");
return 0;
}
}
+END {
+ return unless $instance;
+ return if RT::Test->builder->{Original_Pid} != $$;
+ $instance->no_warnings_ok if !$RT::Test::Web::DESTROY++;
+}
+
1;
# here is where we store extra data, say if it's a keyword or
# something. (I.e. "TYPE SPECIFIC STUFF")
- push @{ $clause{$realfield} }, $data;
+ if (lc $ea eq 'none') {
+ $clause{$realfield} = [ $data ];
+ } else {
+ push @{ $clause{$realfield} }, $data;
+ }
}
return \%clause;
}
sub HREF {
my $self = shift;
if ($self->IsLocal && $self->Object) {
- return ( RT->Config->Get('WebURL') . "/Articles/Article/Display.html?id=".$self->Object->Id);
+ return ( RT->Config->Get('WebURL') . "Articles/Article/Display.html?id=".$self->Object->Id);
}
else {
return ($self->URI);
print STDERR <<EOT;
Full text search is disabled in your RT configuration. Run
-/www/data/rt/4.0.5/sbin/rt-setup-fulltext-index to configure and enable it.
+/www/var/rt/sbin/rt-setup-fulltext-index to configure and enable it.
EOT
exit 1;
use warnings;
use strict;
-use lib '/www/data/rt/rt-perl/current-perl10/share/perl5';
-use lib '/www/data/rt/rt-perl/current-perl10/lib/perl5';
-use lib '/www/data/rt/rt-perl/current-perl10/lib64/perl5';
-
# fix lib paths, some may be relative
BEGIN {
die <<EOT if ${^TAINT};
require RT;
RT->LoadConfig();
+RT->InitLogging();
require Module::Refresh if RT->Config->Get('DevelMode');
require RT::Handle;
=head2 --sqldump <filename>
-Outputs INSERT queiries into file. This dump can be used to restore data
+Outputs INSERT queries into file. This dump can be used to restore data
after wiping out.
By default creates files
use strict;
use warnings FATAL => 'all';
-use lib '/www/data/rt/rt-perl/current-perl10/share/perl5';
-use lib '/www/data/rt/rt-perl/current-perl10/lib/perl5';
-use lib '/www/data/rt/rt-perl/current-perl10/lib64/perl5';
-
# fix lib paths, some may be relative
BEGIN {
require File::Spec;
File::Spec 0.8
HTML::Quoted
HTML::Scrubber 0.08
+HTML::TreeBuilder
+HTML::FormatText
Log::Dispatch 2.23
Sys::Syslog 0.16
Locale::Maketext 1.06
.
$deps{'MAILGATE'} = [ text_to_hash( << '.') ];
-HTML::TreeBuilder
-HTML::FormatText
Getopt::Long
LWP::UserAgent
Pod::Usage
Test::MockTime
Log::Dispatch::Perl
Test::WWW::Mechanize::PSGI
-Plack::Middleware::Test::StashWarnings
+Plack::Middleware::Test::StashWarnings 0.06
Test::LongString
.
$deps{'DASHBOARDS'} = [ text_to_hash( << '.') ];
HTML::RewriteAttributes 0.04
MIME::Types
+URI 1.59
.
$deps{'GRAPHVIZ'} = [ text_to_hash( << '.') ];
require RT;
RT->LoadConfig();
+RT->InitLogging();
require Module::Refresh if RT->Config->Get('DevelMode');
require RT::Handle;
$Object => undef
</%ARGS>
% my $name = (defined $Object->Filename and length $Object->Filename) ? $Object->Filename : loc("(no value)");
-<a href="<% RT->Config->Get('WebURL') %>/Ticket/Attachment/<% $Object->TransactionId %>/<% $Object->id %>/">
+<a href="<% RT->Config->Get('WebPath') %>/Ticket/Attachment/<% $Object->TransactionId %>/<% $Object->id %>/">
<% loc('Attachment') %>(<% loc('id') %>:<% $Object->id %>, <% loc('Filename') %>: <% $name %>)
</a>
<%ARGS>
$Object => undef
</%ARGS>
-<a href="<% RT->Config->Get('WebURL') %>/Ticket/Display.html?id=<% $Object->id %>">
+<a href="<% RT->Config->Get('WebPath') %>/Ticket/Display.html?id=<% $Object->id %>">
<% loc('Ticket') %>(<% loc('id') %>:<% $Object->id %>, <% loc('Subject') %>: <% substr($Object->Subject, 0, 30) %>...)
</a>
<%ARGS>
$Object => undef
</%ARGS>
-<a href="<% RT->Config->Get('WebURL') %>/Admin/Users/Modify.html?id=<% $Object->id %>">
+<a href="<% RT->Config->Get('WebPath') %>/Admin/Users/Modify.html?id=<% $Object->id %>">
<% loc('User') %>(<% loc('id') %>:<% $Object->id %>, <% loc('Name') %>: <% $Object->Name %>)
</a>
% if ($UserObj->id) {
<& /Elements/EditCustomField, %ARGS, Object => $UserObj, CustomField => $CF &>
% } else {
-<& /Elements/EditCustomField, %ARGS, NamePrefix => 'Object-RT::User-new-CustomField-', CustomField => $CF &>
+<& /Elements/EditCustomField, %ARGS, NamePrefix => 'Object-RT::User--CustomField-', CustomField => $CF &>
% }
</td></tr>
% }
if ($val) {
push @results, $msg;
- foreach my $key ( keys %ARGS) {
- # Convert custom fields on the "new" object to custom fields on the one we've just created
- if ($key =~ /^Object-RT::User-new-CustomField-(.*)$/) {
- $ARGS{'Object-RT::User-'.$val.'-CustomField-'.$1} = delete $ARGS{$key};
- }
- }
push @results, ProcessObjectCustomFieldUpdates( ARGSRef => \%ARGS, Object => $UserObj );
} else {
push @results, loc('User could not be created: [_1]', $msg);
# collection is ordered or not
if ( @OrderBy && ($AllowSorting || !$Collection->{'order_by'}) ) {
if ( $OrderBy[0] =~ /\|/ ) {
- @OrderBy = split /\|/, $OrderBy[0];
+ @OrderBy = grep length($_), split /\|/, $OrderBy[0];
@Order = split /\|/,$Order[0];
}
$Collection->OrderByCols(
<& /Elements/Framekiller &>
% if ($Refresh && $Refresh =~ /^(\d+)/ && $1 > 0) {
-% my $URL = $m->notes->{LogoutURL}; $URL = $URL ? ";URL=$URL" : "";
+% my $URL = $m->notes->{RefreshURL}; $URL = $URL ? ";URL=$URL" : "";
<meta http-equiv="refresh" content="<% "$1$URL" %>" />
% }
title => 'Encrypt', # loc
value => sub { return $_[0]->Encrypt? $_[0]->loc('yes') : $_[0]->loc('no') },
},
+ Lifecycle => {
+ title => 'Lifecycle',
+ attribute => 'Lifecycle',
+ value => sub { return $_[0]->Lifecycle->Name },
+ },
};
foreach my $field (qw(
Name Description CorrespondAddress CommentAddress
InitialPriority FinalPriority DefaultDueIn
- Lifecycle
)) {
$COLUMN_MAP->{$field} = {
title => $field,
@objects = ($TicketObj);
} elsif ($QueueObj) {
@objects = ($QueueObj);
-} elsif ($cfqueues) {
- @objects = keys %{$cfqueues};
+} elsif (%Queues) {
+ for my $name (keys %Queues) {
+ my $q = RT::Queue->new($session{'CurrentUser'});
+ $q->Load($name);
+ push @objects, $q;
+ }
} else {
# Let's check rights on an empty queue object. that will do a search
# for any queue.
<%ARGS>
$TicketObj => undef
$QueueObj => undef
-$cfqueues => undef
+%Queues => ()
</%ARGS>
}
elsif ( $TicketObj ) {
my $current = $TicketObj->Status;
+ push @status, $current;
+
my $lifecycle = $TicketObj->QueueObj->Lifecycle;
my %has = ();
}
elsif ( $QueueObj ) {
@status = $QueueObj->Lifecycle->Transitions('');
-}
-else {
+} elsif ( %Queues ) {
+ for my $id (keys %Queues) {
+ my $queue = RT::Queue->new($session{'CurrentUser'});
+ $queue->Load($id);
+ push @status, $queue->Lifecycle->Valid if $queue->id;
+ }
+ my %seen;
+ @status = grep { not $seen{$_}++ } @status;
+} else {
@status = RT::Queue->Lifecycle->Valid;
}
</%INIT>
@Statuses => ()
$TicketObj => undef
$QueueObj => undef
+%Queues => ()
$Default => ''
$SkipDeleted => 0
%# those contributions and any derivatives thereof.
%#
%# END BPS TAGGED BLOCK }}}
-<a href="<%$URI->Resolver->HREF%>">
+<a href="<% $href %>">
% if ($URI->IsLocal) {
% my $member = $URI->Object;
% my $has_name = UNIVERSAL::can($member, 'Name') || (UNIVERSAL::can($member, '_Accessible') && $member->_Accessible('Name', 'read'));
<%ARGS>
$URI => undef
</%ARGS>
+
+<%INIT>
+my $href = $URI->Resolver->HREF;
+if ( $URI->IsLocal ) {
+ my $base = RT->Config->Get('WebBaseURL');
+ # URI->rel doesn't contain the leading '/'
+ $href = '/' . URI->new($href)->rel($base);
+}
+</%INIT>
# XXX: dispatch to different handler here
$query_display_component
= '/Search/Elements/' . $SearchArg->{SearchType};
- $query_link_url = RT->Config->Get('WebURL') . "/Search/$SearchArg->{SearchType}.html";
+ $query_link_url = RT->Config->Get('WebPath') . "/Search/$SearchArg->{SearchType}.html";
} elsif ($ShowCustomize) {
$customize = RT->Config->Get('WebPath') . '/Search/Build.html?'
. $m->comp( '/Elements/QueryString',
my $section;
if ( $request_path =~ m|^/Admin/$type/?(?:index.html)?$|
|| ( $request_path =~ m|^/Admin/$type/(?:Modify.html)$|
- && $m->request_args->{'Create'} )
+ && $DECODED_ARGS->{'Create'} )
)
{
$section = $tabs;
}
if ( $request_path =~ m{^/Admin/Queues} ) {
- if ( $m->request_args->{'id'} && $m->request_args->{'id'} =~ /^\d+$/
+ if ( $DECODED_ARGS->{'id'} && $DECODED_ARGS->{'id'} =~ /^\d+$/
||
- $m->request_args->{'Queue'} && $m->request_args->{'Queue'} =~ /^\d+$/
+ $DECODED_ARGS->{'Queue'} && $DECODED_ARGS->{'Queue'} =~ /^\d+$/
) {
- my $id = $m->request_args->{'Queue'} || $m->request_args->{'id'};
+ my $id = $DECODED_ARGS->{'Queue'} || $DECODED_ARGS->{'id'};
my $queue_obj = RT::Queue->new( $session{'CurrentUser'} );
$queue_obj->Load($id);
}
}
if ( $request_path =~ m{^/Admin/Users} ) {
- if ( $m->request_args->{'id'} && $m->request_args->{'id'} =~ /^\d+$/ ) {
- my $id = $m->request_args->{'id'};
+ if ( $DECODED_ARGS->{'id'} && $DECODED_ARGS->{'id'} =~ /^\d+$/ ) {
+ my $id = $DECODED_ARGS->{'id'};
my $obj = RT::User->new( $session{'CurrentUser'} );
$obj->Load($id);
}
if ( $request_path =~ m{^/Admin/Groups} ) {
- if ( $m->request_args->{'id'} && $m->request_args->{'id'} =~ /^\d+$/ ) {
- my $id = $m->request_args->{'id'};
+ if ( $DECODED_ARGS->{'id'} && $DECODED_ARGS->{'id'} =~ /^\d+$/ ) {
+ my $id = $DECODED_ARGS->{'id'};
my $obj = RT::Group->new( $session{'CurrentUser'} );
$obj->Load($id);
}
if ( $request_path =~ m{^/Admin/CustomFields/} ) {
- if ( $m->request_args->{'id'} && $m->request_args->{'id'} =~ /^\d+$/ ) {
- my $id = $m->request_args->{'id'};
+ if ( $DECODED_ARGS->{'id'} && $DECODED_ARGS->{'id'} =~ /^\d+$/ ) {
+ my $id = $DECODED_ARGS->{'id'};
my $obj = RT::CustomField->new( $session{'CurrentUser'} );
$obj->Load($id);
if ( $request_path =~ m{^/Admin/Articles/Classes/} ) {
my $tabs = PageMenu();
- if ( my $id = $m->request_args->{'id'} ) {
+ if ( my $id = $DECODED_ARGS->{'id'} ) {
my $obj = RT::CustomField->new( $session{'CurrentUser'} );
$obj->Load($id);
$about_me->child( logout => title => loc('Logout'), path => '/NoAuth/Logout.html' );
}
if ( $request_path =~ m{^/Dashboards/(\d+)?}) {
- if ( my $id = ( $1 || $m->request_args->{'id'} ) ) {
+ if ( my $id = ( $1 || $DECODED_ARGS->{'id'} ) ) {
my $obj = RT::Dashboard->new( $session{'CurrentUser'} );
$obj->LoadById($id);
if ( $obj and $obj->id ) {
if ( $request_path =~ m{^/Ticket/} ) {
- if ( ( $m->request_args->{'id'} || '' ) =~ /^(\d+)$/ ) {
+ if ( ( $DECODED_ARGS->{'id'} || '' ) =~ /^(\d+)$/ ) {
my $id = $1;
my $obj = RT::Ticket->new( $session{'CurrentUser'} );
$obj->Load($id);
&& $request_path !~ m{^/Search/Simple\.html}
)
|| ( $request_path =~ m{^/Search/Simple\.html}
- && $m->request_args->{'q'} )
+ && $DECODED_ARGS->{'q'} )
)
{
my $search = Menu()->child('search');
my $args = '';
my $has_query = '';
my $current_search = $session{"CurrentSearchHash"} || {};
- my $search_id = $m->request_args->{'SavedSearchLoad'} || $m->request_args->{'SavedSearchId'} || $current_search->{'SearchId'} || '';
- my $chart_id = $m->request_args->{'SavedChartSearchId'} || $current_search->{SavedChartSearchId};
+ my $search_id = $DECODED_ARGS->{'SavedSearchLoad'} || $DECODED_ARGS->{'SavedSearchId'} || $current_search->{'SearchId'} || '';
+ my $chart_id = $DECODED_ARGS->{'SavedChartSearchId'} || $current_search->{SavedChartSearchId};
- $has_query = 1 if ( $m->request_args->{'Query'} or $current_search->{'Query'} );
+ $has_query = 1 if ( $DECODED_ARGS->{'Query'} or $current_search->{'Query'} );
my %query_args;
my %fallback_query_args = (
(
map {
my $p = $_;
- $p => $m->request_args->{$p} || $current_search->{$p}
+ $p => $DECODED_ARGS->{$p} || $current_search->{$p}
} qw(Query Format OrderBy Order Page)
),
RowsPerPage => (
- defined $m->request_args->{'RowsPerPage'}
- ? $m->request_args->{'RowsPerPage'}
+ defined $DECODED_ARGS->{'RowsPerPage'}
+ ? $DECODED_ARGS->{'RowsPerPage'}
: $current_search->{'RowsPerPage'}
),
);
}
if ( $request_path =~ m{^/Article/} ) {
- if ( $m->request_args->{'id'} && $m->request_args->{'id'} =~ /^\d+$/ ) {
- my $id = $m->request_args->{'id'};
+ if ( $DECODED_ARGS->{'id'} && $DECODED_ARGS->{'id'} =~ /^\d+$/ ) {
+ my $id = $DECODED_ARGS->{'id'};
my $tabs = PageMenu();
$tabs->child( display => title => loc('Display'), path => "/Articles/Article/Display.html?id=".$id );
my $tabs = PageMenu();
$tabs->child( search => title => loc("Search"), path => "/Articles/Article/Search.html" );
$tabs->child( create => title => loc("New Article" ), path => "/Articles/Article/PreCreate.html" );
- if ( $request_path =~ m{^/Articles/Article/} and ( $m->request_args->{'id'} || '' ) =~ /^(\d+)$/ ) {
+ if ( $request_path =~ m{^/Articles/Article/} and ( $DECODED_ARGS->{'id'} || '' ) =~ /^(\d+)$/ ) {
my $id = $1;
my $obj = RT::Article->new( $session{'CurrentUser'} );
$obj->Load($id);
$queues->UnLimit;
my $queue_count = 0;
- my $queue_id = 1;
+ my $queue_id;
while ( my $queue = $queues->Next ) {
next unless $queue->CurrentUserHasRight('CreateTicket');
}
+ if ( $queue_count > 1 ) {
+ Menu->child( new => title => loc('New ticket'), path => '/SelfService/CreateTicketInQueue.html' );
+ } elsif ( $queue_id ) {
+ Menu->child( new => title => loc('New ticket'), path => '/SelfService/Create.html?Queue=' . $queue_id );
+ }
my $tickets = Menu->child( tickets => title => loc('Tickets'));
$tickets->child( open => title => loc('Open tickets'), path => '/SelfService/' );
$tickets->child( closed => title => loc('Closed tickets'), path => '/SelfService/Closed.html' );
- if ( $queue_count > 1 ) {
- $tickets->child( new => title => loc('New ticket'), path => '/SelfService/CreateTicketInQueue.html' );
- } else {
- $tickets->child( new => title => loc('New ticket'), path => '/SelfService/Create.html?Queue=' . $queue_id );
- }
my $username = '<span class="current-user">'
$RT::Handle = RT::Handle->new;
RT::Init();
my $file = $RT::EtcPath . "/initialdata";
- ($status, $msg) = $RT::Handle->InsertData( $file );
+ ($status, $msg) = $RT::Handle->InsertData( $file, undef, disconnect_after => 0 );
}
unless ( $status ) {
push @errors, loc('ERROR: [_1]', $msg);
my @errors;
my $locked;
-my $file = File::Spec->catfile( $RT::EtcPath, 'RT_SiteConfig.pm' );
+my $file = RT::Installer->ConfigFile;
if ( ! -e $file ) {
# write a blank RT_SiteConfig.pm
}
$m->callback( %ARGS, CallbackName => 'AfterSessionDelete' );
-$m->notes->{LogoutURL} = $URL;
+$m->notes->{RefreshURL} = $URL;
</%INIT>
my ( $AvailableColumns, $CurrentFormat );
( $ARGS{Format}, $AvailableColumns, $CurrentFormat ) = $m->comp(
'/Search/Elements/BuildFormatString',
- cfqueues => {}, %ARGS
+ %ARGS
);
if ($ARGS{'Save'}) {
}
# Set custom field
elsif ($k =~ /^$cf_spec/) {
- my $cf = RT::CustomField->new( RT->SystemUser );
- my $cfk = $1 || $2;
- unless($cf->LoadByName( Name => $cfk )) {
- push @comments, "# Invalid custom field name ($cfk)";
+ my $key = $1 || $2;
+
+ my $cf = RT::CustomField->new( $session{CurrentUser} );
+ $cf->LoadByName( Name => $key, Queue => $data{Queue} || $v{Queue} );
+ unless ( $cf->id ) {
+ $cf->LoadByName( Name => $key, Queue => 0 );
+ }
+
+ if (not $cf->id) {
+ push @comments, "# Invalid custom field name ($key)";
delete $data{$k};
next;
}
}
# Set custom field
elsif ($key =~ /^$cf_spec/) {
- my $cf = RT::CustomField->new( RT->SystemUser );
$key = $1 || $2;
- if (not $cf->LoadByName( Name => $key )) {
+
+ my $cf = RT::CustomField->new( $session{CurrentUser} );
+ $cf->LoadByName( Name => $key, Queue => $ticket->Queue );
+ unless ( $cf->id ) {
+ $cf->LoadByName( Name => $key, Queue => 0 );
+ }
+
+ if (not $cf->id) {
$n = 0;
$s = "Unknown custom field.";
}
my $tick = RT::Ticket->new($session{CurrentUser});
$tick->Load($nkey);
if ($tick->Id) {
- $nkey = $uri->FromObject($tick);
+ $uri->FromObject($tick);
+ $nkey = $uri->URI;
}
else {
$n = 0;
goto OUTPUT;
}
$id ||= $object;
-unless ($id =~ /^\d+$/ && $to =~ /^\d+$/) {
- my $bad = ($id !~ /^\d+$/) ? $id : $to;
+unless ($id =~ /^\d+$/) {
$output = $r->path_info. "\n";
- $output .= "Invalid ticket id: '$bad'.\n";
+ $output .= "Invalid ticket id: '$id'.\n";
$status = "400 Bad Request";
goto OUTPUT;
}
<div id="pick-criteria">
- <& Elements/PickCriteria, query => $query{'Query'}, cfqueues => $queues &>
+ <& Elements/PickCriteria, query => $query{'Query'}, queues => $queues &>
</div>
<& /Elements/Submit, Label => loc('Add these terms'), SubmitId => 'AddClause', Name => 'AddClause'&>
<& /Elements/Submit, Label => loc('Add these terms and Search'), SubmitId => 'DoSearch', Name => 'DoSearch'&>
( $query{'Format'}, $AvailableColumns, $CurrentFormat ) = $m->comp(
'Elements/BuildFormatString',
%ARGS,
- cfqueues => $queues,
+ queues => $queues,
Format => $query{'Format'},
);
SavedChartSearchId => $ARGS{'SavedChartSearchId'},
SavedSearchId => $saved_search{'Id'},
);
- RT::Interface::Web::Redirect(RT->Config->Get('WebPath') . '/Search/Results.html?' . $redir_query_string);
+ RT::Interface::Web::Redirect(RT->Config->Get('WebURL') . 'Search/Results.html?' . $redir_query_string);
$m->abort;
}
<%ARGS>
$Format => RT->Config->Get('DefaultSearchResultFormat')
-%cfqueues => ()
+%queues => ()
$Face => undef
$Size => undef
$m->callback( CallbackOnce => 1, CallbackName => 'SetFieldsOnce', Fields => \@fields );
my $CustomFields = RT::CustomFields->new( $session{'CurrentUser'});
-foreach my $id (keys %cfqueues) {
+foreach my $id (keys %queues) {
# Gotta load up the $queue object, since queues get stored by name now. my $id
my $queue = RT::Queue->new($session{'CurrentUser'});
$queue->Load($id);
- unless ($queue->id) {
- # XXX TODO: This ancient code dates from a former developer
- # we have no idea what it means or why cfqueues are so encoded.
- $id =~ s/^.'*(.*).'*$/$1/;
- $queue->Load($id);
- }
- $CustomFields->LimitToQueue($queue->Id);
+ $CustomFields->LimitToQueue($queue->Id) if $queue->Id;
}
$CustomFields->LimitToGlobal;
);
</%perl>
<td class="label collection-as-table">
-<a href=<% RT->Config->Get('WebURL') %>Search/Results.html?<%$QueryString%>><%$key%></a>
+<a href=<% RT->Config->Get('WebPath') %>/Search/Results.html?<%$QueryString%>><%$key%></a>
</td>
<td class="value collection-as-table">
-<a href=<% RT->Config->Get('WebURL') %>Search/Results.html?<%$QueryString%>><%$value%></a>
+<a href=<% RT->Config->Get('WebPath') %>/Search/Results.html?<%$QueryString%>><%$value%></a>
</td>
% } else {
<td class="label collection-as-table"><% $key %></td>
Value => {
Type => 'component',
Path => '/Elements/SelectStatus',
- Arguments => { SkipDeleted => 1 },
+ Arguments => { SkipDeleted => 1, Queues => \%queues },
},
},
{
Value => {
Type => 'component',
Path => '/Elements/SelectOwner',
- Arguments => { ValueAttribute => 'Name' },
+ Arguments => { ValueAttribute => 'Name', Queues => \%queues },
},
},
{
$m->callback( Conditions => \@lines );
</%INIT>
+<%ARGS>
+%queues => ()
+</%ARGS>
% }
<%INIT>
my $CustomFields = RT::CustomFields->new( $session{'CurrentUser'});
-foreach my $id (keys %cfqueues) {
- # Gotta load up the $queue object, since queues get stored by name now. my $id
+foreach my $id (keys %queues) {
+ # Gotta load up the $queue object, since queues get stored by name now.
my $queue = RT::Queue->new($session{'CurrentUser'});
$queue->Load($id);
- unless ($queue->id) {
- # XXX TODO: This ancient code dates from a former developer
- # we have no idea what it means or why cfqueues are so encoded.
- $id =~ s/^.'*(.*).'*$/$1/;
-
- # unescape internal quotes
- $id =~ s/(\\(.))/$2 eq "'" ? "'" : $1/eg;
-
- $queue->Load($id);
- }
- $CustomFields->LimitToQueue($queue->Id);
+ $CustomFields->LimitToQueue($queue->Id) if $queue->Id;
}
$CustomFields->LimitToGlobal;
$m->callback(
push @lines, \%line;
}
-$m->callback( Conditions => \@lines, Queues => \%cfqueues );
+$m->callback( Conditions => \@lines, Queues => \%queues );
</%INIT>
<%ARGS>
-%cfqueues => undef
+%queues => ()
</%ARGS>
-<& PickBasics &>
-<& PickCFs, cfqueues => \%cfqueues &>
+<& PickBasics, queues => \%queues &>
+<& PickCFs, queues => \%queues &>
<tr class="separator"><td colspan="3"><hr /></td></tr>
<tr>
<%ARGS>
$addquery => 0
$query => undef
-%cfqueues => undef
+%queues => ()
</%ARGS>
%#
%# END BPS TAGGED BLOCK }}}
<& /Elements/Header, Title => $title,
- Refresh => $session{'tickets_refresh_interval'} || RT->Config->Get('SearchResultsRefreshInterval', $session{'CurrentUser'} ),
+ Refresh => $refresh,
LinkRel => \%link_rel &>
<& /Elements/Tabs &>
<& /Elements/CollectionList,
$session{'tickets_refresh_interval'} = $ARGS{'TicketsRefreshInterval'};
}
+my $refresh = $session{'tickets_refresh_interval'}
+ || RT->Config->Get('SearchResultsRefreshInterval', $session{'CurrentUser'} );
+
+if (RT->Config->Get('RestrictReferrer') and $refresh and not $m->request_args->{CSRF_Token}) {
+ my $token = RT::Interface::Web::StoreRequestToken( $session{'CurrentSearchHash'} );
+ $m->notes->{RefreshURL} = RT->Config->Get('WebURL')
+ . "Search/Results.html?CSRF_Token="
+ . $token;
+}
+
my %link_rel;
my $genpage = sub {
return $m->comp(
<& /Ticket/Elements/EditTransactionCustomFields, %ARGS, QueueObj => $QueueObj, InTable => 1 &>
</table>
</&>
-% $m->callback( CallbackName => 'AfterBasics', QueueObj => $QueueObj );
+% $m->callback( CallbackName => 'AfterBasics', QueueObj => $QueueObj, ARGSRef => \%ARGS );
</div>
<div id="ticket-create-message">
my $members = $CloneTicketObj->Members;
my ( @members, @members_of, @refers, @refers_by, @depends, @depends_by );
my $refers = $CloneTicketObj->RefersTo;
+ my $get_link_value = sub {
+ my ($link, $type) = @_;
+ my $uri_method = $type . 'URI';
+ my $local_method = 'Local' . $type;
+ my $uri = $link->$uri_method;
+ return if $uri->IsLocal and
+ $uri->Object and
+ $uri->Object->isa('RT::Ticket') and
+ $uri->Object->Type eq 'reminder';
+
+ return $link->$local_method || $uri->URI;
+ };
while ( my $refer = $refers->Next ) {
- push @refers, $refer->LocalTarget;
+ my $refer_value = $get_link_value->($refer, 'Target');
+ push @refers, $refer_value if defined $refer_value;
}
$clone->{'new-RefersTo'} = join ' ', @refers;
my $refers_by = $CloneTicketObj->ReferredToBy;
while ( my $refer_by = $refers_by->Next ) {
- push @refers_by, $refer_by->LocalBase;
+ my $refer_by_value = $get_link_value->($refer_by, 'Base');
+ push @refers_by, $refer_by_value if defined $refer_by_value;
}
$clone->{'RefersTo-new'} = join ' ', @refers_by;
- if (0) { # Temporarily disabled
- my $depends = $CloneTicketObj->DependsOn;
- while ( my $depend = $depends->Next ) {
- push @depends, $depend->LocalTarget;
- }
- $clone->{'new-DependsOn'} = join ' ', @depends;
-
- my $depends_by = $CloneTicketObj->DependedOnBy;
- while ( my $depend_by = $depends_by->Next ) {
- push @depends_by, $depend_by->LocalBase;
- }
- $clone->{'DependsOn-new'} = join ' ', @depends_by;
-
- while ( my $member = $members->Next ) {
- push @members, $member->LocalBase;
- }
- $clone->{'MemberOf-new'} = join ' ', @members;
-
- my $members_of = $CloneTicketObj->MemberOf;
- while ( my $member_of = $members_of->Next ) {
- push @members_of, $member_of->LocalTarget;
- }
- $clone->{'new-MemberOf'} = join ' ', @members_of;
-
- }
my $cfs = $CloneTicketObj->QueueObj->TicketCustomFields();
while ( my $cf = $cfs->Next ) {
<& /Elements/ListActions, actions => \@Actions &>
<& Elements/ShowUpdateStatus, Ticket => $TicketObj &>
-% $m->callback( %ARGS, Ticket => $TicketObj, CallbackName => 'BeforeShowSummary' );
+% $m->callback( %ARGS, Ticket => $TicketObj, Transactions => $transactions, Attachments => $attachments, CallbackName => 'BeforeShowSummary' );
<div class="summary">
<&| /Widgets/TitleBox, title => loc('Ticket metadata') &>
<& /Ticket/Elements/ShowSummary, Ticket => $TicketObj, Attachments => $attachments &>
</div>
<br />
-% $m->callback( Ticket => $TicketObj, %ARGS, CallbackName => 'BeforeShowHistory' );
+% $m->callback( Ticket => $TicketObj, %ARGS, Transactions => $transactions, Attachments => $attachments, CallbackName => 'BeforeShowHistory' );
% if (not $ForceShowHistory and RT->Config->Get( 'DeferTransactionLoading', $session{'CurrentUser'} )) {
<& /Ticket/Elements/ClickToShowHistory,
% $m->callback( %ARGS,
% Ticket => $TicketObj,
+% Transactions => $transactions,
+% Attachments => $attachments,
% CallbackName => 'AfterShowHistory',
% );
<%init>
$Ticket = LoadTicket($id) if ($id);
+my $resolve_status = $Ticket->QueueObj->Lifecycle->ReminderStatusOnResolve;
my $count_reminders = RT::Reminders->new($session{'CurrentUser'});
$count_reminders->Ticket($Ticket->id);
my $count_tickets = $count_reminders->Collection;
if (!$ShowCompleted) {
# XXX: don't break encapsulation if we can avoid it
- $count_tickets->FromSQL('Type = "reminder" AND RefersTo = "'.$Ticket->id.'" AND Status != "resolved"');
+ $count_tickets->FromSQL(q{Type = "reminder" AND RefersTo = "} . $Ticket->id . qq{" AND Status != "$resolve_status" });
}
my $has_reminders = $count_tickets->Count;
% my $visible = 0;
% while ( my $reminder = $reminder_collection->Next ) {
% $i++;
-% if ( $reminder->Status eq 'resolved' && !$ShowCompleted ) {
+% if ( $reminder->Status eq $resolve_status && !$ShowCompleted ) {
<tr class="hidden"><td><input type="hidden" class="hidden" name="Complete-Reminder-<% $reminder->id %>" value="1" /></td></tr>
% $i++;
% } elsif ($Edit) {
%# we must always include resolved reminders due to the browser
%# checkbox-with-false-value issue
% while ( my $reminder = $reminder_collection->Next ) {
-% if ( $reminder->Status eq 'resolved' && !$ShowCompleted ) {
+% if ( $reminder->Status eq $resolve_status && !$ShowCompleted ) {
<input type="hidden" class="hidden" name="Complete-Reminder-<% $reminder->id %>" value="1" />
% }
% }
$Index
</%args>
<tr class="<% $Index%2 ? 'oddline' : 'evenline' %>">
-<td class="entry"><input type="checkbox" value="1" name="Complete-Reminder-<% $Reminder->id %>" <% $Reminder->Status eq 'resolved' ? 'checked="checked"' : '' |n %> /></td>
+<td class="entry"><input type="checkbox" value="1" name="Complete-Reminder-<% $Reminder->id %>" <% $Reminder->Status eq $Reminder->QueueObj->Lifecycle->ReminderStatusOnResolve ? 'checked="checked"' : '' |n %> /></td>
<td class="label"><&|/l&>Subject</&>:</td>
<td class="entry" colspan="3"><input type="text" size="50" name="Reminder-Subject-<% $Reminder->id %>" value="<% $Reminder->Subject %>" /></td>
</tr>
% my $dueobj = $Reminder->DueObj;
% my $overdue = $dueobj->Unix > 0 && $dueobj->Diff < 0 ? 1 : 0;
<tr class="<% $Index%2 ? 'oddline' : 'evenline' %>">
-<td class="collection-as-table"><input type="checkbox" value="1" name="Complete-Reminder-<% $Reminder->id %>" <% $Reminder->Status eq 'resolved' ? 'checked="checked"' : '' |n %> /></td>
+<td class="collection-as-table"><input type="checkbox" value="1" name="Complete-Reminder-<% $Reminder->id %>" <% $Reminder->Status eq $Reminder->QueueObj->Lifecycle->ReminderStatusOnResolve ? 'checked="checked"' : '' |n %> /></td>
<td class="collection-as-table"><% $Reminder->Subject %></td>
<td class="collection-as-table"><% $overdue ? '<span class="overdue">' : '' |n %><% $dueobj->AgeAsString || loc('Not set') %><% $overdue ? '</span>' : '' |n %></td>
<td class="collection-as-table"><& /Elements/ShowUser, User => $Reminder->OwnerObj &></td>
% $m->callback( CallbackName => 'BeforeActionList', %ARGS, Actions => \@results, ARGSRef => \%ARGS );
<& /Elements/ListActions, actions => \@results &>
<form method="post" action="<% RT->Config->Get('WebPath') . $m->request_comp->path %>?id=<% $id %>">
-<a href="<% RT->Config->Get('WebURL') %>Ticket/Display.html?id=<% $txn->Ticket %>#txn-<% $id %>">
+<a href="<% RT->Config->Get('WebPath') %>/Ticket/Display.html?id=<% $txn->Ticket %>#txn-<% $id %>">
<% loc('Return back to the ticket') %>
</a>
<& /Elements/Submit,
% $m->callback(CallbackName => 'BeforeActionList', Actions => \@results, ARGSRef => \%ARGS, Ticket => $Ticket);
<& /Elements/ListActions, actions => \@results &>
-<form action="ModifyLinks.html" method="post">
+<form action="ModifyLinks.html" name="ModifyLinks" method="post">
<input type="hidden" class="hidden" name="id" value="<%$Ticket->id%>" />
% $m->callback( CallbackName => 'FormStart', ARGSRef => \%ARGS );
% my (@extra);
my $members = $CloneTicketObj->Members;
my ( @members, @members_of, @refers, @refers_by, @depends, @depends_by );
my $refers = $CloneTicketObj->RefersTo;
+ my $get_link_value = sub {
+ my ($link, $type) = @_;
+ my $uri_method = $type . 'URI';
+ my $local_method = 'Local' . $type;
+ my $uri = $link->$uri_method;
+ return if $uri->IsLocal and
+ $uri->Object and
+ $uri->Object->isa('RT::Ticket') and
+ $uri->Object->Type eq 'reminder';
+
+ return $link->$local_method || $uri->URI;
+ };
while ( my $refer = $refers->Next ) {
- push @refers, $refer->LocalTarget;
+ my $refer_value = $get_link_value->($refer, 'Target');
+ push @refers, $refer_value if defined $refer_value;
}
$clone->{'new-RefersTo'} = join ' ', @refers;
my $refers_by = $CloneTicketObj->ReferredToBy;
while ( my $refer_by = $refers_by->Next ) {
- push @refers_by, $refer_by->LocalBase;
+ my $refer_by_value = $get_link_value->($refer_by, 'Base');
+ push @refers_by, $refer_by_value if defined $refer_by_value;
}
$clone->{'RefersTo-new'} = join ' ', @refers_by;
- if (0) { # Temporarily disabled
- my $depends = $CloneTicketObj->DependsOn;
- while ( my $depend = $depends->Next ) {
- push @depends, $depend->LocalTarget;
- }
- $clone->{'new-DependsOn'} = join ' ', @depends;
-
- my $depends_by = $CloneTicketObj->DependedOnBy;
- while ( my $depend_by = $depends_by->Next ) {
- push @depends_by, $depend_by->LocalBase;
- }
- $clone->{'DependsOn-new'} = join ' ', @depends_by;
-
- while ( my $member = $members->Next ) {
- push @members, $member->LocalBase;
- }
- $clone->{'MemberOf-new'} = join ' ', @members;
-
- my $members_of = $CloneTicketObj->MemberOf;
- while ( my $member_of = $members_of->Next ) {
- push @members_of, $member_of->LocalTarget;
- }
- $clone->{'new-MemberOf'} = join ' ', @members_of;
-
- }
my $cfs = $CloneTicketObj->QueueObj->TicketCustomFields();
while ( my $cf = $cfs->Next ) {