sub edit {
my ($action) = @_;
my (%data, $type, @objects);
- my ($cl, $text, $edit, $input, $output);
+ my ($cl, $text, $edit, $input, $output, $content_type);
use vars qw(%set %add %del);
%set = %add = %del = ();
if (/^-e$/) { $edit = 1 }
elsif (/^-i$/) { $input = 1 }
elsif (/^-o$/) { $output = 1 }
+ elsif (/^-ct$/) { $content_type = shift @ARGV }
elsif (/^-t$/) {
$bad = 1, last unless defined($type = get_type_argument());
}
return 0;
}
+ my @files;
+ @files = @{ vsplit($set{'attachment'}) } if exists $set{'attachment'};
+
my $synerr = 0;
EDIT:
# We'll let the user edit the form before sending it to the server,
# unless we have enough information to submit it non-interactively.
+ if ( $type && $type eq 'ticket' && $text !~ /^Content-Type:/m ) {
+ $text .= "Content-Type: $content_type\n"
+ if $content_type and $content_type ne "text/plain";
+ }
+
if ($edit || (!$input && !$cl)) {
- my $newtext = vi($text);
+ my ($newtext) = vi_form_while(
+ $text,
+ sub {
+ my ($text, $form) = @_;
+ return 1 unless exists $form->[2]{'Attachment'};
+
+ foreach my $f ( @{ vsplit($form->[2]{'Attachment'}) } ) {
+ return (0, "File '$f' doesn't exist") unless -f $f;
+ }
+ @files = @{ vsplit($form->[2]{'Attachment'}) };
+ return 1;
+ },
+ );
+ return $newtext unless $newtext;
# We won't resubmit a bad form unless it was changed.
$text = ($synerr && $newtext eq $text) ? undef : $newtext;
}
+ delete @data{ grep /^attachment_\d+$/, keys %data };
+ my $i = 1;
+ foreach my $file (@files) {
+ $data{"attachment_$i"} = bless([ $file ], "Attachment");
+ $i++;
+ }
+
if ($text) {
my $r = submit("$REST/edit", {content => $text, %data});
if ($r->code == 409) {
sub comment {
my ($action) = @_;
- my (%data, $id, @files, @bcc, @cc, $msg, $wtime, $edit);
+ my (%data, $id, @files, @bcc, @cc, $msg, $content_type, $wtime, $edit);
my $bad = 0;
while (@ARGV) {
if (/^-e$/) {
$edit = 1;
}
- elsif (/^-[abcmw]$/) {
+ elsif (/^-(?:[abcmw]|ct)$/) {
unless (@ARGV) {
whine "No argument specified with $_.";
$bad = 1; last;
}
push @files, shift @ARGV;
}
+ elsif (/-ct/) {
+ $content_type = shift @ARGV;
+ }
elsif (/-([bc])/) {
my $a = $_ eq "-b" ? \@bcc : \@cc;
@$a = split /\s*,\s*/, shift @ARGV;
while (<STDIN>) { $msg .= $_ }
}
}
-
elsif (/-w/) { $wtime = shift @ARGV }
}
elsif (!$id && m|^(?:ticket/)?($idlist)$|) {
my $form = [
"",
- [ "Ticket", "Action", "Cc", "Bcc", "Attachment", "TimeWorked", "Text" ],
+ [ "Ticket", "Action", "Cc", "Bcc", "Attachment", "TimeWorked", "Content-Type", "Text" ],
{
Ticket => $id,
Action => $action,
Bcc => [ @bcc ],
Attachment => [ @files ],
TimeWorked => $wtime || '',
+ 'Content-Type' => $content_type || 'text/plain',
Text => $msg || '',
Status => ''
}
my $text = Form::compose([ $form ]);
if ($edit || !$msg) {
- my $error = 0;
- my ($c, $o, $k, $e);
-
- do {
- my $ntext = vi($text);
- return if ($error && $ntext eq $text);
- $text = $ntext;
- $form = Form::parse($text);
- $error = 0;
-
- ($c, $o, $k, $e) = @{ $form->[0] };
- if ($e) {
- $error = 1;
- $c = "# Syntax error.";
- goto NEXT;
- }
- elsif (!@$o) {
- return 0;
- }
- @files = @{ vsplit($k->{Attachment}) };
-
- NEXT:
- $text = Form::compose([[$c, $o, $k, $e]]);
- } while ($error);
+ my ($tmp) = vi_form_while(
+ $text,
+ sub {
+ my ($text, $form) = @_;
+ foreach my $f ( @{ vsplit($form->[2]{'Attachment'}) } ) {
+ return (0, "File '$f' doesn't exist") unless -f $f;
+ }
+ @files = @{ vsplit($form->[2]{'Attachment'}) };
+ return 1;
+ },
+ );
+ return $tmp unless $tmp;
+ $text = $tmp;
}
my $i = 1;
return $passwd;
}
+sub vi_form_while {
+ my $text = shift;
+ my $cb = shift;
+
+ my $error = 0;
+ my ($c, $o, $k, $e);
+ do {
+ my $ntext = vi($text);
+ return undef if ($error && $ntext eq $text);
+
+ $text = $ntext;
+
+ my $form = Form::parse($text);
+ $error = 0;
+ ($c, $o, $k, $e) = @{ $form->[0] };
+ if ( $e ) {
+ $error = 1;
+ $c = "# Syntax error.";
+ goto NEXT;
+ }
+ elsif (!@$o) {
+ return 0;
+ }
+
+ my ($status, $msg) = $cb->( $text, [$c, $o, $k, $e] );
+ unless ( $status ) {
+ $error = 1;
+ $c = "# $msg";
+ }
+
+ NEXT:
+ $text = Form::compose([[$c, $o, $k, $e]]);
+ } while ($error);
+
+ return $text;
+}
+
sub vi {
my ($text) = @_;
my $editor = $ENV{EDITOR} || $ENV{VISUAL} || "vi";
}
push @words, $s;
}
- elsif ( $a =~ /^q{/ ) {
+ elsif ( $a =~ /^q\{/ ) {
my $s = $a;
- while ( $a !~ /}$/ ) {
+ while ( $a !~ /\}$/ ) {
( $a, $b ) =
split /\s*,\s*/, $b, 2;
$s .= ',' . $a;
}
- $s =~ s/^q{/'/;
- $s =~ s/}/'/;
+ $s =~ s/^q\{/'/;
+ $s =~ s/\}/'/;
push @words, $s;
}
else {
-S var=val
Submits the specified variable with the request.
-t type Specifies object type.
+ -ct content-type Specifies content type of message(tickets only).
Examples:
# Interactive (starts $EDITOR with a form).
rt edit ticket/3
rt create -t ticket
+ rt create -t ticket -ct text/html
# Non-interactive.
rt edit ticket/1-3 add cc=foo@example.com set priority=3 due=tomorrow
Options:
-m <text> Specify comment text.
+ -ct <content-type> Specify content-type of comment text.
-a <file> Attach a file to the comment. (May be used more
than once to attach multiple files.)
-c <addrs> A comma-separated list of Cc addresses.
Site-specific custom types (anything but ticket, reminder or approval)
are not affected by these changes.
+
+=head1 UPGRADING FROM 4.0.13 AND EARLIER
+
+=head2 Outgoing mail From: header
+
+The "Default" key of the C<$OverrideOutgoingMailFrom> config option now,
+as previously documented, only applies when no ticket is involved.
+Previously it was also used when a ticket was involved but the
+associated queue had no specific correspond address. In such cases the
+global correspond address is now used.
+
+The config option C<$SetOutgoingMailFrom> now accepts an email address
+as a value which will act as a global default. This covers the simple
+case of sending all bounces to a specific address, without the previous
+solution of resorting to defining all queues in
+$OverrideOutgoingMailFrom. Any definitions in the Override option
+(including Default) still take precedence. See
+L<RT_Config/$SetOutgoingMailFrom> for more information.
+
+=head2 Reminder statuses
+
+New reminders are now created in the "reminder_on_open" status defined in your
+lifecycles. For the default lifecycle, this means reminders will start as
+"open" instead of "new". This change is for consistency when a completed
+reminder is reopened at a later date. If you use custom lifecycles and added
+further transition restrictions, you may need to adjust the L<"reminder_on_open"
+setting|RT_Config/reminder_on_open> in your lifecycles.
+
+=head2 Bookmarks
+
+Previously, the list of Bookmarks on your homepage was unlimited (if you
+had 100 bookmarked tickets, you would see a 100 item list on your RT at
+a Glance). 'Bookmarked Tickets' now uses the same size limits as any
+other search on your homepage. This can be customized using the 'Rows
+per box' setting on your RT at a Glance configuration page.
+
+=head2 PostgreSQL 9.2
+
+If you are upgrading an RT from 3.8 (or earlier) to 4.0 on PostgreSQL
+9.2, you should make sure that you have installed DBD::Pg 2.19.3 or
+higher. If you start your upgrade without installing a recent-enough
+version of DBD::Pg RT will stop the upgrade during the 3.9.8 step and
+remind you to upgrade DBD::Pg. If this happens, you can re-start your
+upgrade by running:
+
+ ./sbin/rt-setup-database --action insert --datadir etc/upgrade/3.9.8/
+
+Followed by re-running make upgrade-database and answering 3.9.8 when
+prompted for which RT version you're upgrading from.
+>>>>>>> 4.0/pg-9.2-compatibility
( mysqldump rt4 --tables sessions --no-data; \
mysqldump rt4 --ignore-table rt4.sessions --single-transaction ) \
- | gzip > rt-`date +%Y%M%d`.sql.gz
+ | gzip > rt-`date +%Y%m%d`.sql.gz
If you're using a MySQL version older than 4.1.2 (only supported on RT 3.8.x
and older), you should be also pass the C<--default-character-set=binary>
( pg_dump rt4 --table=sessions --schema-only; \
pg_dump rt4 --exclude-table=sessions ) \
- | gzip > rt-`date +%Y%M%d`.sql.gz
+ | gzip > rt-`date +%Y%m%d`.sql.gz
=head2 FILESYSTEM
Simply saving a tarball should be sufficient, with something like:
- tar czvpf rt-backup-`date +%Y%M%d`.tar.gz /opt/rt4 /etc/aliases /etc/httpd ...
+ tar czvpf rt-backup-`date +%Y%m%d`.tar.gz /opt/rt4 /etc/aliases /etc/httpd ...
Be sure to include all the directories and files you enumerated above!
Additionally, the C<MemberOf> field is specially handled to make it easier to
add the new group to other groups. C<MemberOf> may be a single value or an
array ref. Each value should be a user-defined group name or hashref to pass
-into L<< RT::Group->LoadByCols >>. Each group found will have the new group
+into L<RT::Group/LoadByCols>. Each group found will have the new group
added as a member.
Unfortunately you can't specify the I<members> of a group at this time. As a
to a temporary file. RT will log the location of the temporary file
so you can extract mail from it afterward.
+On shutdown, RT will clean up the temporary file created when using
+the 'testfile' option. If testing while the RT server is still running,
+you can find the files in the location noted in the log file. If you run
+a tool like C<rt-crontool> however, or if you look after stopping the server,
+the files will have been deleted when the process completed. If you need to
+keep the files for development or debugging, you can manually set
+C<< UNLINK => 0 >> where the testfile config is processed in
+F<lib/RT/Interface/Email.pm>.
+
=cut
Set($MailCommand, "sendmailpipe");
Warning: If you use this setting, bounced mails will appear to be
incoming mail to the system, thus creating new tickets.
+If the value contains an C<@>, it is assumed to be an email address and used as
+a global envelope sender. Expected usage in this case is to simply set the
+same envelope sender on all mail from RT, without defining
+C<$OverrideOutgoingMailFrom>. If you do define C<$OverrideOutgoingMailFrom>,
+anything specified there overrides the global value (including Default).
+
This option only works if C<$MailCommand> is set to 'sendmailpipe'.
=cut
--- /dev/null
+sub acl {
+ return ();
+}
+1;
--- /dev/null
+sub acl {
+ my $dbh = shift;
+
+ my @acls;
+
+ my @tables = qw (
+ attributes_id_seq
+ attributes
+ );
+
+ foreach my $table (@tables) {
+ push @acls,
+ "GRANT SELECT, INSERT, UPDATE, DELETE ON $table to "
+ . RT->Config->Get('DatabaseUser') . ";";
+
+ }
+ return (@acls);
+}
+1;
--- /dev/null
+sub acl {
+ return ();
+}
+1;
--- /dev/null
+sub acl {
+ return ();
+}
+1;
--- /dev/null
+# nothing to do
+1;
--- /dev/null
+CREATE SEQUENCE ATTRIBUTES_seq;
+CREATE TABLE Attributes (
+ id NUMBER(11,0) PRIMARY KEY,
+ Name VARCHAR2(255) NOT NULL,
+ Description VARCHAR2(255),
+ Content CLOB,
+ ContentType VARCHAR(16),
+ ObjectType VARCHAR2(25) NOT NULL,
+ ObjectId NUMBER(11,0) DEFAULT 0 NOT NULL,
+ Creator NUMBER(11,0) DEFAULT 0 NOT NULL,
+ Created DATE,
+ LastUpdatedBy NUMBER(11,0) DEFAULT 0 NOT NULL,
+ LastUpdated DATE
+);
+
+CREATE INDEX Attributes1 on Attributes(Name);
+CREATE INDEX Attributes2 on Attributes(ObjectType, ObjectId);
--- /dev/null
+
+
+CREATE SEQUENCE attributes_id_seq;
+
+CREATE TABLE Attributes (
+ id INTEGER DEFAULT nextval('attributes_id_seq'),
+ Name varchar(255) NOT NULL ,
+ Description varchar(255) NULL ,
+ Content text,
+ ContentType varchar(16),
+ ObjectType varchar(64),
+ ObjectId integer,
+ Creator integer NOT NULL DEFAULT 0 ,
+ Created TIMESTAMP NULL ,
+ LastUpdatedBy integer NOT NULL DEFAULT 0 ,
+ LastUpdated TIMESTAMP NULL ,
+ PRIMARY KEY (id)
+
+);
+
+CREATE INDEX Attributes1 on Attributes(Name);
+CREATE INDEX Attributes2 on Attributes(ObjectType, ObjectId);
+
+
+
--- /dev/null
+--- {{{ Attributes
+CREATE TABLE Attributes (
+ id INTEGER PRIMARY KEY ,
+ Name varchar(255) NOT NULL ,
+ Description varchar(255) NULL ,
+ Content LONGTEXT NULL ,
+ ContentType varchar(16),
+ ObjectType varchar(25) NOT NULL ,
+ ObjectId INTEGER default 0,
+ Creator integer NULL ,
+ Created DATETIME NULL ,
+ LastUpdatedBy integer NULL ,
+ LastUpdated DATETIME NULL
+
+) ;
+
+CREATE INDEX Attributes1 on Attributes(Name);
+CREATE INDEX Attributes2 on Attributes(ObjectType, ObjectId);
+
+--- }}}
+
--- /dev/null
+
+CREATE TABLE Attributes (
+ id INTEGER NOT NULL AUTO_INCREMENT,
+ Name varchar(255) NULL ,
+ Description varchar(255) NULL ,
+ Content text,
+ ContentType varchar(16),
+ ObjectType varchar(64),
+ ObjectId integer, # foreign key to anything
+ Creator integer NOT NULL DEFAULT 0 ,
+ Created DATETIME NULL ,
+ LastUpdatedBy integer NOT NULL DEFAULT 0 ,
+ LastUpdated DATETIME NULL ,
+ PRIMARY KEY (id)
+) ENGINE=InnoDB;
+
+CREATE INDEX Attributes1 on Attributes(Name);
+CREATE INDEX Attributes2 on Attributes(ObjectType, ObjectId);
+
--- /dev/null
+@Scrips = (
+ { ScripCondition => 'On Owner Change',
+ ScripAction => 'Notify Owner',
+ Template => 'Transaction' },
+);
+
+1;
--- /dev/null
+@ScripActions = (
+ { Name => 'Notify Ccs as Comment', # loc
+ Description => 'Sends mail to the Ccs as a comment', # loc
+ ExecModule => 'NotifyAsComment',
+ Argument => 'Cc' },
+ { Name => 'Notify Ccs', # loc
+ Description => 'Sends mail to the Ccs', # loc
+ ExecModule => 'Notify',
+ Argument => 'Cc' },
+);
+
+
+@ScripConditions = (
+ {
+ Name => 'On Priority Change', # loc
+ Description => 'Whenever a ticket\'s priority changes', # loc
+ ApplicableTransTypes => 'Set',
+ ExecModule => 'PriorityChange',
+ },
+);
+
+1;
--- /dev/null
+sub acl {
+ return ();
+}
+1;
--- /dev/null
+sub acl {
+ my $dbh = shift;
+
+ my @acls;
+
+ my @tables = qw (
+ objectcustomfieldvalues
+ objectcustomfields_id_s
+ objectcustomfields
+ );
+
+ foreach my $table (@tables) {
+ push @acls,
+ "GRANT SELECT, INSERT, UPDATE, DELETE ON $table to "
+ . RT->Config->Get('DatabaseUser') . ";";
+
+ }
+ return (@acls);
+}
+1;
--- /dev/null
+sub acl {
+ return ();
+}
+1;
--- /dev/null
+sub acl {
+ return ();
+}
+1;
--- /dev/null
+alter Table Transactions ADD ObjectType VARCHAR2(64);
+UPDATE Transactions set ObjectType = 'RT::Ticket';
+ALTER TABLE Transactions modify ObjectType NOT NULL;
+ALTER TABLE Transactions drop column EffectiveTicket;
+ALTER TABLE Transactions ADD ReferenceType VARCHAR2(255) NULL;
+ALTER TABLE Transactions ADD OldReference NUMBER(11,0) NULL;
+ALTER TABLE Transactions ADD NewReference NUMBER(11,0) NULL;
+DROP INDEX transactions1;
+ALTER TABLE Transactions rename column Ticket to ObjectId;
+CREATE INDEX Transactions1 ON Transactions (ObjectType, ObjectId);
+
+ALTER TABLE TicketCustomFieldValues rename to ObjectCustomFieldValues;
+ALTER TABLE ObjectCustomFieldValues rename column Ticket to ObjectId;
+ALTER TABLE ObjectCustomFieldValues ADD ObjectType VARCHAR2(255);
+UPDATE ObjectCustomFieldValues set ObjectType = 'RT::Ticket';
+ALTER TABLE ObjectCustomFieldValues MODIFY ObjectType NOT NULL;
+ALTER TABLE ObjectCustomFieldValues ADD Disabled NUMBER(11,0);
+ALTER TABLE ObjectCustomFieldValues MODIFY Disabled default 0;
+UPDATE ObjectCustomFieldValues SET Disabled = 0;
+ALTER TABLE ObjectCustomFieldValues MODIFY Disabled NOT NULL;
+ALTER TABLE ObjectCustomFieldValues ADD LargeContent CLOB NULL;
+ALTER TABLE ObjectCustomFieldValues ADD ContentType VARCHAR2(80) NULL;
+ALTER TABLE ObjectCustomFieldValues ADD ContentEncoding VARCHAR2(80) NULL;
+ALTER TABLE ObjectCustomFieldValues ADD SortOrder NUMBER(11,0) DEFAULT 0 NOT NULL;
+
+
+
+CREATE INDEX ObjectCustomFieldValues1 on ObjectCustomFieldValues (CustomField,ObjectType,ObjectId,Content);
+CREATE INDEX ObjectCustomFieldValues2 on ObjectCustomFieldValues (CustomField,ObjectType,ObjectId);
+
+
+
+CREATE SEQUENCE OBJECTCUSTOMFIELDS_seq;
+CREATE TABLE ObjectCustomFields (
+ id NUMBER(11,0)
+ CONSTRAINT ObjectCustomFields_Key PRIMARY KEY,
+ CustomField NUMBER(11,0) NOT NULL,
+ ObjectId NUMBER(11,0) NOT NULL,
+ SortOrder NUMBER(11,0) DEFAULT 0 NOT NULL,
+ Creator NUMBER(11,0) DEFAULT 0 NOT NULL,
+ Created DATE,
+ LastUpdatedBy NUMBER(11,0) DEFAULT 0 NOT NULL,
+ LastUpdated DATE
+);
+
+
+INSERT into ObjectCustomFields (id, CustomField, ObjectId, SortOrder, Creator, LastUpdatedBy) SELECT objectcustomfields_seq.nextval, id, Queue, SortOrder, Creator, LastUpdatedBy from CustomFields;
+
+ALTER TABLE CustomFields ADD LookupType VARCHAR2(255);
+ALTER TABLE CustomFields ADD Repeated NUMBER(11,0);
+ALTER TABLE CustomFields ADD Pattern VARCHAR2(255) NULL;
+ALTER TABLE CustomFields ADD MaxValues NUMBER(11,0);
+
+UPDATE CustomFields SET MaxValues = 0 WHERE Type LIKE '%Multiple';
+UPDATE CustomFields SET MaxValues = 1 WHERE Type LIKE '%Single';
+UPDATE CustomFields SET Type = 'Select' WHERE Type LIKE 'Select%';
+UPDATE CustomFields SET Type = 'Freeform' WHERE Type LIKE 'Freeform%';
+UPDATE CustomFields Set LookupType = 'RT::Queue-RT::Ticket';
+ALTER TABLE CustomFields MODIFY LookupType NOT NULL;
+UPDATE CustomFields Set Repeated = 0;
+ALTER TABLE CustomFields MODIFY Repeated DEFAULT 0;
+ALTER TABLE CustomFields MODIFY Repeated NOT NULL;
+ALTER TABLE CustomFields drop column Queue;
+
+
--- /dev/null
+alter Table Transactions ADD Column ObjectType varchar(64);
+update Transactions set ObjectType = 'RT::Ticket';
+ALTER TABLE Transactions ALTER COLUMN ObjectType SET NOT NULL;
+alter table Transactions drop column EffectiveTicket;
+alter table Transactions add column ReferenceType varchar(255) NULL;
+alter table Transactions add column OldReference integer NULL;
+alter table Transactions add column NewReference integer NULL;
+drop index transactions1;
+alter table Transactions rename column Ticket to ObjectId;
+
+
+CREATE INDEX Transactions1 ON Transactions (ObjectType, ObjectId);
+
+alter table TicketCustomFieldValues rename to ObjectCustomFieldValues;
+
+alter table ObjectCustomFieldValues rename column Ticket to ObjectId;
+
+alter table objectcustomfieldvalues add column ObjectType varchar(255);
+
+update objectcustomfieldvalues set ObjectType = 'RT::Ticket';
+
+ALTER TABLE objectcustomfieldvalues ALTER COLUMN ObjectType SET NOT NULL;
+
+alter table objectcustomfieldvalues add column Current int;
+
+alter table objectcustomfieldvalues alter column Current SET default 1;
+
+UPDATE objectcustomfieldvalues SET Current = 1;
+
+alter table objectcustomfieldvalues add column LargeContent TEXT NULL;
+
+alter table objectcustomfieldvalues add column ContentType varchar(80) NULL;
+
+alter table objectcustomfieldvalues add column ContentEncoding varchar(80) NULL;
+
+create index ObjectCustomFieldValues1 on objectcustomfieldvalues (CustomField,ObjectType,ObjectId,Content);
+
+create index ObjectCustomFieldValues2 on objectcustomfieldvalues (CustomField,ObjectType,ObjectId);
+
+
+CREATE SEQUENCE objectcustomfields_id_s;
+
+CREATE TABLE ObjectCustomFields (
+ id INTEGER DEFAULT nextval('objectcustomfields_id_s'),
+ CustomField integer NOT NULL,
+ ObjectId integer NOT NULL,
+ SortOrder integer NOT NULL DEFAULT 0 ,
+
+ Creator integer NOT NULL DEFAULT 0 ,
+ Created TIMESTAMP NULL ,
+ LastUpdatedBy integer NOT NULL DEFAULT 0 ,
+ LastUpdated TIMESTAMP NULL ,
+ PRIMARY KEY (id)
+
+);
+
+
+INSERT into ObjectCustomFields (CustomField, ObjectId, SortOrder, Creator, LastUpdatedBy) SELECT id, Queue, SortOrder, Creator, LastUpdatedBy from CustomFields;
+
+alter table CustomFields add column LookupType varchar(255);
+alter table CustomFields add column Repeated int2;
+alter table CustomFields add column Pattern varchar(255) NULL;
+alter table CustomFields add column MaxValues integer;
+
+UPDATE CustomFields SET MaxValues = 0 WHERE Type LIKE '%Multiple';
+UPDATE CustomFields SET MaxValues = 1 WHERE Type LIKE '%Single';
+UPDATE CustomFields SET Type = 'Select' WHERE Type LIKE 'Select%';
+UPDATE CustomFields SET Type = 'Freeform' WHERE Type LIKE 'Freeform%';
+UPDATE CustomFields Set LookupType = 'RT::Queue-RT::Ticket';
+ALTER TABLE CustomFields ALTER COLUMN LookupType SET NOT NULL;
+UPDATE CustomFields Set Repeated = 0;
+ALTER TABLE CustomFields ALTER COLUMN Repeated SET DEFAULT 0;
+ALTER TABLE CustomFields ALTER COLUMN Repeated SET NOT NULL;
+alter table CustomFields drop column Queue;
--- /dev/null
+drop index transactions1 ON Transactions;
+
+alter Table Transactions
+ ADD COLUMN (ObjectType varchar(64) not null),
+ DROP COLUMN EffectiveTicket,
+ ADD COLUMN ReferenceType varchar(255) NULL,
+ ADD COLUMN OldReference integer NULL,
+ ADD COLUMN NewReference integer NULL,
+ CHANGE Ticket ObjectId integer NOT NULL DEFAULT 0;
+
+UPDATE Transactions set ObjectType = 'RT::Ticket';
+CREATE INDEX Transactions1 ON Transactions (ObjectType, ObjectId);
+
+alter table TicketCustomFieldValues rename ObjectCustomFieldValues,
+ change Ticket ObjectId integer NOT NULL DEFAULT 0 ,
+ add column ObjectType varchar(255) not null,
+ add column Current bool default 1,
+ add column LargeContent LONGTEXT NULL,
+ add column ContentType varchar(80) NULL,
+ add column ContentEncoding varchar(80) NULL;
+
+update ObjectCustomFieldValues set ObjectType = 'RT::Ticket';
+
+# These could fail if there's no such index and there's no "drop index if exists" syntax
+#alter table ObjectCustomFieldValues drop index ticketcustomfieldvalues1;
+#alter table ObjectCustomFieldValues drop index ticketcustomfieldvalues2;
+
+alter table ObjectCustomFieldValues add index ObjectCustomFieldValues1 (Content),
+ add index ObjectCustomFieldValues2 (CustomField,ObjectType,ObjectId);
+
+
+CREATE TABLE ObjectCustomFields (
+ id INTEGER NOT NULL AUTO_INCREMENT,
+ CustomField int NOT NULL ,
+ ObjectId integer NOT NULL,
+ SortOrder integer NOT NULL DEFAULT 0 ,
+
+ Creator integer NOT NULL DEFAULT 0 ,
+ Created DATETIME NULL ,
+ LastUpdatedBy integer NOT NULL DEFAULT 0 ,
+ LastUpdated DATETIME NULL ,
+ PRIMARY KEY (id)
+) ENGINE=InnoDB;
+
+
+INSERT into ObjectCustomFields (id, CustomField, ObjectId, SortOrder, Creator, LastUpdatedBy) SELECT null, id, Queue, SortOrder, Creator, LastUpdatedBy from CustomFields;
+
+alter table CustomFields add column LookupType varchar(255) NOT NULL,
+ add column Repeated int2 NOT NULL DEFAULT 0 ,
+ add column Pattern varchar(255) NULL,
+ add column MaxValues integer;
+# See above
+# alter table CustomFields drop index CustomFields1;
+
+UPDATE CustomFields SET MaxValues = 0 WHERE Type LIKE '%Multiple';
+UPDATE CustomFields SET MaxValues = 1 WHERE Type LIKE '%Single';
+UPDATE CustomFields SET Type = 'Select' WHERE Type LIKE 'Select%';
+UPDATE CustomFields SET Type = 'Freeform' WHERE Type LIKE 'Freeform%';
+UPDATE CustomFields Set LookupType = 'RT::Queue-RT::Ticket';
+alter table CustomFields drop column Queue;
--- /dev/null
+sub acl {
+ return ();
+}
+1;
--- /dev/null
+sub acl {
+ return ();
+}
+1;
--- /dev/null
+sub acl {
+ return ();
+}
+1;
--- /dev/null
+sub acl {
+ return ();
+}
+1;
--- /dev/null
+ALTER TABLE ObjectCustomFieldValues ADD COLUMN SortOrder INTEGER;
+UPDATE ObjectCustomFieldValues SET SortOrder = 0;
+ALTER TABLE ObjectCustomFieldValues ALTER COLUMN SortOrder SET DEFAULT 0;
+ALTER TABLE ObjectCustomFieldValues ALTER COLUMN SortOrder SET NOT NULL;
+ALTER TABLE ObjectCustomFieldValues ADD COLUMN Disabled INTEGER;
+UPDATE ObjectCustomFieldValues SET Disabled = 1 WHERE Current = 0;
+UPDATE ObjectCustomFieldValues SET Disabled = 0 WHERE Current != 0;
+ALTER TABLE ObjectCustomFieldValues ALTER COLUMN Disabled SET DEFAULT 0;
+ALTER TABLE ObjectCustomFieldValues ALTER COLUMN Disabled SET NOT NULL;
+
+ALTER TABLE ObjectCustomFieldValues DROP COLUMN Current;
--- /dev/null
+ALTER TABLE ObjectCustomFieldValues ADD COLUMN SortOrder INTEGER NOT NULL DEFAULT 0,
+ ADD COLUMN Disabled int2 NOT NULL DEFAULT 0;
+
+UPDATE ObjectCustomFieldValues SET Disabled = 1 WHERE Current = 0;
+ALTER TABLE ObjectCustomFieldValues DROP COLUMN Current;
--- /dev/null
+@Attributes = (
+ { Name => 'Search - My Tickets',
+ Description => '[_1] highest priority tickets I own',
+ Content =>
+ { Format => q{'<a href="__WebPath__/Ticket/Display.html?id=__id__">__id__</a>/TITLE:#', '<a href="__WebPath__/Ticket/Display.html?id=__id__">__Subject__</a>/TITLE:Subject', Priority, QueueName, ExtendedStatus},
+ Query => " Owner = '__CurrentUser__' AND ( Status = 'new' OR Status = 'open')",
+ OrderBy => 'Priority',
+ Order => 'DESC' },
+ },
+ { Name => 'Search - Unowned Tickets',
+ Description => '[_1] newest unowned tickets',
+ Content =>
+ { Format => "'<a href=\"__WebPath__/Ticket/Display.html?id=__id__\">__id__</a>/TITLE:#', '<a href=\"__WebPath__/Ticket/Display.html?id=__id__\">__Subject__</a>/TITLE:Subject', QueueName, ExtendedStatus, CreatedRelative, '<A HREF=\"__WebPath__/Ticket/Display.html?Action=Take&id=__id__\">__loc(Take)__</a>/TITLE: ' ",
+ Query => " Owner = 'Nobody' AND ( Status = 'new' OR Status = 'open')",
+ OrderBy => 'Created',
+ Order => 'DESC' },
+ },
+ { Name => 'HomepageSettings',
+ Description => 'HomepageSettings',
+ Content =>
+ { 'body' =>
+ [ { type => 'system', name => 'My Tickets' },
+ { type => 'system', name => 'Unowned Tickets' },
+ { type => 'component', name => 'QuickCreate'},
+ ],
+ 'summary' =>
+ [
+ { type => 'component', name => 'MyReminders' },
+ { type => 'component', name => 'Quicksearch' },
+ { type => 'component', name => 'RefreshHomepage' },
+ ]
+ },
+}
+);
+
+1;
--- /dev/null
+@ScripConditions = (
+ { Name => 'On Close', # loc
+ Description => 'Whenever a ticket is closed', # loc
+ ApplicableTransTypes => 'Status,Set',
+ ExecModule => 'CloseTicket',
+ },
+ { Name => 'On Reopen', # loc
+ Description => 'Whenever a ticket is reopened', # loc
+ ApplicableTransTypes => 'Status,Set',
+ ExecModule => 'ReopenTicket',
+ },
+);
+
+
--- /dev/null
+
+@Templates = (
+ { Queue => 0,
+ Name => "Error: public key", # loc
+ Description =>
+ "Inform user that he has problems with public key and couldn't recieve encrypted content", # loc
+ Content => q{Subject: We have no your public key or it's wrong
+
+You received this message as we have no your public PGP key or we have a problem with your key. Inform the administrator about the problem.
+}
+ },
+ { Queue => 0,
+ Name => "Error to RT owner: public key", # loc
+ Description =>
+ "Inform RT owner that user(s) have problems with public keys", # loc
+ Content => q{Subject: Some users have problems with public keys
+
+You received this message as RT has problems with public keys of the following user:
+{
+ foreach my $e ( @BadRecipients ) {
+ $OUT .= "* ". $e->{'Message'} ."\n";
+ }
+}}
+ },
+ { Queue => 0,
+ Name => "Error: no private key", # loc
+ Description =>
+ "Inform user that we received an encrypted email and we have no private keys to decrypt", # loc
+ Content => q{Subject: we received message we cannot decrypt
+
+You sent an encrypted message with subject '{ $Message->head->get('Subject') }',
+but we have no private key it's encrypted to.
+
+Please, check that you encrypt messages with correct keys
+or contact the system administrator.}
+ },
+ { Queue => 0,
+ Name => "Error: bad GnuPG data", # loc
+ Description =>
+ "Inform user that a message he sent has invalid GnuPG data", # loc
+ Content => q{Subject: We received a message we cannot handle
+
+You sent us a message that we cannot handle due to corrupted GnuPG signature or encrypted block. we get the following error(s):
+{ foreach my $msg ( @Messages ) {
+ $OUT .= "* $msg\n";
+ }
+}}
+ },
+);
--- /dev/null
+
+@Templates = (
+ { Queue => 0,
+ Name => "Forward", # loc
+ Description => "Heading of a forwarded message", # loc
+ Content => q{
+
+This is forward of transaction #{ $Transaction->id } of a ticket #{ $Ticket->id }
+}
+ },
+);
+
--- /dev/null
+
+{ use strict;
+add_description_to_all_scrips();
+
+sub add_description_to_all_scrips {
+ require RT::Scrips;
+ my $scrips = RT::Scrips->new( RT->SystemUser );
+ $scrips->Limit( FIELD => 'Description', OPERATOR => 'IS', VALUE => 'NULL' );
+ $scrips->Limit( FIELD => 'Description', VALUE => '' );
+ while ( my $scrip = $scrips->Next ) {
+ my $desc = $scrip->Description;
+ next if defined $desc && length $desc;
+
+ $desc = gen_scrip_description( $scrip );
+
+ my ($status, $msg) = $scrip->SetDescription( $desc );
+ unless ( $status ) {
+ print STDERR "Couldn't set description of a scrip: $msg";
+ } else {
+ print "Added description to scrip #". $scrip->id ."\n";
+ }
+ }
+}
+
+sub gen_scrip_description {
+ my $scrip = shift;
+ my $condition = $scrip->ConditionObj->Name
+ || $scrip->ConditionObj->Description
+ || ('On Condition #'. $scrip->Condition);
+ my $action = $scrip->ActionObj->Name
+ || $scrip->ActionObj->Description
+ || ('Run Action #'. $scrip->Action);
+ return join ' ', $condition, $action;
+}
+}
+
+1;
--- /dev/null
+alter table CustomFields add Pattern_TMP clob;
+update CustomFields set Pattern_TMP = Pattern;
+commit;
+alter table CustomFields drop column Pattern;
+alter table CustomFields rename column Pattern_TMP to Pattern;
--- /dev/null
+ALTER TABLE customfields ALTER COLUMN pattern TYPE VARCHAR(65536);
--- /dev/null
+ALTER TABLE CustomFields CHANGE Pattern Pattern TEXT NULL;
--- /dev/null
+CREATE INDEX CachedGroupMembers3 on CachedGroupMembers (MemberId, ImmediateParentId);
+
--- /dev/null
+CREATE INDEX CachedGroupMembers3 on CachedGroupMembers (MemberId, ImmediateParentId);
+
--- /dev/null
+@Attributes = (
+ { Name => 'Search - Bookmarked Tickets',
+ Description => 'Bookmarked Tickets', #loc
+ Content =>
+ { Format => q{'<a href="__WebPath__/Ticket/Display.html?id=__id__">__id__</a>/TITLE:#',}
+ . q{'<a href="__WebPath__/Ticket/Display.html?id=__id__">__Subject__</a>/TITLE:Subject',}
+ . q{Priority, QueueName, ExtendedStatus, Bookmark},
+ Query => "__Bookmarks__",
+ OrderBy => 'LastUpdated',
+ Order => 'DESC' },
+ },
+);
+
--- /dev/null
+@Templates = (
+
+ { Queue => '0',
+ Name => 'Email Digest', # loc
+ Description => 'Email template for periodic notification digests', # loc
+ Content => q[Subject: RT Email Digest
+
+{ $Argument }
+],
+ },
+);
--- /dev/null
+@Final = (
+ sub {
+ $RT::Logger->debug("Adding search for bookmarked tickets to defaults");
+ my $sys = RT::System->new(RT->SystemUser);
+
+ my $attrs = RT::Attributes->new( RT->SystemUser );
+ $attrs->LimitToObject( $sys );
+ my ($attr) = $attrs->Named( 'HomepageSettings' );
+ unless ($attr) {
+ $RT::Logger->error("You have no global home page settings");
+ return;
+ }
+ my $content = $attr->Content;
+ unshift @{ $content->{'body'} ||= [] },
+ { type => 'system', name => 'Bookmarked Tickets' };
+
+ my ($status, $msg) = $attr->SetContent( $content );
+ $RT::Logger->error($msg) unless $status;
+
+ $RT::Logger->debug("done.");
+ return 1;
+ },
+);
--- /dev/null
+@Templates = (
+{
+ Queue => 0,
+ Name => "Error: Missing dashboard", # loc
+ Description =>
+ "Inform user that a dashboard he subscribed to is missing", # loc
+ Content => q{Subject: [{RT->Config->Get('rtname')}] Missing dashboard!
+
+Greetings,
+
+You are subscribed to a dashboard that is currently missing. Most likely, the dashboard was deleted.
+
+RT will remove this subscription as it is no longer useful. Here's the information RT had about your subscription:
+
+DashboardID: { $SubscriptionObj->SubValue('DashboardId') }
+Frequency: { $SubscriptionObj->SubValue('Frequency') }
+Hour: { $SubscriptionObj->SubValue('Hour') }
+{
+ $SubscriptionObj->SubValue('Frequency') eq 'weekly'
+ ? "Day of week: " . $SubscriptionObj->SubValue('Dow')
+ : $SubscriptionObj->SubValue('Frequency') eq 'monthly'
+ ? "Day of month: " . $SubscriptionObj->SubValue('Dom')
+ : ''
+}
+}
+},
+);
+
--- /dev/null
+#!/usr/bin/perl
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
+# <sales@bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+use strict;
+use warnings;
+
+use lib "local/lib";
+use lib "lib";
+
+
+use RT;
+RT::LoadConfig();
+RT::Init();
+
+use RT::Queues;
+
+my $queues = RT::Queues->new( RT->SystemUser );
+$queues->UnLimit();
+while ( my $queue = $queues->Next ) {
+ print "Processing queue ". ($queue->Name || $queue->id) ."...\n";
+ my $old_attr = $queue->FirstAttribute('BrandedSubjectTag');
+ unless ( $old_attr ) {
+ print "\thas no old-style subject tag. skipping\n";
+ next;
+ }
+ my $old_value = $old_attr->Content;
+ unless ( $old_value ) {
+ print "\thas empty old-style subject tag\n";
+ } else {
+ my ($status, $msg) = $queue->SetSubjectTag( $old_value );
+ unless ( $status ) {
+ print STDERR "\tERROR. Couldn't set tag: $msg\n";
+ next;
+ } else {
+ print "\thave set new-style subject tag to '$old_value'\n";
+ }
+ }
+
+ my ($status, $msg) = $queue->DeleteAttribute('BrandedSubjectTag');
+ unless ( $status ) {
+ print STDERR "\tERROR. Couldn't delete old-style tag: $msg\n";
+ next;
+ } else {
+ print "\tdeleted old-style tag entry\n";
+ }
+ print "\tDONE\n";
+}
+
+exit 0;
+
--- /dev/null
+#!/usr/bin/perl
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
+# <sales@bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+use strict;
+use warnings;
+
+use lib "local/lib";
+use lib "lib";
+
+
+use RT;
+RT::LoadConfig();
+RT::Init();
+
+use RT::Attributes;
+my $attrs = RT::Attributes->new( RT->SystemUser );
+$attrs->Limit(FIELD => 'ObjectType', OPERATOR=> '=', VALUE => 'RT::User');
+$attrs->Limit(FIELD => 'Name', OPERATOR=> '=', VALUE => 'ical-auth-token');
+while ( my $attr = $attrs->Next ) {
+ my $uid = $attr->ObjectId;
+ print "Processing auth token of user #". $uid ."...\n";
+
+ my $user = RT::User->new( RT->SystemUser );
+ $user->Load( $uid );
+ unless ( $user->id ) {
+ print STDERR "\tERROR. Couldn't load user record\n";
+ next;
+ }
+
+ my ($status, $msg);
+
+ ($status, $msg) = $user->DeleteAttribute('AuthToken')
+ if $user->FirstAttribute('AuthToken');
+ unless ( $status ) {
+ print STDERR "\tERROR. Couldn't delete duplicated attribute: $msg\n";
+ next;
+ } else {
+ print "\tdeleted duplicate attribute\n";
+ }
+
+ ($status, $msg) = $attr->SetName('AuthToken');
+ unless ( $status ) {
+ print STDERR "\tERROR. Couldn't rename attribute: $msg\n";
+ next;
+ } else {
+ print "\trenamed attribute\n";
+ }
+ print "\tDONE\n";
+}
+
+exit 0;
--- /dev/null
+@Final = (
+ # by incident we've changed 'My Bookmarks' to 'Bookmarked Tickets' when
+ # 3.7.82 upgrade script still was creating 'My Bookmarks', try to fix it
+ sub {
+ $RT::Logger->debug("Going to rename 'My Bookmarks' to 'Bookmarked Tickets'");
+ my $sys = RT::System->new(RT->SystemUser);
+
+ my $attrs = RT::Attributes->new( RT->SystemUser );
+ $attrs->LimitToObject( $sys );
+ my ($attr) = $attrs->Named( 'Search - My Bookmarks' );
+ unless ($attr) {
+ $RT::Logger->debug("You have no global search 'My Bookmarks'. Skipped.");
+ return 1;
+ }
+ my ($status, $msg) = $attr->SetName( 'Search - Bookmarked Tickets' );
+ $RT::Logger->error($msg) and return undef unless $status;
+
+ $RT::Logger->debug("Renamed.");
+ return 1;
+ },
+);
+
--- /dev/null
+@Final = (
+ sub {
+ $RT::Logger->debug("Going to adjust 'Bookmarked Tickets'");
+ my $sys = RT::System->new(RT->SystemUser);
+
+ my $attrs = RT::Attributes->new( RT->SystemUser );
+ $attrs->LimitToObject( $sys );
+ my ($attr) = $attrs->Named( 'Search - Bookmarked Tickets' );
+ unless ($attr) {
+ $RT::Logger->debug("You have no global search 'Bookmarked Tickets'. Skipped.");
+ return 1;
+ }
+ my $props = $attr->Content;
+ $props->{'Query'} =~ s/__Bookmarks__/id = '__Bookmarked__'/g;
+
+ my ($status, $msg) = $attr->SetContent( $props );
+ $RT::Logger->error($msg) and return undef unless $status;
+
+ $RT::Logger->debug("Fixed.");
+ return 1;
+ },
+);
+
+
--- /dev/null
+@Initial = (
+ sub {
+ $RT::Logger->warning(
+ "Going to add [OLD] prefix to all templates in approvals queue."
+ ." If you have never used approvals, you can safely delete all the"
+ ." templates with the [OLD] prefix. Leave the new Approval templates because"
+ ." you may eventually want to start using approvals."
+ );
+
+ my $approvals_q = RT::Queue->new( RT->SystemUser );
+ $approvals_q->Load('___Approvals');
+ unless ( $approvals_q->id ) {
+ $RT::Logger->error("You have no approvals queue.");
+ return 1;
+ }
+
+ my $templates = RT::Templates->new( RT->SystemUser );
+ $templates->LimitToQueue( $approvals_q->id );
+ while ( my $tmpl = $templates->Next ) {
+ my ($status, $msg) = $tmpl->SetName( "[OLD] ". $tmpl->Name );
+ unless ( $status ) {
+ $RT::Logger->error("Couldn't rename template #". $tmpl->id .": $msg");
+ }
+ }
+ return 1;
+ },
+);
+@ACL = (
+ { GroupDomain => 'SystemInternal',
+ GroupType => 'privileged',
+ Right => 'ShowApprovalsTab', },
+);
+
+@Templates = (
+ { Queue => '___Approvals',
+ Name => "New Pending Approval", # loc
+ Description =>
+ "Notify Owners and AdminCcs of new items pending their approval", # loc
+ Content => 'Subject: New Pending Approval: {$Ticket->Subject}
+
+Greetings,
+
+There is a new item pending your approval: "{$Ticket->Subject()}",
+a summary of which appears below.
+
+Please visit {RT->Config->Get(\'WebURL\')}Approvals/Display.html?id={$Ticket->id}
+to approve or reject this ticket, or {RT->Config->Get(\'WebURL\')}Approvals/ to
+batch-process all your pending approvals.
+
+-------------------------------------------------------------------------
+{$Transaction->Content()}
+'
+ },
+ { Queue => '___Approvals',
+ Name => "Approval Passed", # loc
+ Description =>
+ "Notify Requestor of their ticket has been approved by some approver", # loc
+ Content => 'Subject: Ticket Approved: {$Ticket->Subject}
+
+Greetings,
+
+Your ticket has been approved by { eval { $Approval->OwnerObj->Name } }.
+Other approvals may be pending.
+
+Approver\'s notes: { $Notes }
+'
+ },
+ { Queue => '___Approvals',
+ Name => "All Approvals Passed", # loc
+ Description =>
+ "Notify Requestor of their ticket has been approved by all approvers", # loc
+ Content => 'Subject: Ticket Approved: {$Ticket->Subject}
+
+Greetings,
+
+Your ticket has been approved by { eval { $Approval->OwnerObj->Name } }.
+Its Owner may now start to act on it.
+
+Approver\'s notes: { $Notes }
+'
+ },
+ { Queue => '___Approvals',
+ Name => "Approval Rejected", # loc
+ Description =>
+ "Notify Owner of their rejected ticket", # loc
+ Content => 'Subject: Ticket Rejected: {$Ticket->Subject}
+
+Greetings,
+
+Your ticket has been rejected by { eval { $Approval->OwnerObj->Name } }.
+
+Approver\'s notes: { $Notes }
+'
+ },
+ { Queue => '___Approvals',
+ Name => "Approval Ready for Owner", # loc
+ Description =>
+ "Notify Owner of their ticket has been approved and is ready to be acted on", # loc
+ Content => 'Subject: Ticket Approved: {$Ticket->Subject}
+
+Greetings,
+
+The ticket has been approved, you may now start to act on it.
+
+'
+ },
+);
+
+@Final = (
+ sub {
+ $RT::Logger->debug("Going to adjust dashboards");
+ my $sys = RT::System->new(RT->SystemUser);
+
+ my $attrs = RT::Attributes->new( RT->SystemUser );
+ $attrs->UnLimit;
+ my @dashboards = $attrs->Named('Dashboard');
+
+ if (@dashboards == 0) {
+ $RT::Logger->debug("You have no dashboards. Skipped.");
+ return 1;
+ }
+
+ for my $attr (@dashboards) {
+ my $props = $attr->Content;
+ if (exists $props->{Searches}) {
+ $props->{Panes} = {
+ body => [
+ map {
+ my ($privacy, $id, $desc) = @$_;
+
+ {
+ portlet_type => 'search',
+ privacy => $privacy,
+ id => $id,
+ description => $desc,
+ pane => 'body',
+ }
+ } @{ delete $props->{Searches} }
+ ],
+ };
+ }
+ my ($status, $msg) = $attr->SetContent( $props );
+ $RT::Logger->error($msg) unless $status;
+ }
+
+ $RT::Logger->debug("Fixed.");
+ return 1;
+ },
+ sub {
+ my $approvals_q = RT::Queue->new( RT->SystemUser );
+ $approvals_q->Load('___Approvals');
+ unless ( $approvals_q->id ) {
+ $RT::Logger->error("You have no approvals queue.");
+ return 1;
+ }
+
+ require File::Temp;
+ my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( 'rt-approvals-scrips-XXXX', CLEANUP => 0 );
+ unless ( $tmp_fh ) {
+ $RT::Logger->error("Couldn't create temporary file.");
+ return 0;
+ }
+
+ $RT::Logger->warning(
+ "IMPORTANT: We're going to delete all scrips in Approvals queue"
+ ." and save them in '$tmp_fn' file."
+ );
+
+ require Data::Dumper;
+
+ my $scrips = RT::Scrips->new( RT->SystemUser );
+ $scrips->LimitToQueue( $approvals_q->id );
+ while ( my $scrip = $scrips->Next ) {
+ my %tmp =
+ map { $tmp->{ $_ } = $scrip->_Value( $_ ) }
+ $scrip->ReadableAttributes;
+
+ print $tmp_fh Data::Dumper::Dumper( \%tmp );
+
+ my ($status, $msg) = $scrip->Delete;
+ unless ( $status ) {
+ $RT::Logger->error( "Couldn't delete scrip: $msg");
+ }
+ }
+ },
+);
--- /dev/null
+@ScripConditions = (
+ { Name => 'On Reject', # loc
+ Description => 'Whenever a ticket is rejected', # loc
+ ApplicableTransTypes => 'Status',
+ ExecModule => 'StatusChange',
+ Argument => 'rejected'
+
+ },
+);
+
+@Final = (
+ sub {
+ $RT::Logger->debug("Going to correct descriptions of notify actions in the DB");
+
+ my $actions = RT::ScripActions->new( RT->SystemUser );
+ $actions->Limit(
+ FIELD => 'ExecModule',
+ VALUE => 'Notify',
+ );
+ $actions->Limit(
+ FIELD => 'Argument',
+ VALUE => 'All',
+ );
+ while ( my $action = $actions->Next ) {
+ my ($status, $msg) = $action->__Set( Field => 'Name', Value => 'Notify Owner, Requestors, Ccs and AdminCcs' );
+ $RT::Logger->warning( "Couldn't change action name: $msg" )
+ unless $status;
+
+ ($status, $msg) = $action->__Set( Field => 'Description', Value => 'Send mail to owner and all watchers' );
+ $RT::Logger->warning( "Couldn't change action description: $msg" )
+ unless $status;
+ }
+
+ $actions = RT::ScripActions->new( RT->SystemUser );
+ $actions->Limit(
+ FIELD => 'ExecModule',
+ VALUE => 'NotifyAsComment',
+ );
+ $actions->Limit(
+ FIELD => 'Argument',
+ VALUE => 'All',
+ );
+ while ( my $action = $actions->Next ) {
+ my ($status, $msg) = $action->__Set( Field => 'Name', Value => 'Notify Owner, Requestors, Ccs and AdminCcs as Comment' );
+ $RT::Logger->warning( "Couldn't change action name: $msg" )
+ unless $status;
+
+ ($status, $msg) = $action->__Set( Field => 'Description', Value => 'Send mail to owner and all watchers as a "comment"' );
+ $RT::Logger->warning( "Couldn't change action description: $msg" )
+ unless $status;
+ }
+
+ $RT::Logger->debug("Corrected descriptions of notify actions in the DB.");
+ return 1;
+ },
+);
+
+
+{
+$RT::Logger->debug("Going to add in Extract Subject Tag actions if they were missed during a previous upgrade");
+
+$actions = RT::ScripActions->new( RT->SystemUser );
+$actions->Limit(
+ FIELD => 'ExecModule',
+ VALUE => 'ExtractSubjectTag',
+);
+my $extract_action = $actions->First;
+
+if ( $extract_action && $extract_action->Id ) {
+ $RT::Logger->debug("You appear to already have an Extract Subject Tag action, skipping");
+ return 1;
+} else {
+ $RT::Logger->debug("Didn't find an existing Extract Subject Tag action, adding it");
+ push @ScripActions, (
+ { Name => 'Extract Subject Tag', # loc
+ Description => 'Extract tags from a Transaction\'s subject and add them to the Ticket\'s subject.', # loc
+ ExecModule => 'ExtractSubjectTag'
+ },
+ );
+
+ $RT::Logger->debug("Adding Extract Subject Tag Scrip");
+ push @Scrips, (
+ { Description => "On transaction, add any tags in the transaction's subject to the ticket's subject",
+ ScripCondition => 'On Transaction',
+ ScripAction => 'Extract Subject Tag',
+ Template => 'Blank'
+ },
+ );
+}
+}
+
--- /dev/null
+
+CREATE UNIQUE INDEX GroupMembers1 ON GroupMembers(GroupId, MemberId);
+
--- /dev/null
+
+@Final = (
+ sub {
+ $RT::Logger->debug("Going to correct arguments of NotifyGroup actions if you have any");
+ use strict;
+
+ my $actions = RT::ScripActions->new( RT->SystemUser );
+ $actions->Limit(
+ FIELD => 'ExecModule',
+ VALUE => 'NotifyGroup',
+ );
+ $actions->Limit(
+ FIELD => 'ExecModule',
+ VALUE => 'NotifyGroupAsComment',
+ );
+
+ my $converter = sub {
+ my $arg = shift;
+ my @res;
+ foreach my $r ( @{ $arg } ) {
+ my $obj;
+ next unless $r->{'Type'};
+ if( lc $r->{'Type'} eq 'user' ) {
+ $obj = RT::User->new( RT->SystemUser );
+ } elsif ( lc $r->{'Type'} eq 'group' ) {
+ $obj = RT::Group->new( RT->SystemUser );
+ } else {
+ next;
+ }
+ $obj->Load( $r->{'Instance'} );
+ my $id = $obj->id;
+ next unless( $id );
+
+ push @res, $id;
+ }
+
+ return join ',', @res;
+ };
+
+ require Storable;
+ while ( my $action = $actions->Next ) {
+ my $argument = $action->Argument;
+ my $new = '';
+ local $@;
+ if ( my $struct = eval { Storable::thaw( $argument ) } ) {
+ $new = $converter->( $struct );
+ } else {
+ $new = join ", ", grep length, split /[^0-9]+/, $argument;
+ }
+ next if $new eq $argument;
+
+ my ($status, $msg) = $action->__Set( Field => 'Argument', Value => $new );
+ $RT::Logger->warning( "Couldn't change argument value of the action: $msg" )
+ unless $status;
+ }
+ },
+);
+
+
--- /dev/null
+@Templates = (
+ { Queue => 0,
+ Name => "Forward Ticket", # loc
+ Description => "Heading of a forwarded Ticket", # loc
+ Content => q{
+
+This is a forward of ticket #{ $Ticket->id }
+}
+ },
+);
--- /dev/null
+@Initial = (
+ sub {
+ # make sure global CFs are not applied to local objects
+ my $ocfs = RT::ObjectCustomFields->new( RT->SystemUser );
+ $ocfs->Limit( FIELD => 'ObjectId', OPERATOR => '!=', VALUE => 0 );
+ my $alias = $ocfs->Join(
+ FIELD1 => 'CustomField',
+ TABLE2 => 'ObjectCustomFields',
+ FIELD2 => 'CustomField',
+ );
+ $ocfs->Limit( ALIAS => $alias, FIELD => 'ObjectId', VALUE => 0 );
+ while ( my $ocf = $ocfs->Next ) {
+ $ocf->Delete;
+ }
+ },
+ sub {
+ # sort SortOrder
+ my $sth = $RT::Handle->dbh->prepare(
+ "SELECT cfs.LookupType, ocfs.id"
+ ." FROM ObjectCustomFields ocfs, CustomFields cfs"
+ ." WHERE cfs.id = ocfs.CustomField"
+ ." ORDER BY cfs.LookupType, ocfs.SortOrder, cfs.Name"
+ );
+ $sth->execute;
+
+ my ($i, $prev_type) = (0, '');
+ while ( my ($lt, $id) = $sth->fetchrow_array ) {
+ $i = 0 if $prev_type ne $lt;
+ my $ocf = RT::ObjectCustomField->new( RT->SystemUser );
+ $ocf->Load( $id );
+ my ($status, $msg) = $ocf->SetSortOrder( $i++ );
+ $RT::Logger->warning("Couldn't set SortOrder: $msg")
+ unless $status;
+ $prev_type = $lt;
+ }
+ },
+);
+
--- /dev/null
+@Initial = (
+ sub {
+ use strict;
+ $RT::Logger->debug('Make sure local links are local');
+
+ use RT::URI::fsck_com_rt;
+ my $prefix = RT::URI::fsck_com_rt->LocalURIPrefix . '/ticket/';
+
+ foreach my $dir (qw(Target Base)) {
+ my $found;
+ do {
+ $found = 0;
+ my $links = RT::Links->new( RT->SystemUser );
+ $links->Limit( FIELD => $dir, OPERATOR => 'STARTSWITH', VALUE => $prefix );
+ $links->Limit( FIELD => 'Local'.$dir, VALUE => 0 );
+ $links->Limit(
+ ENTRYAGGREGATOR => 'OR',
+ FIELD => 'Local'.$dir,
+ OPERATOR => 'IS',
+ VALUE => 'NULL',
+ );
+ $links->RowsPerPage( 1000 );
+ while ( my $link = $links->Next ) {
+ $found++;
+ my $uri = $link->$dir();
+ $uri =~ s/^\Q$prefix//;
+ if ( int($uri) eq $uri && $uri > 0 ) {
+ my $method = 'SetLocal'. $dir;
+ my ($status, $msg) = $link->$method( $uri );
+ unless ( $status ) {
+ die "Couldn't change local $dir: $msg";
+ }
+ } else {
+ die "$dir URI looks like local, but is not parseable";
+ }
+ }
+ } while $found == 1000;
+ }
+ },
+ sub {
+ my $queue = RT::Queue->new( $RT::SystemUser );
+ $queue->Load('___Approvals');
+ return unless $queue->id;
+
+ for my $name (
+ 'All Approvals Passed', 'Approval Passed', 'Approval Rejected'
+ )
+ {
+ my $template = RT::Template->new($RT::SystemUser);
+ $template->LoadQueueTemplate( Name => $name, Queue => $queue->id );
+ next unless $template->id;
+ my $content = $template->Content;
+
+ # there is only one OwnerObj->Name normally, so no need /g
+ if ( $content =~
+s!(?<=Your ticket has been (?:approved|rejected) by { eval { )\$Approval->OwnerObj->Name!\$Approver->Name!
+ )
+ {
+ $template->SetContent($content);
+ }
+ }
+ },
+);
--- /dev/null
+@Initial = (
+ sub {
+ use strict;
+ $RT::Logger->debug('Make sure templates all have known types');
+
+ # We update all NULL rows, below. We want to find non-NULL
+ # rows, which weren't created by the current codebase running
+ # through earlier initialdatas. Type != 'Perl' enforces the
+ # non-NULL part, as well
+ my $templates = RT::Templates->new(RT->SystemUser);
+ $templates->Limit(
+ FIELD => 'Type',
+ OPERATOR => '!=',
+ VALUE => 'Perl',
+ );
+
+ if ($templates->Count) {
+ die "You have templates with Type already set. This will interfere with your upgrade because RT used to ignore the template Type field, but now uses it.";
+ }
+
+ $templates = RT::Templates->new(RT->SystemUser);
+ $templates->Limit(
+ FIELD => 'Type',
+ OPERATOR => 'IS',
+ VALUE => 'NULL',
+ );
+ while (my $template = $templates->Next) {
+ my ($status, $msg) = $template->SetType('Perl');
+ $RT::Logger->warning( "Couldn't change Type of Template #" . $template->Id . ": $msg" ) unless $status;
+ }
+ },
+ sub {
+ use strict;
+ $RT::Logger->debug('Adding ExecuteCode right to principals that currently have ModifyTemplate or ModifyScrips');
+
+ my $acl = RT::ACL->new(RT->SystemUser);
+ $acl->Limit(
+ FIELD => 'RightName',
+ OPERATOR => '=',
+ VALUE => 'ModifyTemplate',
+ ENTRYAGGREGATOR => 'OR',
+ );
+ $acl->Limit(
+ FIELD => 'RightName',
+ OPERATOR => '=',
+ VALUE => 'ModifyScrips',
+ ENTRYAGGREGATOR => 'OR',
+ );
+
+ while (my $ace = $acl->Next) {
+ my $principal = $ace->PrincipalObj;
+ next if $principal->HasRight(
+ Right => 'ExecuteCode',
+ Object => $RT::System,
+ );
+
+ my ($ok, $msg) = $principal->GrantRight(
+ Right => 'ExecuteCode',
+ Object => $RT::System,
+ );
+
+ if (!$ok) {
+ $RT::Logger->warn("Unable to grant ExecuteCode on principal " . $principal->id . ": $msg");
+ }
+ }
+ },
+);
+
--- /dev/null
+@Initial = (
+ sub {
+ use strict;
+ $RT::Logger->debug('Removing all delegated rights');
+
+ my $acl = RT::ACL->new(RT->SystemUser);
+ $acl->Limit( CLAUSE => 'search',
+ FIELD => 'DelegatedBy',
+ OPERATOR => '>',
+ VALUE => '0'
+ );
+ $acl->Limit( CLAUSE => 'search',
+ FIELD => 'DelegatedFrom',
+ OPERATOR => '>',
+ VALUE => '0',
+ ENTRYAGGREGATOR => 'OR',
+ );
+
+ while ( my $ace = $acl->Next ) {
+ my ( $ok, $msg ) = $ace->Delete();
+
+ if ( !$ok ) {
+ $RT::Logger->warn(
+ "Unable to delete ACE " . $ace->id . ": " . $msg );
+ }
+ }
+
+ my $groups = RT::Groups->new(RT->SystemUser);
+ $groups->Limit( FIELD => 'Domain',
+ OPERATOR => '=',
+ VALUE => 'Personal'
+ );
+ while ( my $group = $groups->Next ) {
+ my $members = $group->MembersObj();
+ while ( my $member = $members->Next ) {
+ my ( $ok, $msg ) = $group->DeleteMember( $member->MemberId );
+ if ( !$ok ) {
+ $RT::Logger->warn( "Unable to remove group member "
+ . $member->id . ": "
+ . $msg );
+ }
+ }
+ $group->PrincipalObj->Delete;
+ $group->RT::Record::Delete();
+ }
+ },
+);
+
--- /dev/null
+ALTER TABLE ACL DROP COLUMN DelegatedBy;
+ALTER TABLE ACL DROP COLUMN DelegatedFrom;
--- /dev/null
+ALTER TABLE ACL DROP COLUMN DelegatedBy;
+ALTER TABLE ACL DROP COLUMN DelegatedFrom;
--- /dev/null
+ALTER TABLE ACL DROP COLUMN DelegatedBy;
+ALTER TABLE ACL DROP COLUMN DelegatedFrom;
--- /dev/null
+RT::ACE LastUpdated LastUpdatedBy Creator Created
--- /dev/null
+alter Table CustomFieldValues ADD Category varchar2(255);
+
+UPDATE CustomFieldValues SET Category = (SELECT Content FROM Attributes WHERE
+Name = 'Category' AND ObjectType = 'RT::CustomFieldValue'
+AND CustomFieldValues.id = Attributes.ObjectId);
+
+DELETE FROM Attributes WHERE Name = 'Category' AND ObjectType = 'RT::CustomFieldValue';
+
+ALTER TABLE Groups ADD Creator NUMBER(11,0) DEFAULT 0 NOT NULL;
+ALTER TABLE Groups ADD Created DATE;
+ALTER TABLE Groups ADD LastUpdatedBy NUMBER(11,0) DEFAULT 0 NOT NULL;
+ALTER TABLE Groups ADD LastUpdated DATE;
+ALTER TABLE GroupMembers ADD Creator NUMBER(11,0) DEFAULT 0 NOT NULL;
+ALTER TABLE GroupMembers ADD Created DATE;
+ALTER TABLE GroupMembers ADD LastUpdatedBy NUMBER(11,0) DEFAULT 0 NOT NULL;
+ALTER TABLE GroupMembers ADD LastUpdated DATE;
+ALTER TABLE ACL ADD Creator NUMBER(11,0) DEFAULT 0 NOT NULL;
+ALTER TABLE ACL ADD Created DATE;
+ALTER TABLE ACL ADD LastUpdatedBy NUMBER(11,0) DEFAULT 0 NOT NULL;
+ALTER TABLE ACL ADD LastUpdated DATE;
--- /dev/null
+alter Table CustomFieldValues ADD Column Category varchar(255);
+
+UPDATE CustomFieldValues SET Category = (SELECT Content FROM Attributes WHERE
+Name = 'Category' AND ObjectType = 'RT::CustomFieldValue'
+AND CustomFieldValues.id = Attributes.ObjectId);
+
+DELETE FROM Attributes WHERE Name = 'Category' AND ObjectType = 'RT::CustomFieldValue';
+
+ALTER TABLE Groups ADD COLUMN Creator integer NOT NULL DEFAULT 0;
+ALTER TABLE Groups ADD COLUMN Created TIMESTAMP NULL;
+ALTER TABLE Groups ADD COLUMN LastUpdatedBy integer NOT NULL DEFAULT 0;
+ALTER TABLE Groups ADD COLUMN LastUpdated TIMESTAMP NULL;
+ALTER TABLE GroupMembers ADD COLUMN Creator integer NOT NULL DEFAULT 0;
+ALTER TABLE GroupMembers ADD COLUMN Created TIMESTAMP NULL;
+ALTER TABLE GroupMembers ADD COLUMN LastUpdatedBy integer NOT NULL DEFAULT 0;
+ALTER TABLE GroupMembers ADD COLUMN LastUpdated TIMESTAMP NULL;
+ALTER TABLE ACL ADD COLUMN Creator integer NOT NULL DEFAULT 0;
+ALTER TABLE ACL ADD COLUMN Created TIMESTAMP NULL;
+ALTER TABLE ACL ADD COLUMN LastUpdatedBy integer NOT NULL DEFAULT 0;
+ALTER TABLE ACL ADD COLUMN LastUpdated TIMESTAMP NULL;
--- /dev/null
+ALTER TABLE CustomFieldValues ADD Column Category varchar(255);
+UPDATE CustomFieldValues SET Category = (SELECT Content FROM Attributes WHERE
+Name = 'Category' AND ObjectType = 'RT::CustomFieldValue'
+AND CustomFieldValues.id = Attributes.ObjectId);
+
+DELETE FROM Attributes WHERE Name = 'Category' AND ObjectType = 'RT::CustomFieldValue';
+
+ALTER TABLE Groups ADD COLUMN Creator integer NOT NULL DEFAULT 0;
+ALTER TABLE Groups ADD COLUMN Created DATETIME NULL;
+ALTER TABLE Groups ADD COLUMN LastUpdatedBy integer NOT NULL DEFAULT 0;
+ALTER TABLE Groups ADD COLUMN LastUpdated DATETIME NULL;
+ALTER TABLE GroupMembers ADD COLUMN Creator integer NOT NULL DEFAULT 0;
+ALTER TABLE GroupMembers ADD COLUMN Created DATETIME NULL;
+ALTER TABLE GroupMembers ADD COLUMN LastUpdatedBy integer NOT NULL DEFAULT 0;
+ALTER TABLE GroupMembers ADD COLUMN LastUpdated DATETIME NULL;
+ALTER TABLE ACL ADD COLUMN Creator integer NOT NULL DEFAULT 0;
+ALTER TABLE ACL ADD COLUMN Created DATETIME NULL;
+ALTER TABLE ACL ADD COLUMN LastUpdatedBy integer NOT NULL DEFAULT 0;
+ALTER TABLE ACL ADD COLUMN LastUpdated DATETIME NULL;
--- /dev/null
+alter Table CustomFieldValues ADD Column Category varchar(255);
+
+UPDATE CustomFieldValues SET Category = (SELECT Content FROM Attributes WHERE
+Name = 'Category' AND ObjectType = 'RT::CustomFieldValue'
+AND CustomFieldValues.id = Attributes.ObjectId);
+
+DELETE FROM Attributes WHERE Name = 'Category' AND ObjectType = 'RT::CustomFieldValue';
+
+ALTER TABLE Groups ADD COLUMN Creator integer NOT NULL DEFAULT 0,
+ ADD COLUMN Created DATETIME NULL,
+ ADD COLUMN LastUpdatedBy integer NOT NULL DEFAULT 0,
+ ADD COLUMN LastUpdated DATETIME NULL;
+ALTER TABLE GroupMembers ADD COLUMN Creator integer NOT NULL DEFAULT 0,
+ ADD COLUMN Created DATETIME NULL,
+ ADD COLUMN LastUpdatedBy integer NOT NULL DEFAULT 0,
+ ADD COLUMN LastUpdated DATETIME NULL;
+ALTER TABLE ACL ADD COLUMN Creator integer NOT NULL DEFAULT 0,
+ ADD COLUMN Created DATETIME NULL,
+ ADD COLUMN LastUpdatedBy integer NOT NULL DEFAULT 0,
+ ADD COLUMN LastUpdated DATETIME NULL;
--- /dev/null
+ALTER TABLE Tickets MODIFY Status VARCHAR2(64);
--- /dev/null
+ALTER TABLE Tickets ALTER Status TYPE varchar(64);
--- /dev/null
+BEGIN TRANSACTION;
+CREATE TEMPORARY TABLE Tickets_backup (
+ id INTEGER PRIMARY KEY ,
+ EffectiveId integer NULL ,
+ Queue integer NULL ,
+ Type varchar(16) NULL ,
+ IssueStatement integer NULL ,
+ Resolution integer NULL ,
+ Owner integer NULL ,
+ Subject varchar(200) NULL DEFAULT '[no subject]' ,
+ InitialPriority integer NULL ,
+ FinalPriority integer NULL ,
+ Priority integer NULL ,
+ TimeEstimated integer NULL ,
+ TimeWorked integer NULL ,
+ Status varchar(64) NULL ,
+ TimeLeft integer NULL ,
+ Told DATETIME NULL ,
+ Starts DATETIME NULL ,
+ Started DATETIME NULL ,
+ Due DATETIME NULL ,
+ Resolved DATETIME NULL ,
+ LastUpdatedBy integer NULL ,
+ LastUpdated DATETIME NULL ,
+ Creator integer NULL ,
+ Created DATETIME NULL ,
+ Disabled int2 NOT NULL DEFAULT 0
+);
+
+INSERT INTO Tickets_backup SELECT * FROM Tickets;
+DROP TABLE Tickets;
+
+CREATE TABLE Tickets (
+ id INTEGER PRIMARY KEY ,
+ EffectiveId integer NULL ,
+ Queue integer NULL ,
+ Type varchar(16) NULL ,
+ IssueStatement integer NULL ,
+ Resolution integer NULL ,
+ Owner integer NULL ,
+ Subject varchar(200) NULL DEFAULT '[no subject]' ,
+ InitialPriority integer NULL ,
+ FinalPriority integer NULL ,
+ Priority integer NULL ,
+ TimeEstimated integer NULL ,
+ TimeWorked integer NULL ,
+ Status varchar(64) NULL ,
+ TimeLeft integer NULL ,
+ Told DATETIME NULL ,
+ Starts DATETIME NULL ,
+ Started DATETIME NULL ,
+ Due DATETIME NULL ,
+ Resolved DATETIME NULL ,
+ LastUpdatedBy integer NULL ,
+ LastUpdated DATETIME NULL ,
+ Creator integer NULL ,
+ Created DATETIME NULL ,
+ Disabled int2 NOT NULL DEFAULT 0
+);
+
+CREATE INDEX Tickets1 ON Tickets (Queue, Status) ;
+CREATE INDEX Tickets2 ON Tickets (Owner) ;
+CREATE INDEX Tickets3 ON Tickets (EffectiveId) ;
+CREATE INDEX Tickets4 ON Tickets (id, Status) ;
+CREATE INDEX Tickets5 ON Tickets (id, EffectiveId) ;
+
+INSERT INTO Tickets SELECT * FROM Tickets_backup;
+DROP TABLE Tickets_backup;
+COMMIT;
--- /dev/null
+ALTER TABLE Tickets Modify Status varchar(64);
--- /dev/null
+my $move_attributes = sub {
+ my ($table, $type, $column) = @_;
+ my $query = "UPDATE $table SET $column = (SELECT Content FROM Attributes WHERE"
+ ." Name = ? AND ObjectType = ? AND $table.id = Attributes.ObjectId)";
+
+ my $res = $RT::Handle->SimpleQuery( $query, $column, $type );
+ unless ( $res ) {
+ $RT::Logger->error("Failed to move $column on $type from Attributes into $table table");
+ return;
+ }
+
+ $query = 'DELETE FROM Attributes WHERE Name = ? AND ObjectType = ?';
+ $res = $RT::Handle->SimpleQuery( $query, $column, $type );
+ unless ( $res ) {
+ $RT::Logger->error("Failed to delete $column on $type from Attributes");
+ return;
+ }
+ return 1;
+};
+
+@Initial = (
+ sub {
+ return $move_attributes->( 'Users', 'RT::User', 'AuthToken');
+ },
+ sub {
+ return $move_attributes->( 'CustomFields', 'RT::CustomField', 'RenderType');
+ },
+ sub {
+ my $cfs = RT::CustomFields->new($RT::SystemUser);
+ $cfs->UnLimit;
+ $cfs->FindAllRows;
+ while ( my $cf = $cfs->Next ) {
+ # Explicitly remove 'ORDER BY id asc' to emulate the
+ # previous functionality, where Pg might return the the
+ # rows in arbitrary order
+ $cf->Attributes->OrderByCols();
+
+ my $attr = $cf->FirstAttribute('BasedOn');
+ next unless $attr;
+ $cf->SetBasedOn($attr->Content);
+ }
+ $query = 'DELETE FROM Attributes WHERE Name = ? AND ObjectType = ?';
+ $res = $RT::Handle->SimpleQuery( $query, 'BasedOn', 'RT::CustomField' );
+ unless ( $res ) {
+ $RT::Logger->error("Failed to delete BasedOn CustomFields from Attributes");
+ return;
+ }
+ return 1;
+ },
+ sub {
+ $move_attributes->( 'CustomFields', 'RT::CustomField', 'ValuesClass')
+ or return;
+
+ my $query = "UPDATE CustomFields SET ValuesClass = NULL WHERE ValuesClass = ?";
+ my $res = $RT::Handle->SimpleQuery( $query, 'RT::CustomFieldValues' );
+ unless ( $res ) {
+ $RT::Logger->error("Failed to replace default with NULLs");
+ return;
+ }
+ return 1;
+ },
+ sub {
+ my $attr = RT->System->FirstAttribute('BrandedSubjectTag');
+ return 1 unless $attr;
+
+ my $map = $attr->Content || {};
+ while ( my ($qid, $tag) = each %$map ) {
+ my $queue = RT::Queue->new( RT->SystemUser );
+ $queue->Load( $qid );
+ unless ( $queue->id ) {
+ $RT::Logger->warning("Couldn't load queue #$qid. Skipping...");
+ next;
+ }
+
+ my ($status, $msg) = $queue->SetSubjectTag($tag);
+ unless ( $status ) {
+ $RT::Logger->error("Couldn't set subject tag for queue #$qid: $msg");
+ next;
+ }
+ }
+ },
+);
--- /dev/null
+ALTER TABLE Users ADD AuthToken VARCHAR2(16) NULL;
+ALTER TABLE CustomFields ADD BasedOn NUMBER(11,0) NULL;
+ALTER TABLE CustomFields ADD RenderType VARCHAR2(64) NULL;
+ALTER TABLE CustomFields ADD ValuesClass VARCHAR2(64) NULL;
+ALTER TABLE Queues ADD SubjectTag VARCHAR2(120) NULL;
+ALTER TABLE Queues ADD Lifecycle VARCHAR2(32) NULL;
--- /dev/null
+ALTER TABLE Users ADD COLUMN AuthToken VARCHAR(16) NULL;
+ALTER TABLE CustomFields ADD COLUMN BasedOn INTEGER NULL;
+ALTER TABLE CustomFields ADD COLUMN RenderType VARCHAR(64) NULL;
+ALTER TABLE CustomFields ADD COLUMN ValuesClass VARCHAR(64) NULL;
+ALTER TABLE Queues ADD COLUMN SubjectTag VARCHAR(120) NULL;
+ALTER TABLE Queues ADD COLUMN Lifecycle VARCHAR(32) NULL;
--- /dev/null
+ALTER TABLE Users ADD COLUMN AuthToken VARCHAR(16) NULL;
+ALTER TABLE CustomFields ADD COLUMN BasedOn INTEGER NULL;
+ALTER TABLE CustomFields ADD COLUMN RenderType VARCHAR(64) NULL;
+ALTER TABLE CustomFields ADD COLUMN ValuesClass VARCHAR(64) NULL;
+ALTER TABLE Queues ADD COLUMN SubjectTag VARCHAR(120) NULL;
+ALTER TABLE Queues ADD COLUMN Lifecycle VARCHAR(32) NULL;
--- /dev/null
+ALTER TABLE Users ADD COLUMN AuthToken VARCHAR(16) CHARACTER SET ascii NULL;
+ALTER TABLE CustomFields ADD COLUMN BasedOn INTEGER NULL,
+ ADD COLUMN RenderType VARCHAR(64) NULL,
+ ADD COLUMN ValuesClass VARCHAR(64) CHARACTER SET ascii NULL;
+ALTER TABLE Queues ADD COLUMN SubjectTag VARCHAR(120) NULL,
+ ADD COLUMN Lifecycle VARCHAR(32) NULL;
--- /dev/null
+@Initial = sub {
+ my $found_fm_tables = {};
+ foreach my $name ( $RT::Handle->_TableNames ) {
+ next unless $name =~ /^fm_/i;
+ $found_fm_tables->{lc $name}++;
+ }
+
+ return unless %$found_fm_tables;
+
+ unless ( $found_fm_tables->{fm_topics} && $found_fm_tables->{fm_objecttopics} ) {
+ $RT::Logger->error("You appear to be upgrading from RTFM 2.0 - We don't support upgrading this old of an RTFM yet");
+ }
+
+ $RT::Logger->error("We found RTFM tables in your database. Checking for content.");
+
+ my $dbh = $RT::Handle->dbh;
+ my $result = $dbh->selectall_arrayref("SELECT count(*) AS articlecount FROM FM_Articles", { Slice => {} } );
+
+ if ($result->[0]{articlecount} > 0) {
+ $RT::Logger->error("You appear to have RTFM Articles. You can upgrade using the etc/upgrade/upgrade-articles script. Read more about it in docs/UPGRADING-4.0");
+ }
+};
--- /dev/null
+CREATE SEQUENCE Classes_seq;
+CREATE TABLE Classes (
+id NUMBER(11,0)
+ CONSTRAINT Classes_key PRIMARY KEY,
+Name varchar2(255) DEFAULT '',
+Description varchar2(255) DEFAULT '',
+SortOrder NUMBER(11,0) DEFAULT 0 NOT NULL,
+Disabled NUMBER(11,0) DEFAULT 0 NOT NULL,
+Creator NUMBER(11,0) DEFAULT 0 NOT NULL,
+Created DATE,
+LastUpdatedBy NUMBER(11,0) DEFAULT 0 NOT NULL,
+LastUpdated DATE,
+HotList NUMBER(11,0) DEFAULT 0 NOT NULL
+);
+
+CREATE SEQUENCE Articles_seq;
+CREATE TABLE Articles (
+id NUMBER(11,0)
+ CONSTRAINT Articles_key PRIMARY KEY,
+Name varchar2(255) DEFAULT '',
+Summary varchar2(255) DEFAULT '',
+SortOrder NUMBER(11,0) DEFAULT 0 NOT NULL,
+Class NUMBER(11,0) DEFAULT 0 NOT NULL,
+Parent NUMBER(11,0) DEFAULT 0 NOT NULL,
+URI varchar2(255),
+Creator NUMBER(11,0) DEFAULT 0 NOT NULL,
+Created DATE,
+LastUpdatedBy NUMBER(11,0) DEFAULT 0 NOT NULL,
+LastUpdated DATE
+);
+
+
+CREATE SEQUENCE Topics_seq;
+CREATE TABLE Topics (
+id NUMBER(11,0)
+ CONSTRAINT Topics_key PRIMARY KEY,
+Parent NUMBER(11,0) DEFAULT 0 NOT NULL,
+Name varchar2(255) DEFAULT '',
+Description varchar2(255) DEFAULT '',
+ObjectType varchar2(64) DEFAULT '' NOT NULL,
+ObjectId NUMBER(11,0) NOT NULL
+);
+
+
+CREATE SEQUENCE ObjectTopics_seq;
+CREATE TABLE ObjectTopics (
+id NUMBER(11,0)
+ CONSTRAINT ObjectTopics_key PRIMARY KEY,
+Topic NUMBER(11,0) NOT NULL,
+ObjectType varchar2(64) DEFAULT '' NOT NULL,
+ObjectId NUMBER(11,0) NOT NULL
+);
+
+CREATE SEQUENCE ObjectClasses_seq;
+CREATE TABLE ObjectClasses (
+id NUMBER(11,0)
+ CONSTRAINT ObjectClasses_key PRIMARY KEY,
+Class NUMBER(11,0) NOT NULL,
+ObjectType varchar2(255) DEFAULT '' NOT NULL,
+ObjectId NUMBER(11,0) NOT NULL,
+Creator NUMBER(11,0) DEFAULT 0 NOT NULL,
+Created DATE,
+LastUpdatedBy NUMBER(11,0) DEFAULT 0 NOT NULL,
+LastUpdated DATE
+);
--- /dev/null
+CREATE TABLE Classes (
+id SERIAL,
+Name varchar(255) NOT NULL DEFAULT '',
+Description varchar(255) NOT NULL DEFAULT '',
+SortOrder integer NOT NULL DEFAULT 0,
+Disabled smallint NOT NULL DEFAULT 0,
+Creator integer NOT NULL DEFAULT 0,
+Created TIMESTAMP NULL,
+LastUpdatedBy integer NOT NULL DEFAULT 0,
+LastUpdated TIMESTAMP NULL,
+HotList smallint NOT NULL DEFAULT 0,
+PRIMARY KEY (id)
+);
+
+CREATE TABLE Articles (
+id SERIAL,
+Name varchar(255) NOT NULL DEFAULT '',
+Summary varchar(255) NOT NULL DEFAULT '',
+SortOrder integer NOT NULL DEFAULT 0,
+Class integer NOT NULL DEFAULT 0,
+Parent integer NOT NULL DEFAULT 0,
+URI varchar(255),
+Creator integer NOT NULL DEFAULT 0,
+Created TIMESTAMP NULL,
+LastUpdatedBy integer NOT NULL DEFAULT 0,
+LastUpdated TIMESTAMP NULL,
+PRIMARY KEY (id)
+);
+
+
+CREATE TABLE Topics (
+id SERIAL,
+Parent integer NOT NULL DEFAULT 0,
+Name varchar(255) NOT NULL DEFAULT '',
+Description varchar(255) NOT NULL DEFAULT '',
+ObjectType varchar(64) NOT NULL DEFAULT '',
+ObjectId integer NOT NULL,
+PRIMARY KEY (id)
+);
+
+
+CREATE TABLE ObjectTopics (
+id SERIAL,
+Topic integer NOT NULL,
+ObjectType varchar(64) NOT NULL DEFAULT '',
+ObjectId integer NOT NULL,
+PRIMARY KEY (id)
+);
+
+
+CREATE TABLE ObjectClasses (
+id SERIAL,
+Class integer NOT NULL,
+ObjectType varchar(255) NOT NULL DEFAULT '',
+ObjectId integer NOT NULL,
+Creator integer NOT NULL DEFAULT 0,
+Created TIMESTAMP NULL,
+LastUpdatedBy integer NOT NULL DEFAULT 0,
+LastUpdated TIMESTAMP NULL,
+PRIMARY KEY (id)
+);
+
--- /dev/null
+CREATE TABLE Classes (
+id INTEGER PRIMARY KEY,
+Name varchar(255) NOT NULL DEFAULT '',
+Description varchar(255) NOT NULL DEFAULT '',
+SortOrder integer NOT NULL DEFAULT 0,
+Disabled smallint NOT NULL DEFAULT 0,
+Creator integer NOT NULL DEFAULT 0,
+Created TIMESTAMP NULL,
+LastUpdatedBy integer NOT NULL DEFAULT 0,
+LastUpdated TIMESTAMP NULL,
+HotList smallint NOT NULL DEFAULT 0
+);
+
+CREATE TABLE Articles (
+id INTEGER PRIMARY KEY,
+Name varchar(255) NOT NULL DEFAULT '',
+Summary varchar(255) NOT NULL DEFAULT '',
+SortOrder integer NOT NULL DEFAULT 0,
+Class integer NOT NULL DEFAULT 0,
+Parent integer NOT NULL DEFAULT 0,
+URI varchar(255),
+Creator integer NOT NULL DEFAULT 0,
+Created TIMESTAMP NULL,
+LastUpdatedBy integer NOT NULL DEFAULT 0,
+LastUpdated TIMESTAMP NULL
+);
+
+
+CREATE TABLE Topics (
+id INTEGER PRIMARY KEY,
+Parent integer NOT NULL DEFAULT 0,
+Name varchar(255) NOT NULL DEFAULT '',
+Description varchar(255) NOT NULL DEFAULT '',
+ObjectType varchar(64) NOT NULL DEFAULT '',
+ObjectId integer NOT NULL
+);
+
+
+CREATE TABLE ObjectTopics (
+id INTEGER PRIMARY KEY,
+Topic integer NOT NULL,
+ObjectType varchar(64) NOT NULL DEFAULT '',
+ObjectId integer NOT NULL
+);
+
+CREATE TABLE ObjectClasses (
+id INTEGER PRIMARY KEY,
+Class integer NOT NULL,
+ObjectType varchar(64) NOT NULL DEFAULT '',
+ObjectId integer NOT NULL,
+Creator integer NOT NULL DEFAULT 0,
+Created TIMESTAMP NULL,
+LastUpdatedBy integer NOT NULL DEFAULT 0,
+LastUpdated TIMESTAMP NULL
+);
--- /dev/null
+CREATE TABLE Classes (
+ id int(11) NOT NULL auto_increment,
+ Name varchar(255) NOT NULL default '',
+ Description varchar(255) NOT NULL default '',
+ SortOrder int(11) NOT NULL default '0',
+ Disabled int(2) NOT NULL default '0',
+ Creator int(11) NOT NULL default '0',
+ Created datetime default NULL,
+ LastUpdatedBy int(11) NOT NULL default '0',
+ LastUpdated datetime default NULL,
+ HotList int(2) NOT NULL default '0',
+ PRIMARY KEY (id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE Articles (
+ id int(11) NOT NULL auto_increment,
+ Name varchar(255) NOT NULL default '',
+ Summary varchar(255) NOT NULL default '',
+ SortOrder int(11) NOT NULL default '0',
+ Class int(11) NOT NULL default '0',
+ Parent int(11) NOT NULL default '0',
+ URI varchar(255) character set ascii default NULL,
+ Creator int(11) NOT NULL default '0',
+ Created datetime default NULL,
+ LastUpdatedBy int(11) NOT NULL default '0',
+ LastUpdated datetime default NULL,
+ PRIMARY KEY (id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE Topics (
+ id int(11) NOT NULL auto_increment,
+ Parent int(11) NOT NULL default '0',
+ Name varchar(255) NOT NULL default '',
+ Description varchar(255) NOT NULL default '',
+ ObjectType varchar(64) character set ascii NOT NULL default '',
+ ObjectId int(11) NOT NULL default '0',
+ PRIMARY KEY (id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE ObjectTopics (
+ id int(11) NOT NULL auto_increment,
+ Topic int(11) NOT NULL default '0',
+ ObjectType varchar(64) character set ascii NOT NULL default '',
+ ObjectId int(11) NOT NULL default '0',
+ PRIMARY KEY (id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE ObjectClasses (
+ id int(11) NOT NULL auto_increment,
+ Class int(11) NOT NULL default '0',
+ ObjectType varchar(255) character set ascii NOT NULL default '',
+ ObjectId int(11) NOT NULL default '0',
+ Creator int(11) NOT NULL default '0',
+ Created datetime default NULL,
+ LastUpdatedBy int(11) NOT NULL default '0',
+ LastUpdated datetime default NULL,
+ PRIMARY KEY (id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
--- /dev/null
+
+DROP TABLE IF EXISTS sessions;
+
+CREATE TABLE sessions (
+ id char(32) NOT NULL,
+ a_session LONGBLOB,
+ LastUpdated TIMESTAMP,
+ PRIMARY KEY (id)
+) ENGINE=InnoDB CHARACTER SET ascii;
+
--- /dev/null
+ALTER TABLE Users MODIFY Password VARCHAR2(256);
--- /dev/null
+ALTER TABLE Users ALTER Password TYPE varchar(256);
--- /dev/null
+ALTER TABLE Users MODIFY Password varchar(256);
--- /dev/null
+@Initial = (
+ sub {
+ $RT::Logger->debug("Going to set lifecycle for approvals");
+
+ my $queue = RT::Queue->new( RT->SystemUser );
+ $queue->Load('___Approvals');
+ unless ( $queue->id ) {
+ $RT::Logger->warning("There is no ___Approvals queue in the DB");
+ return 1;
+ }
+
+ return 1 if $queue->Lifecycle->Name eq 'approvals';
+
+ my ($status, $msg) = $queue->SetLifecycle('approvals');
+ unless ( $status ) {
+ $RT::Logger->error("Couldn't set lifecycle for '___Approvals' queue: $msg");
+ return 0;
+ }
+ return 1;
+ },
+);
--- /dev/null
+
+sub acl {
+ my $dbh = shift;
+
+ my @acls;
+
+ my @tables = qw (
+ classes_id_seq
+ Classes
+ articles_id_seq
+ Articles
+ topics_id_seq
+ Topics
+ objecttopics_id_seq
+ ObjectTopics
+ objectclasses_id_seq
+ ObjectClasses
+ );
+
+ my $db_user = RT->Config->Get('DatabaseUser');
+
+ my $sequence_right
+ = ( $dbh->{pg_server_version} >= 80200 )
+ ? "USAGE, SELECT, UPDATE"
+ : "SELECT, UPDATE";
+
+ foreach my $table (@tables) {
+ # Tables are upper-case, sequences are lowercase
+ if ( $table =~ /^[a-z]/ ) {
+ push @acls, "GRANT $sequence_right ON $table TO \"$db_user\";"
+ }
+ else {
+ push @acls, "GRANT SELECT, INSERT, UPDATE, DELETE ON $table TO \"$db_user\";"
+ }
+ }
+ return (@acls);
+}
+
+1;
--- /dev/null
+@Initial = (
+ sub {
+ use strict;
+ $RT::Logger->debug('Removing all delegated rights');
+
+ my $acl = RT::ACL->new(RT->SystemUser);
+ my $groupjoin = $acl->NewAlias('Groups');
+ $acl->Join( ALIAS1 => 'main',
+ FIELD1 => 'PrincipalId',
+ ALIAS2 => $groupjoin,
+ FIELD2 => 'id'
+ );
+ $acl->Limit( ALIAS => $groupjoin,
+ FIELD => 'Domain',
+ OPERATOR => '=',
+ VALUE => 'Personal',
+ );
+
+ while ( my $ace = $acl->Next ) {
+ my ( $ok, $msg ) = $ace->Delete();
+
+ if ( !$ok ) {
+ $RT::Logger->warn( "Unable to delete ACE " . $ace->id . ": " . $msg );
+ }
+ }
+
+ my $groups = RT::Groups->new(RT->SystemUser);
+ $groups->Limit( FIELD => 'Domain',
+ OPERATOR => '=',
+ VALUE => 'Personal'
+ );
+ while ( my $group = $groups->Next ) {
+ my $members = $group->MembersObj();
+ while ( my $member = $members->Next ) {
+ my ( $ok, $msg ) = $group->DeleteMember( $member->MemberId );
+ if ( !$ok ) {
+ $RT::Logger->warn( "Unable to remove group member "
+ . $member->id . ": "
+ . $msg );
+ }
+ }
+ $group->PrincipalObj->Delete;
+ $group->RT::Record::Delete();
+ }
+ },
+ sub {
+ use strict;
+ $RT::Logger->debug('Removing all Delegate and PersonalGroup rights');
+
+ my $acl = RT::ACL->new(RT->SystemUser);
+ for my $right (qw/AdminOwnPersonalGroups AdminAllPersonalGroups DelegateRights/) {
+ $acl->Limit( FIELD => 'RightName', VALUE => $right );
+ }
+
+ while ( my $ace = $acl->Next ) {
+ my ( $ok, $msg ) = $ace->Delete();
+ $RT::Logger->debug("Removing ACE ".$ace->id." for right ".$ace->__Value('RightName'));
+
+ if ( !$ok ) {
+ $RT::Logger->warn( "Unable to delete ACE " . $ace->id . ": " . $msg );
+ }
+ }
+ },
+ sub {
+ use strict;
+ $RT::Logger->debug('Removing unimplemented RejectTicket and ModifyTicketStatus rights');
+
+ my $acl = RT::ACL->new(RT->SystemUser);
+ for my $right (qw/RejectTicket ModifyTicketStatus/) {
+ $acl->Limit( FIELD => 'RightName', VALUE => $right );
+ }
+
+ while ( my $ace = $acl->Next ) {
+ my ( $ok, $msg ) = $ace->Delete();
+ $RT::Logger->debug("Removing ACE ".$ace->id." for right ".$ace->__Value('RightName'));
+
+ if ( !$ok ) {
+ $RT::Logger->warn( "Unable to delete ACE " . $ace->id . ": " . $msg );
+ }
+ }
+ },
+);
+
--- /dev/null
+UPDATE Tickets SET Type = LOWER(Type) WHERE LOWER(Type) IN ('ticket', 'approval', 'reminder');
--- /dev/null
+UPDATE Tickets SET Type = LOWER(Type) WHERE LOWER(Type) IN ('ticket', 'approval', 'reminder');
--- /dev/null
+UPDATE Tickets SET Type = LOWER(Type) WHERE LOWER(Type) IN ('ticket', 'approval', 'reminder');
--- /dev/null
+UPDATE Tickets SET Subject = REPLACE(Subject,CHR(10),''), Status = LOWER(Status);
+UPDATE Transactions SET OldValue = LOWER(OldValue), NewValue = LOWER(NewValue) WHERE Type = 'Status' AND Field = 'Status';
--- /dev/null
+UPDATE Tickets SET Subject = REPLACE(Subject,E'\n',''), Status = LOWER(Status);
+UPDATE Transactions SET OldValue = LOWER(OldValue), NewValue = LOWER(NewValue) WHERE Type = 'Status' AND Field = 'Status';
--- /dev/null
+UPDATE Tickets SET Subject = REPLACE(Subject,'\n',''), Status = LOWER(Status);
+UPDATE Transactions SET OldValue = LOWER(OldValue), NewValue = LOWER(NewValue) WHERE Type = 'Status' AND Field = 'Status';
--- /dev/null
+@ScripConditions = (
+ {
+
+ Name => 'On Forward', # loc
+ Description => 'Whenever a ticket or transaction is forwarded', # loc
+ ApplicableTransTypes => 'Forward Transaction,Forward Ticket',
+ ExecModule => 'AnyTransaction', },
+
+ {
+
+ Name => 'On Forward Ticket', # loc
+ Description => 'Whenever a ticket is forwarded', # loc
+ ApplicableTransTypes => 'Forward Ticket',
+ ExecModule => 'AnyTransaction', },
+
+ {
+
+ Name => 'On Forward Transaction', # loc
+ Description => 'Whenever a transaction is forwarded', # loc
+ ApplicableTransTypes => 'Forward Transaction',
+ ExecModule => 'AnyTransaction', },
+
+);
--- /dev/null
+@Initial = (
+ sub {
+ use strict;
+ my $templates = RT::Templates->new(RT->SystemUser);
+ $templates->Limit(
+ FIELD => 'Type',
+ OPERATOR => 'IS',
+ VALUE => 'NULL',
+ );
+ while (my $template = $templates->Next) {
+ my ($status, $msg) = $template->SetType('Perl');
+ $RT::Logger->warning( "Couldn't change Type of Template #" . $template->Id . ": $msg" ) unless $status;
+ }
+ },
+);
+
--- /dev/null
+@Initial = (
+ sub {
+ my $txns = RT::Transactions->new( $RT::SystemUser );
+ $txns->Limit(
+ FIELD => "ObjectType",
+ VALUE => "RT::User",
+ );
+ $txns->Limit(
+ FIELD => "Field",
+ VALUE => "Password",
+ );
+ while (my $txn = $txns->Next) {
+ $txn->__Set( Field => $_, Value => '********' )
+ for qw/OldValue NewValue/;
+ }
+ },
+);
--- /dev/null
+ALTER TABLE Attributes MODIFY Content LONGBLOB;
--- /dev/null
+@Initial = (
+ sub {
+ $RT::Logger->debug(
+ 'Going to update empty Queue Lifecycle column to "default"');
+
+ my $queues = RT::Queues->new( RT->SystemUser );
+ $queues->FindAllRows;
+ $queues->Limit(
+ FIELD => 'Lifecycle',
+ OPERATOR => 'IS',
+ VALUE => 'NULL',
+ );
+
+ $queues->Limit(
+ FIELD => 'Lifecycle',
+ VALUE => '',
+ ENTRYAGGREGATOR => 'OR',
+ );
+
+ $queues->Limit(
+ FIELD => 'Lifecycle',
+ VALUE => 0,
+ ENTRYAGGREGATOR => 'OR',
+ );
+
+ while ( my $q = $queues->Next ) {
+ $q->SetLifecycle('default');
+ }
+ },
+ sub {
+ use strict;
+ my $groups = RT::Groups->new(RT->SystemUser);
+ $groups->Limit( FIELD => 'Domain',
+ OPERATOR => '=',
+ VALUE => 'Personal'
+ );
+ $groups->LimitToDeleted;
+ while ( my $group = $groups->Next ) {
+ my $members = $group->MembersObj();
+ while ( my $member = $members->Next ) {
+ my ( $ok, $msg ) = $group->DeleteMember( $member->MemberId );
+ if ( !$ok ) {
+ $RT::Logger->warn( "Unable to remove group member "
+ . $member->id . ": "
+ . $msg );
+ }
+ }
+ $group->PrincipalObj->Delete;
+ $group->RT::Record::Delete();
+ }
+ },
+);
--- /dev/null
+#!/usr/bin/perl
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
+# <sales@bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+use strict;
+use warnings;
+
+use lib "local/lib";
+use lib "lib";
+
+use RT;
+RT::LoadConfig();
+RT->Config->Set('LogToScreen' => 'debug');
+RT::Init();
+
+$| = 1;
+
+if (my $re = RT->Config->Get('RTAddressRegexp')) {
+ print "No need to use this script, you already have RTAddressRegexp set to $re\n";
+ exit;
+}
+
+use RT::Queues;
+my $queues = RT::Queues->new( RT->SystemUser );
+$queues->UnLimit;
+
+my %merged;
+merge(\%merged, RT->Config->Get('CorrespondAddress'), RT->Config->Get('CommentAddress'));
+while ( my $queue = $queues->Next ) {
+ merge(\%merged, $queue->CorrespondAddress, $queue->CommentAddress);
+}
+
+my @domains;
+for my $domain (sort keys %merged) {
+ my @addresses;
+ for my $base (sort keys %{$merged{$domain}}) {
+ my @subbits = keys(%{$merged{$domain}{$base}});
+ if (@subbits > 1) {
+ push @addresses, "\Q$base\E(?:".join("|",@subbits).")";
+ } else {
+ push @addresses, "\Q$base\E$subbits[0]";
+ }
+ }
+ if (@addresses > 1) {
+ push @domains, "(?:".join("|", @addresses).")\Q\@".$domain."\E";
+ } else {
+ push @domains, "$addresses[0]\Q\@$domain\E";
+ }
+}
+my $re = join "|", @domains;
+
+print <<ENDDESCRIPTION;
+You can add the following to RT_SiteConfig.pm, but may want to collapse it into a more efficient regexp.
+Keep in mind that this only contains the email addresses that RT knows about, you should also examine
+your mail system for aliases that reach RT but which RT doesn't know about.
+ENDDESCRIPTION
+print "Set(\$RTAddressRegexp,qr{^(?:${re})\$}i);\n";
+
+sub merge {
+ my $merged = shift;
+ for my $address (grep {defined and length} @_) {
+ $address =~ /^\s*(.*?)(-comments?)?\@(.*?)\s*$/;
+ $merged->{lc $3}{$1}{$2||''}++;
+ }
+}
--- /dev/null
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
+# <sales@bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+use strict;
+use warnings;
+
+use RT;
+RT::LoadConfig();
+RT->Config->Set('LogToScreen' => 'debug');
+RT::Init();
+
+$| = 1;
+
+use RT::Users;
+my $users = RT::Users->new( $RT::SystemUser );
+$users->UnLimit();
+
+my @comp_roots = RT::Interface::Web->ComponentRoots;
+my %comp_root_check_cache;
+sub stylesheet_exists {
+ my $stylesheet = shift;
+
+ return $comp_root_check_cache{$stylesheet}
+ if exists $comp_root_check_cache{$stylesheet};
+
+ for my $comp_root (@comp_roots) {
+ return ++$comp_root_check_cache{$stylesheet}
+ if -d "$comp_root/NoAuth/css/$stylesheet";
+ }
+
+ return $comp_root_check_cache{$stylesheet} = 0;
+}
+
+my $system_stylesheet = RT->Config->Get('WebDefaultStylesheet');
+
+while (my $u = $users->Next) {
+ my $stylesheet = RT->Config->Get('WebDefaultStylesheet', $u);
+ unless (stylesheet_exists $stylesheet) {
+ my $prefs = $u->Preferences($RT::System);
+ $prefs->{WebDefaultStylesheet} = $system_stylesheet;
+ $u->SetPreferences($RT::System, $prefs);
+ }
+}
--- /dev/null
+#!/usr/bin/env perl
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
+# <sales@bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+use 5.8.3;
+use strict;
+use warnings;
+
+use RT;
+RT::LoadConfig();
+RT->Config->Set('LogToScreen' => 'debug');
+RT::Init();
+
+use RT::CachedGroupMembers;
+my $cgms = RT::CachedGroupMembers->new( RT->SystemUser );
+$cgms->Limit(
+ FIELD => 'id',
+ OPERATOR => '!=',
+ VALUE => 'main.Via',
+ QUOTEVALUE => 0,
+ ENTRYAGGREGATOR => 'AND',
+);
+$cgms->FindAllRows;
+
+my $alias = $cgms->Join(
+ TYPE => 'LEFT',
+ FIELD1 => 'Via',
+ TABLE2 => 'CachedGroupMembers',
+ FIELD2 => 'id',
+);
+$cgms->Limit(
+ ALIAS => $alias,
+ FIELD => 'MemberId',
+ OPERATOR => '=',
+ VALUE => $alias .'.GroupId',
+ QUOTEVALUE => 0,
+ ENTRYAGGREGATOR => 'AND',
+);
+$cgms->Limit(
+ ALIAS => $alias,
+ FIELD => 'id',
+ OPERATOR => '=',
+ VALUE => $alias .'.Via',
+ QUOTEVALUE => 0,
+ ENTRYAGGREGATOR => 'AND',
+);
+
+$| = 1;
+my $total = $cgms->Count;
+my $i = 0;
+
+FetchNext( $cgms, 'init' );
+while ( my $rec = FetchNext( $cgms ) ) {
+ $i++;
+ printf("\r%0.2f %%", 100 * $i / $total);
+ $RT::Handle->BeginTransaction;
+ my ($status) = $rec->Delete;
+ unless ($status) {
+ print STDERR "Couldn't delete CGM #". $rec->id;
+ exit 1;
+ }
+ $RT::Handle->Commit;
+}
+
+use constant PAGE_SIZE => 10000;
+sub FetchNext {
+ my ($objs, $init) = @_;
+ if ( $init ) {
+ $objs->RowsPerPage( PAGE_SIZE );
+ $objs->FirstPage;
+ return;
+ }
+
+ my $obj = $objs->Next;
+ return $obj if $obj;
+ $objs->RedoSearch;
+ $objs->FirstPage;
+ return $objs->Next;
+}
+
--- /dev/null
+#!/usr/bin/env perl
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
+# <sales@bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+use 5.8.3;
+use strict;
+use warnings;
+
+use RT;
+RT::LoadConfig();
+RT->Config->Set('LogToScreen' => 'debug');
+RT::Init();
+
+use RT::Transactions;
+my $txns = RT::Transactions->new( RT->SystemUser );
+$txns->Limit(
+ FIELD => 'ObjectType',
+ OPERATOR => '=',
+ VALUE => 'RT::Group',
+ QUOTEVALUE => 1,
+ ENTRYAGGREGATOR => 'AND',
+);
+
+my $alias = $txns->Join(
+ TYPE => 'LEFT',
+ FIELD1 => 'ObjectId',
+ TABLE2 => 'Groups',
+ FIELD2 => 'Id',
+);
+$txns->Limit(
+ ALIAS => $alias,
+ FIELD => 'Domain',
+ OPERATOR => '=',
+ VALUE => 'ACLEquivalence',
+ QUOTEVALUE => 1,
+ ENTRYAGGREGATOR => 'AND',
+);
+
+$txns->Limit(
+ ALIAS => $alias,
+ FIELD => 'Type',
+ OPERATOR => '=',
+ VALUE => 'UserEquiv',
+ QUOTEVALUE => 1,
+ ENTRYAGGREGATOR => 'AND',
+);
+
+$| = 1;
+my $total = $txns->Count;
+my $i = 0;
+
+FetchNext( $txns, 'init' );
+while ( my $rec = FetchNext( $txns ) ) {
+ $i++;
+ printf("\r%0.2f %%", 100 * $i / $total);
+ $RT::Handle->BeginTransaction;
+ my ($status) = $rec->Delete;
+ unless ($status) {
+ print STDERR "Couldn't delete TXN #". $rec->id;
+ exit 1;
+ }
+ $RT::Handle->Commit;
+}
+
+use constant PAGE_SIZE => 10000;
+sub FetchNext {
+ my ($objs, $init) = @_;
+ if ( $init ) {
+ $objs->RowsPerPage( PAGE_SIZE );
+ $objs->FirstPage;
+ return;
+ }
+
+ my $obj = $objs->Next;
+ return $obj if $obj;
+ $objs->RedoSearch;
+ $objs->FirstPage;
+ return $objs->Next;
+}
+
--- /dev/null
+#!/usr/bin/perl
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
+# <sales@bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+use strict;
+use warnings;
+
+use lib "local/lib";
+use lib "lib";
+
+use RT;
+RT::LoadConfig();
+RT->Config->Set('LogToScreen' => 'debug');
+RT::Init();
+
+$| = 1;
+
+$RT::Handle->BeginTransaction();
+
+use RT::CustomFields;
+my $CFs = RT::CustomFields->new( RT->SystemUser );
+$CFs->UnLimit;
+$CFs->Limit( FIELD => 'Type', VALUE => 'Select' );
+
+my $seen;
+while (my $cf = $CFs->Next ) {
+ next if $cf->BasedOnObj->Id;
+ my @categories;
+ my %mapping;
+ my $values = $cf->Values;
+ while (my $value = $values->Next) {
+ next unless defined $value->Category and length $value->Category;
+ push @categories, $value->Category unless grep {$_ eq $value->Category} @categories;
+ $mapping{$value->Name} = $value->Category;
+ }
+ next unless @categories;
+
+ $seen++;
+ print "Found CF '@{[$cf->Name]}' with categories:\n";
+ print " $_\n" for @categories;
+
+ print "Split this CF's categories into a hierarchical custom field (Y/n)? ";
+ my $dothis = <>;
+ next if $dothis =~ /n/i;
+
+ print "Enter name of CF to create as category ('@{[$cf->Name]} category'): ";
+ my $newname = <>;
+ chomp $newname;
+ $newname = $cf->Name . " category" unless length $newname;
+
+ # bump the CF's sort oder up by one
+ $cf->SetSortOrder( ($cf->SortOrder || 0) + 1 );
+
+ # ..and add a new CF before it
+ my $new = RT::CustomField->new( RT->SystemUser );
+ my ($id, $msg) = $new->Create(
+ Name => $newname,
+ Type => 'Select',
+ MaxValues => 1,
+ LookupType => $cf->LookupType,
+ SortOrder => $cf->SortOrder - 1,
+ );
+ die "Can't create custom field '$newname': $msg" unless $id;
+
+ # Set the CF to be based on what we just made
+ $cf->SetBasedOn( $new->Id );
+
+ # Apply it to all of the same things
+ {
+ my $ocfs = RT::ObjectCustomFields->new( RT->SystemUser );
+ $ocfs->LimitToCustomField( $cf->Id );
+ while (my $ocf = $ocfs->Next) {
+ my $newocf = RT::ObjectCustomField->new( RT->SystemUser );
+ ($id, $msg) = $newocf->Create(
+ SortOrder => $ocf->SortOrder,
+ CustomField => $new->Id,
+ ObjectId => $ocf->ObjectId,
+ );
+ die "Can't create ObjectCustomField: $msg" unless $id;
+ }
+ }
+
+ # Copy over all of the rights
+ {
+ my $acl = RT::ACL->new( RT->SystemUser );
+ $acl->LimitToObject( $cf );
+ while (my $ace = $acl->Next) {
+ my $newace = RT::ACE->new( RT->SystemUser );
+ ($id, $msg) = $newace->Create(
+ PrincipalId => $ace->PrincipalId,
+ PrincipalType => $ace->PrincipalType,
+ RightName => $ace->RightName,
+ Object => $new,
+ );
+ die "Can't assign rights: $msg" unless $id;
+ }
+ }
+
+ # Add values for all of the categories
+ for my $i (0..$#categories) {
+ ($id, $msg) = $new->AddValue(
+ Name => $categories[$i],
+ SortOrder => $i + 1,
+ );
+ die "Can't create custom field value: $msg" unless $id;
+ }
+
+ # Grovel through all ObjectCustomFieldValues, and add the
+ # appropriate category
+ {
+ my $ocfvs = RT::ObjectCustomFieldValues->new( RT->SystemUser );
+ $ocfvs->LimitToCustomField( $cf->Id );
+ while (my $ocfv = $ocfvs->Next) {
+ next unless exists $mapping{$ocfv->Content};
+ my $newocfv = RT::ObjectCustomFieldValue->new( RT->SystemUser );
+ ($id, $msg) = $newocfv->Create(
+ CustomField => $new->Id,
+ ObjectType => $ocfv->ObjectType,
+ ObjectId => $ocfv->ObjectId,
+ Content => $mapping{$ocfv->Content},
+ );
+ }
+ }
+}
+
+$RT::Handle->Commit;
+print "No custom fields with categories found\n" unless $seen;
--- /dev/null
+#!/usr/bin/perl
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
+# <sales@bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+use strict;
+use warnings;
+
+use lib "local/lib";
+use lib "lib";
+
+use RT;
+RT::LoadConfig();
+RT->Config->Set('LogToScreen' => 'debug');
+RT::Init();
+
+$| = 1;
+
+my $db_name = RT->Config->Get('DatabaseName');
+my $db_type = RT->Config->Get('DatabaseType');
+
+my $dbh = $RT::Handle->dbh;
+
+my $found_fm_tables;
+foreach my $name ( $RT::Handle->_TableNames ) {
+ next unless $name =~ /^fm_/i;
+ $found_fm_tables->{lc $name}++;
+}
+
+unless ( $found_fm_tables->{fm_topics} && $found_fm_tables->{fm_objecttopics} ) {
+ warn "Couldn't find topics tables, it appears you have RTFM 2.0 or earlier.";
+ warn "This script cannot yet upgrade RTFM versions which are that old";
+ exit;
+}
+
+{ # port over Articles
+ my @columns = qw(id Name Summary SortOrder Class Parent URI Creator Created LastUpdatedBy LastUpdated);
+ copy_tables('FM_Articles','Articles',\@columns);
+
+}
+
+
+{ # port over Classes
+ my @columns = qw(id Name Description SortOrder Disabled Creator Created LastUpdatedBy LastUpdated);
+ if ( grep lc($_) eq 'hotlist', $RT::Handle->Fields('FM_Classes') ) {
+ push @columns, 'HotList';
+ }
+ copy_tables('FM_Classes','Classes',\@columns);
+}
+
+{ # port over Topics
+ my @columns = qw(id Parent Name Description ObjectType ObjectId);
+ copy_tables('FM_Topics','Topics',\@columns);
+}
+
+{ # port over ObjectTopics
+ my @columns = qw(id Topic ObjectType ObjectId);
+ copy_tables('FM_ObjectTopics','ObjectTopics',\@columns);
+}
+
+sub copy_tables {
+ my ($source, $dest, $columns) = @_;
+ my $column_list = join(', ',@$columns);
+ my $sql;
+ # SQLite: http://www.sqlite.org/lang_insert.html
+ if ( $db_type eq 'mysql' || $db_type eq 'SQLite' ) {
+ $sql = "insert into $dest ($column_list) select $column_list from $source";
+ }
+ # Oracle: http://www.adp-gmbh.ch/ora/sql/insert/select_and_subquery.html
+ elsif ( $db_type eq 'Pg' || $db_type eq 'Oracle' ) {
+ $sql = "insert into $dest ($column_list) (select $column_list from $source)";
+ }
+ $RT::Logger->debug($sql);
+ $dbh->do($sql);
+}
+
+{ # create ObjectClasses
+ # this logic will need updating when folks have an FM_ObjectClasses table
+ use RT::Classes;
+ use RT::ObjectClass;
+
+ my $classes = RT::Classes->new(RT->SystemUser);
+ $classes->UnLimit;
+ while ( my $class = $classes->Next ) {
+ my $objectclass = RT::ObjectClass->new(RT->SystemUser);
+ my ($ret, $msg ) = $objectclass->Create( Class => $class->Id, ObjectType => 'RT::System', ObjectId => 0 );
+ if ($ret) {
+ warn("Applied Class '".$class->Name."' globally");
+ } else {
+ warn("Couldn't create linkage for Class ".$class->Name.": $msg");
+ }
+ }
+}
+
+{ # update ACLs
+ use RT::ACL;
+ my $acl = RT::ACL->new(RT->SystemUser);
+ $acl->Limit( FIELD => 'ObjectType', VALUE => 'RT::FM::Class' );
+ $acl->Limit( FIELD => 'ObjectType', VALUE => 'RT::FM::System' );
+ while ( my $ace = $acl->Next ) {
+ if ( $ace->__Value('ObjectType') eq 'RT::FM::Class' ) {
+ my ($ret, $msg ) = $ace->__Set( Field => 'ObjectType', Value => 'RT::Class');
+ warn "Fixing ACL ".$ace->Id." to refer to RT::Class: $msg";
+ } elsif ( $ace->__Value('ObjectType') eq 'RT::FM::System' ) {
+ my ($ret, $msg) = $ace->__Set(Field => 'ObjectType', Value => 'RT::System');
+ warn "Fixing ACL ".$ace->Id." to refer to RT::System: $msg";
+ }
+ }
+
+
+}
+
+{ # update CustomFields
+ use RT::CustomFields;
+ my $cfs = RT::CustomFields->new(RT->SystemUser);
+ $cfs->Limit( FIELD => 'LookupType', VALUE => 'RT::FM::Class-RT::FM::Article' );
+ while ( my $cf = $cfs->Next ) {
+ my ($ret, $msg) = $cf->__Set( Field => 'LookupType', Value => 'RT::Class-RT::Article' );
+ warn "Update Custom Field LookupType for CF.".$cf->Id." $msg";
+ }
+}
+
+{ # update ObjectCustomFieldValues
+ use RT::ObjectCustomFieldValues;
+ my $ocfvs = RT::ObjectCustomFieldValues->new(RT->System);
+ $ocfvs->Limit( FIELD => 'ObjectType', VALUE => 'RT::FM::Article' );
+ while ( my $ocfv = $ocfvs->Next ) {
+ my ($ret, $msg) = $ocfv->__Set( Field => 'ObjectType', Value => 'RT::Article' );
+ warn "Updated CF ".$ocfv->__Value('CustomField')." Value for Article ".$ocfv->__Value('ObjectId');
+ }
+
+}
+
+{ # update Topics
+ use RT::Topics;
+ my $topics = RT::Topics->new(RT->SystemUser);
+ $topics->Limit( FIELD => 'ObjectType', VALUE => 'RT::FM::Class' );
+ $topics->Limit( FIELD => 'ObjectType', VALUE => 'RT::FM::System' );
+ while ( my $topic = $topics->Next ) {
+ if ( $topic->__Value('ObjectType') eq 'RT::FM::Class' ) {
+ my ($ret, $msg ) = $topic->__Set( Field => 'ObjectType', Value => 'RT::Class');
+ warn "Fixing Topic ".$topic->Id." to refer to RT::Class: $msg";
+ } elsif ( $topic->__Value('ObjectType') eq 'RT::FM::System' ) {
+ my ($ret, $msg) = $topic->__Set(Field => 'ObjectType', Value => 'RT::System');
+ warn "Fixing Topic ".$topic->Id." to refer to RT::System: $msg";
+ }
+ }
+}
+
+{ # update ObjectTopics
+ use RT::ObjectTopics;
+ my $otopics = RT::ObjectTopics->new(RT->SystemUser);
+ $otopics->UnLimit;
+ while ( my $otopic = $otopics->Next ) {
+ if ( $otopic->ObjectType eq 'RT::FM::Article' ) {
+ my ($ret, $msg) = $otopic->SetObjectType('RT::Article');
+ warn "Fixing Topic ".$otopic->Topic." to apply to article: $msg";
+ }
+ }
+}
+
+{ # update Links
+ use RT::Links;
+ my $links = RT::Links->new(RT->SystemUser);
+ $links->Limit(FIELD => 'Base', VALUE => 'rtfm', OPERATOR => 'LIKE', SUBCLAUSE => 'stopanding', ENTRYAGGREGATOR => 'OR');
+ $links->Limit(FIELD => 'Target', VALUE => 'rtfm', OPERATOR => 'LIKE', SUBCLAUSE => 'stopanding', ENTRYAGGREGATOR => 'OR' );
+ while ( my $link = $links->Next ) {
+ my $base = $link->__Value('Base');
+ my $target = $link->__Value('Target');
+ if ( $base =~ s/rtfm/article/i ) {
+ my ($ret, $msg) = $link->__Set( Field => 'Base', Value => $base );
+ warn "Updating base to $base: $msg for link ".$link->id;
+ }
+ if ( $target =~ s/rtfm/article/i ) {
+ my ($ret, $msg) = $link->__Set( Field => 'Target', Value => $target );
+ warn "Updating target to $target: $msg for link ".$link->id;
+ }
+
+ }
+}
+
+{ # update Transactions
+ # we only keep article transactions at this point
+ no warnings 'once';
+ use RT::Transactions;
+ # Next calls Type to check readability and Type calls _Accessible
+ # which called CurrentUserCanSee which calls Object which tries to instantiate
+ # an RT::FM::Article. Rather than a shim RT::FM::Article class, I'm just avoiding
+ # the ACL check since we're running around as the superuser.
+ local *RT::Transaction::Type = sub { shift->__Value('Type') };
+ my $transactions = RT::Transactions->new(RT->SystemUser);
+ $transactions->Limit( FIELD => 'ObjectType', VALUE => 'RT::FM::Article' );
+ while ( my $t = $transactions->Next ) {
+ my ($ret, $msg) = $t->__Set( Field => 'ObjectType', Value => 'RT::Article' );
+ warn "Updated Transaction ".$t->Id." to point to RT::Article";
+ }
+
+ # we also need to change links that point to articles
+ $transactions = RT::Transactions->new(RT->SystemUser);
+ $transactions->Limit( FIELD => 'Type', VALUE => 'AddLink' );
+ $transactions->Limit( FIELD => 'NewValue', VALUE => 'rtfm', OPERATOR => 'LIKE' );
+ while ( my $t = $transactions->Next ) {
+ my $value = $t->__Value('NewValue');
+ $value =~ s/rtfm/article/;
+ my ($ret, $msg) = $t->__Set( Field => 'NewValue', Value => $value );
+ warn "Updated Transaction ".$t->Id." to link to $value";
+ }
+}
+
+{ # update Attributes
+ # these are all things we should make real columns someday
+ use RT::Attributes;
+ my $attributes = RT::Attributes->new(RT->SystemUser);
+ $attributes->Limit( FIELD => 'ObjectType', VALUE => 'RT::FM::Class' );
+ while ( my $a = $attributes->Next ) {
+ my ($ret,$msg) = $a->__Set( Field => 'ObjectType', Value => 'RT::Class' );
+ warn "Updating Attribute ".$a->Name." to point to RT::Class";
+ }
+}
--- /dev/null
+#!/usr/bin/env perl
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
+# <sales@bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+use strict;
+use warnings;
+
+use DBI;
+use DBD::mysql 4.002;
+
+unless (@ARGV) {
+ print STDERR "usage: $0 db_name[:server_name] db_user db_password\n";
+ exit 1;
+}
+
+# pretty correct support of charsets has been introduced in mysql 4.1
+# as RT doesn't use it may result in issues:
+# 1) data corruptions when default charset of mysql server has data restrictions like utf8
+# 2) wrong ordering (collations)
+
+# we have to define correct types for all columns. RT uses UTF-8, ascii and binary.
+# * ascii is subset of many mysql's charsets except may be one or two rare where some ascii
+# characters replaced with local
+# * for many charsets mysql allows us to store any octets sequences even when those are
+# invalid for this particula set, for example we can store UTF-8 data in latin1
+# column and fetch it as UTF-8, however sorting will be wrong
+
+# here is tricky algorithm to change column to desired charset:
+# * text to binary convertion is pretty straight forward except that text types
+# have length definitions in terms of characters and in some cases we must
+# use longer binary types to satisfy space requirements
+# * binary to text is much easier as we know that there is ascii or UTF-8 then
+# we just make convertion, also 32 chars are long enough to store 32 bytes, so
+# length changes is not required
+# * text to text convertion is trickier. no matter what is the current character set
+# of the column we know that there is either ascii or UTF-8, so we can not use
+# direct convertion, instead we do text to binary plus binary to text convertion
+# instead
+# * as well we add charset definition for all tables and for the DB as well,
+# so all new columns by default will be in UTF-8 charset
+
+my @tables = qw(
+ ACL
+ Attachments
+ Attributes
+ CustomFields
+ CustomFieldValues
+ GroupMembers
+ Groups
+ Links
+ ObjectCustomFields
+ ObjectCustomFieldValues
+ Principals
+ Queues
+ ScripActions
+ ScripConditions
+ Scrips
+ sessions
+ Templates
+ Tickets
+ Transactions
+ Users
+ FM_Articles
+ FM_Classes
+ FM_ObjectTopics
+ FM_Topics
+);
+
+my %charset = (
+ ACL => {
+ RightName => 'ascii',
+ ObjectType => 'ascii',
+ PrincipalType => 'ascii',
+ },
+ Attachments => {
+ MessageId => 'ascii',
+ Subject => 'utf8',
+ Filename => 'utf8',
+ ContentType => 'ascii',
+ ContentEncoding => 'ascii',
+ Content => 'binary',
+ Headers => 'utf8',
+ },
+ Attributes => {
+ Name => 'utf8',
+ Description => 'utf8',
+ Content => 'binary',
+ ContentType => 'ascii',
+ ObjectType => 'ascii',
+ },
+ CustomFields => {
+ Name => 'utf8',
+ Type => 'ascii',
+ Pattern => 'utf8',
+ Description => 'utf8',
+ LookupType => 'ascii',
+ },
+ CustomFieldValues => {
+ Name => 'utf8',
+ Description => 'utf8',
+ },
+ FM_Articles => {
+ Name => 'utf8',
+ Summary => 'utf8',
+ URI => 'ascii',
+ },
+ FM_Classes => {
+ Name => 'utf8',
+ Description => 'utf8',
+ },
+ FM_ObjectTopics => {
+ ObjectType => 'ascii',
+ },
+ FM_Topics => {
+ Name => 'utf8',
+ Description => 'utf8',
+ ObjectType => 'ascii',
+ },
+ Groups => {
+ Name => 'utf8',
+ Description => 'utf8',
+ Domain => 'ascii',
+ Type => 'ascii',
+ },
+ Links => {
+ Base => 'ascii',
+ Target => 'ascii',
+ Type => 'ascii',
+ },
+ ObjectCustomFieldValues => {
+ ObjectType => 'ascii',
+ Content => 'utf8',
+ LargeContent => 'binary',
+ ContentType => 'ascii',
+ ContentEncoding => 'ascii',
+ },
+ Principals => {
+ PrincipalType => 'ascii',
+ },
+ Queues => {
+ Name => 'utf8',
+ Description => 'utf8',
+ CorrespondAddress => 'ascii',
+ CommentAddress => 'ascii',
+ },
+ ScripActions => {
+ Name => 'utf8',
+ Description => 'utf8',
+ ExecModule => 'ascii',
+ Argument => 'binary',
+ },
+ ScripConditions => {
+ Name => 'utf8',
+ Description => 'utf8',
+ ExecModule => 'ascii',
+ Argument => 'binary',
+ ApplicableTransTypes => 'ascii',
+ },
+ Scrips => {
+ Description => 'utf8',
+ ConditionRules => 'utf8',
+ ActionRules => 'utf8',
+ CustomIsApplicableCode => 'utf8',
+ CustomPrepareCode => 'utf8',
+ CustomCommitCode => 'utf8',
+ Stage => 'ascii',
+ },
+ sessions => {
+ id => 'binary', # ascii?
+ a_session => 'binary',
+ },
+ Templates => {
+ Name => 'utf8',
+ Description => 'utf8',
+ Type => 'ascii',
+ Language => 'ascii',
+ Content => 'utf8',
+ },
+ Tickets => {
+ Type => 'ascii',
+ Subject => 'utf8',
+ Status => 'ascii',
+ },
+ Transactions => {
+ ObjectType => 'ascii',
+ Type => 'ascii',
+ Field => 'ascii',
+ OldValue => 'utf8',
+ NewValue => 'utf8',
+ ReferenceType => 'ascii',
+ Data => 'utf8',
+ },
+ Users => {
+ Name => 'utf8',
+ Password => 'binary',
+ Comments => 'utf8',
+ Signature => 'utf8',
+ EmailAddress => 'ascii',
+ FreeformContactInfo => 'utf8',
+ Organization => 'utf8',
+ RealName => 'utf8',
+ NickName => 'utf8',
+ Lang => 'ascii',
+ EmailEncoding => 'ascii',
+ WebEncoding => 'ascii',
+ ExternalContactInfoId => 'utf8',
+ ContactInfoSystem => 'utf8',
+ ExternalAuthId => 'utf8',
+ AuthSystem => 'utf8',
+ Gecos => 'utf8',
+ HomePhone => 'utf8',
+ WorkPhone => 'utf8',
+ MobilePhone => 'utf8',
+ PagerPhone => 'utf8',
+ Address1 => 'utf8',
+ Address2 => 'utf8',
+ City => 'utf8',
+ State => 'utf8',
+ Zip => 'utf8',
+ Country => 'utf8',
+ Timezone => 'ascii',
+ PGPKey => 'binary',
+ },
+);
+
+my %max_type_length = (
+ char => int 1<<8,
+ varchar => int 1<<8,
+ tinytext => int 1<<8,
+ mediumtext => int 1<<16,
+ text => int 1<<24,
+ longtext => int 1<<32,
+);
+
+my @sql_commands;
+
+my ($db_datasource, $db_user, $db_pass) = (shift, shift, shift);
+my $dbh = DBI->connect("dbi:mysql:$db_datasource", $db_user, $db_pass, { RaiseError => 1 });
+my $db_name = $db_datasource;
+$db_name =~ s/:.*$//;
+
+my $version = ($dbh->selectrow_array("show variables like 'version'"))[1];
+($version) = $version =~ /^(\d+\.\d+)/;
+
+push @sql_commands, qq{ALTER DATABASE `$db_name` DEFAULT CHARACTER SET utf8};
+convert_table($_) foreach @tables;
+
+print join "\n", map(/;$/? $_ : "$_;", @sql_commands), "";
+my $use_p = $db_pass ? " -p" : '';
+print STDERR <<ENDREMINDER;
+-- ** NOTICE: No database changes have been made. **
+-- Please review the generated SQL, ensure you have a full backup of your database
+-- and apply it to your database using a command like:
+-- mysql -u ${db_user}${use_p} $db_name < queries.sql";
+ENDREMINDER
+exit 0;
+
+my %alter_aggregator;
+sub convert_table {
+ my $table = shift;
+ @alter_aggregator{'char_to_binary','binary_to_char'} = (['DEFAULT CHARACTER SET utf8'],[]);
+
+ my $sth = $dbh->column_info( undef, $db_name, $table, undef );
+ $sth->execute;
+ my $columns = $sth->fetchall_arrayref({});
+ return unless @$columns;
+ foreach my $info (@$columns) {
+ convert_column(%$info);
+ }
+ for my $conversiontype (qw(char_to_binary binary_to_char)) {
+ next unless @{$alter_aggregator{$conversiontype}};
+ push @sql_commands, qq{ALTER TABLE $table\n }.
+ join(",\n ",@{$alter_aggregator{$conversiontype}});
+ }
+}
+
+sub convert_column {
+ my %info = @_;
+ my $table = $info{'TABLE_NAME'};
+ my $column = $info{'COLUMN_NAME'};
+ my $type = $info{'TYPE_NAME'};
+ return unless $type =~ /(CHAR|TEXT|BLOB|BINARY)$/i;
+
+ my $required_charset = $charset{$table}{$column};
+ unless ( $required_charset ) {
+ print STDERR join(".", @info{'TABLE_SCHEMA', 'TABLE_NAME', 'COLUMN_NAME'})
+ ." has type $type however mapping is missing.\n";
+ return;
+ }
+
+ my $collation = column_info($table, $column)->{'collation'};
+ # mysql 4.1 returns literal NULL instead of undef
+ my $current_charset = $collation && $collation ne 'NULL'? (split /_/, $collation)[0]: 'binary';
+ return if $required_charset eq $current_charset;
+
+ if ( $required_charset eq 'binary' ) {
+ char_to_binary(%info);
+ }
+ elsif ( $current_charset eq 'binary' ) {
+ binary_to_char( $required_charset, %info);
+ } else {
+ char_to_char( $required_charset, %info);
+ }
+}
+
+sub char_to_binary {
+ my %info = @_;
+
+ my $table = $info{'TABLE_NAME'};
+ my $column = $info{'COLUMN_NAME'};
+ my $new_type = calc_suitable_binary_type(%info);
+ push @{$alter_aggregator{char_to_binary}},
+ "MODIFY $column $new_type ".build_column_definition(%info);
+
+}
+
+sub binary_to_char {
+ my ($charset, %info) = @_;
+
+ my $table = $info{'TABLE_NAME'};
+ my $column = $info{'COLUMN_NAME'};
+ my $new_type = lc $info{'TYPE_NAME'};
+ if ( $new_type =~ /binary/ ) {
+ $new_type =~ s/binary/char/;
+ $new_type .= '('. $info{'COLUMN_SIZE'} .')';
+ } else {
+ $new_type =~ s/blob/text/;
+ }
+
+ push @{$alter_aggregator{binary_to_char}},
+ "MODIFY $column ". uc($new_type) ." CHARACTER SET ". $charset
+ ." ". build_column_definition(%info);
+}
+
+sub char_to_char {
+ my ($charset, %info) = @_;
+
+ my $table = $info{'TABLE_NAME'};
+ my $column = $info{'COLUMN_NAME'};
+ my $new_type = $info{'mysql_type_name'};
+
+ char_to_binary(%info);
+ push @{$alter_aggregator{binary_to_char}},
+ "MODIFY $column ". uc($new_type)." CHARACTER SET ". $charset
+ ." ". build_column_definition(%info);
+}
+
+sub calc_suitable_binary_type {
+ my %info = @_;
+ my $type = lc $info{'TYPE_NAME'};
+ return 'LONGBLOB' if $type eq 'longtext';
+
+ my $current_max_byte_length = column_byte_length(@info{qw(TABLE_NAME COLUMN_NAME)}) || 0;
+ if ( $max_type_length{ $type } > $current_max_byte_length ) {
+ if ( $type eq 'varchar' || $type eq 'char' ) {
+ my $new_type = $type;
+ $new_type =~ s/char/binary/;
+ $new_type .= $info{'COLUMN_SIZE'} >= $current_max_byte_length
+ ? '('. $info{'COLUMN_SIZE'} .')'
+ : '('. $current_max_byte_length .')';
+ return uc $new_type;
+ } else {
+ my $new_type = $type;
+ $new_type =~ s/text/blob/;
+ return uc $new_type;
+ }
+ } else {
+ my $new_type;
+ foreach ( sort { $max_type_length{$a} <=> $max_type_length{$b} } keys %max_type_length ) {
+ next if $max_type_length{ $_ } <= $current_max_byte_length;
+
+ $new_type = $_; last;
+ }
+ $new_type =~ s/text/blob/;
+ return uc $new_type;
+ }
+}
+
+sub build_column_definition {
+ my %info = @_;
+
+ my $res = '';
+ $res .= 'NOT ' unless $info{'NULLABLE'};
+ $res .= 'NULL';
+ my $default = column_info(@info{qw(TABLE_NAME COLUMN_NAME)})->{default};
+ if ( defined $default ) {
+ $res .= ' DEFAULT '. $dbh->quote($default);
+ } elsif ( $info{'NULLABLE'} ) {
+ $res .= ' DEFAULT NULL';
+ }
+ $res .= ' AUTO_INCREMENT' if $info{'mysql_is_auto_increment'};
+ return $res;
+}
+
+sub column_byte_length {
+ my ($table, $column) = @_;
+ if ( $version >= 5.0 ) {
+ # information_schema searches can be case sensitive
+ # and users may use lower_case_table_names, use LOWER
+ # for everything just in case
+ # http://dev.mysql.com/doc/refman/5.1/en/charset-collation-information-schema.html
+ my ($char, $octet) = @{ $dbh->selectrow_arrayref(
+ "SELECT CHARACTER_MAXIMUM_LENGTH, CHARACTER_OCTET_LENGTH FROM information_schema.COLUMNS WHERE"
+ ." LOWER(TABLE_SCHEMA) = ". lc( $dbh->quote($db_name) )
+ ." AND LOWER(TABLE_NAME) = ". lc( $dbh->quote($table) )
+ ." AND LOWER(COLUMN_NAME) = ". lc( $dbh->quote($column) )
+ ) };
+ return $octet if $octet == $char;
+ }
+ return $dbh->selectrow_arrayref("SELECT MAX(LENGTH(". $dbh->quote_identifier($column) .")) FROM $table")->[0];
+}
+
+sub column_info {
+ my ($table, $column) = @_;
+ # XXX: DBD::mysql doesn't provide this info, may be will do in 4.0007 if I'll write a patch
+ local $dbh->{FetchHashKeyName} = 'NAME_lc';
+ return $dbh->selectrow_hashref("SHOW FULL COLUMNS FROM $table LIKE " . $dbh->quote($column));
+}
+
--- /dev/null
+#!/usr/bin/perl
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
+# <sales@bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+use strict;
+use warnings;
+
+use lib "local/lib";
+use lib "lib";
+
+use RT;
+RT::LoadConfig;
+RT::Init;
+
+$| = 1;
+
+use Getopt::Long;
+use Digest::SHA;
+my $fix;
+GetOptions("fix!" => \$fix);
+
+use RT::Users;
+my $users = RT::Users->new( $RT::SystemUser );
+$users->Limit(
+ FIELD => 'Password',
+ OPERATOR => 'IS NOT',
+ VALUE => 'NULL',
+ ENTRYAGGREGATOR => 'AND',
+);
+$users->Limit(
+ FIELD => 'Password',
+ OPERATOR => '!=',
+ VALUE => '*NO-PASSWORD*',
+ ENTRYAGGREGATOR => 'AND',
+);
+$users->Limit(
+ FIELD => 'Password',
+ OPERATOR => 'NOT STARTSWITH',
+ VALUE => '!',
+ ENTRYAGGREGATOR => 'AND',
+);
+push @{$users->{'restrictions'}{ "main.Password" }}, "AND", {
+ field => 'LENGTH(main.Password)',
+ op => '<',
+ value => '40',
+};
+
+# we want to update passwords on disabled users
+$users->{'find_disabled_rows'} = 1;
+
+my $count = $users->Count;
+if ($count == 0) {
+ print "No users with unsalted or weak cryptography found.\n";
+ exit 0;
+}
+
+if ($fix) {
+ print "Upgrading $count users...\n";
+ while (my $u = $users->Next) {
+ my $stored = $u->__Value("Password");
+ my $raw;
+ if (length $stored == 32) {
+ $raw = pack("H*",$stored);
+ } elsif (length $stored == 22) {
+ $raw = MIME::Base64::decode_base64($stored);
+ } elsif (length $stored == 13) {
+ printf "%20s => Old crypt() format, cannot upgrade\n", $u->Name;
+ } else {
+ printf "%20s => Unknown password format!\n", $u->Name;
+ }
+ next unless $raw;
+
+ my $salt = pack("C4",map{int rand(256)} 1..4);
+ my $sha = Digest::SHA::sha256(
+ $salt . $raw
+ );
+ $u->_Set(
+ Field => "Password",
+ Value => MIME::Base64::encode_base64(
+ $salt . substr($sha,0,26), ""),
+ );
+ }
+ print "Done.\n";
+ exit 0;
+} else {
+ if ($count < 20) {
+ print "$count users found with unsalted or weak-cryptography passwords:\n";
+ print " Id | Name\n", "-"x9, "+", "-"x9, "\n";
+ while (my $u = $users->Next) {
+ printf "%8d | %s\n", $u->Id, $u->Name;
+ }
+ } else {
+ print "$count users found with unsalted or weak-cryptography passwords\n";
+ }
+
+ print "\n", "Run again with --fix to upgrade.\n";
+ exit 1;
+}
=head2 AddAttachment $attachment
-Takes one attachment object of L<RT::Attachmment> class and attaches it to the message
+Takes one attachment object of L<RT::Attachment> class and attaches it to the message
we're building.
=cut
and $attach->TransactionObj->CurrentUserCanSee;
# ->attach expects just the disposition type; extract it if we have the header
+ # or default to "attachment"
my $disp = ($attach->GetHeader('Content-Disposition') || '')
- =~ /^\s*(inline|attachment)/i ? $1 : undef;
+ =~ /^\s*(inline|attachment)/i ? $1 : "attachment";
$MIMEObj->attach(
Type => $attach->ContentType,
Charset => $attach->OriginalEncoding,
Data => $attach->OriginalContent,
- Disposition => $disp, # a false value defaults to inline in MIME::Entity
+ Disposition => $disp,
Filename => $self->MIMEEncodeString( $attach->Filename ),
'RT-Attachment:' => $self->TicketObj->Id . "/"
. $self->TransactionObj->Id . "/"
@_);
if ($args{Object} and UNIVERSAL::can($args{Object}, 'Id')) {
- $args{ObjectType} = ref($args{Object});
+ $args{ObjectType} = $args{Object}->isa("RT::CurrentUser") ? "RT::User" : ref($args{Object});
$args{ObjectId} = $args{Object}->Id;
} else {
return(0, $self->loc("Required parameter '[_1]' not specified", 'Object'));
unless (eval { $obj->id} ){
return undef;
}
- $self->Limit(FIELD => 'ObjectType', OPERATOR=> '=', VALUE => ref($obj), ENTRYAGGREGATOR => 'OR');
+
+ my $type = $obj->isa("RT::CurrentUser") ? "RT::User" : ref($obj);
+
+ $self->Limit(FIELD => 'ObjectType', OPERATOR=> '=', VALUE => $type, ENTRYAGGREGATOR => 'OR');
$self->Limit(FIELD => 'ObjectId', OPERATOR=> '=', VALUE => $obj->id, ENTRYAGGREGATOR => 'OR', QUOTEVALUE => 0);
}
sub Table {'Classes'}
+=head2 _Init
+
+=cut
+
+ sub _Init {
+ my $self = shift;
+ $self->{'with_disabled_column'} = 1;
+ return ($self->SUPER::_Init(@_));
+ }
=head2 Next
$ref_type = 'SCALAR' if $ref_type eq 'REF';
my $entry_ref = *{$entry}{ $ref_type };
+ next if ref $entry_ref && ref $entry_ref ne ref $ref;
next unless $entry_ref;
# if references are equal then we've found
use strict;
use warnings;
-
+use Scalar::Util 'blessed';
use base 'RT::Record';
return ( $self->loc( $FriendlyObjectTypes[$#types], @types ) );
}
+=head1 RecordClassFromLookupType
+
+Returns the type of Object referred to by ObjectCustomFields' ObjectId column
+
+Optionally takes a LookupType to use instead of using the value on the loaded
+record. In this case, the method may be called on the class instead of an
+object.
+
+=cut
+
sub RecordClassFromLookupType {
my $self = shift;
- my ($class) = ($self->LookupType =~ /^([^-]+)/);
+ my $type = shift || $self->LookupType;
+ my ($class) = ($type =~ /^([^-]+)/);
unless ( $class ) {
- $RT::Logger->error(
- "Custom Field #". $self->id
- ." has incorrect LookupType '". $self->LookupType ."'"
- );
+ if (blessed($self) and $self->LookupType eq $type) {
+ $RT::Logger->error(
+ "Custom Field #". $self->id
+ ." has incorrect LookupType '$type'"
+ );
+ } else {
+ RT->Logger->error("Invalid LookupType passed as argument: $type");
+ }
+ return undef;
+ }
+ return $class;
+}
+
+=head1 ObjectTypeFromLookupType
+
+Returns the ObjectType used in ObjectCustomFieldValues rows for this CF
+
+Optionally takes a LookupType to use instead of using the value on the loaded
+record. In this case, the method may be called on the class instead of an
+object.
+
+=cut
+
+sub ObjectTypeFromLookupType {
+ my $self = shift;
+ my $type = shift || $self->LookupType;
+ my ($class) = ($type =~ /([^-]+)$/);
+ unless ( $class ) {
+ if (blessed($self) and $self->LookupType eq $type) {
+ $RT::Logger->error(
+ "Custom Field #". $self->id
+ ." has incorrect LookupType '$type'"
+ );
+ } else {
+ RT->Logger->error("Invalid LookupType passed as argument: $type");
+ }
return undef;
}
return $class;
use base qw(RT::CustomFieldValues::External);
+=head1 NAME
+
+RT::CustomFieldValues::Groups - Provide RT's groups as a dynamic list of CF values
+
+=head1 SYNOPSIS
+
+To use as a source of CF values, add the following to your F<RT_SiteConfig.pm>
+and restart RT.
+
+ # In RT_SiteConfig.pm
+ Set( @CustomFieldValuesSources, "RT::CustomFieldValues::Groups" );
+
+Then visit the modify CF page in the RT admin configuration.
+
+=head1 METHODS
+
+Most methods are inherited from L<RT::CustomFieldValues::External>, except the
+ones below.
+
+=head2 SourceDescription
+
+Returns a brief string describing this data source.
+
+=cut
+
sub SourceDescription {
return 'RT user defined groups';
}
+=head2 ExternalValues
+
+Returns an arrayref containing a hashref for each possible value in this data
+source, where the value name is the group name.
+
+=cut
+
sub ExternalValues {
my $self = shift;
$self->Limit( FIELD => 'LookupType', STARTSWITH => "$lookup" );
}
+=head2 LimitToObjectId
+
+Takes an ObjectId and limits the collection to CFs applied to said object.
+
+When called multiple times the ObjectId limits are joined with OR.
+
+=cut
+
+sub LimitToObjectId {
+ my $self = shift;
+ my $id = shift;
+ $self->Limit(
+ ALIAS => $self->_OCFAlias,
+ FIELD => 'ObjectId',
+ OPERATOR => '=',
+ VALUE => $id || 0,
+ ENTRYAGGREGATOR => 'OR'
+ );
+}
=head2 LimitToGlobalOrObjectId
foreach my $id (@_) {
- $self->Limit( ALIAS => $self->_OCFAlias,
- FIELD => 'ObjectId',
- OPERATOR => '=',
- VALUE => $id || 0,
- ENTRYAGGREGATOR => 'OR' );
- $global_only = 0 if $id;
+ $self->LimitToObjectId($id);
+ $global_only = 0 if $id;
}
- $self->Limit( ALIAS => $self->_OCFAlias,
- FIELD => 'ObjectId',
- OPERATOR => '=',
- VALUE => 0,
- ENTRYAGGREGATOR => 'OR' ) unless $global_only;
+ $self->LimitToObjectId(0) unless $global_only;
}
sub _LimitToOCFs {
# accommodate this by pausing and retrying.
last
if ( $fh, $temp_file ) =
- eval { File::Temp::tempfile( undef, UNLINK => 0 ) };
+ eval { File::Temp::tempfile( UNLINK => 0 ) };
sleep 1;
}
if ($fh) {
use warnings;
use strict;
-our $VERSION = '4.0.13';
+our $VERSION = '4.0.17';
$self->SUPER::Connect(
User => RT->Config->Get('DatabaseUser'),
Password => RT->Config->Get('DatabasePassword'),
+ DisconnectHandleOnDestroy => 1,
%args,
);
Port => $db_port,
Driver => $db_type,
RequireSSL => RT->Config->Get('DatabaseRequireSSL'),
- DisconnectHandleOnDestroy => 1,
);
if ( $db_type eq 'Oracle' && $db_host ) {
$args{'SID'} = delete $args{'Database'};
push @{$self->{'StatementLog'}} , ([Time::HiRes::time(), $statement, [@bind], $duration, HTML::Mason::Exception->new->as_string]);
}
+
+sub _TableNames {
+ my $self = shift;
+ my $dbh = shift || $self->dbh;
+
+ {
+ local $@;
+ if (
+ $dbh->{Driver}->{Name} eq 'Pg'
+ && $dbh->{'pg_server_version'} >= 90200
+ && !eval { DBD::Pg->VERSION('2.19.3'); 1 }
+ ) {
+ die "You're using PostgreSQL 9.2 or newer. You have to upgrade DBD::Pg module to 2.19.3 or newer: $@";
+ }
+ }
+
+ my @res;
+
+ my $sth = $dbh->table_info( '', undef, undef, "'TABLE'");
+ while ( my $table = $sth->fetchrow_hashref ) {
+ push @res, $table->{TABLE_NAME} || $table->{table_name};
+ }
+
+ return @res;
+}
+
__PACKAGE__->FinalizeDatabaseType;
RT::Base->_ImportOverlays();
# SetOutgoingMailFrom and bounces conflict, since they both want -f
if ( $args{'Bounce'} ) {
push @args, shellwords(RT->Config->Get('SendmailBounceArguments'));
- } elsif ( RT->Config->Get('SetOutgoingMailFrom') ) {
- my $OutgoingMailAddress;
+ } elsif ( my $MailFrom = RT->Config->Get('SetOutgoingMailFrom') ) {
+ my $OutgoingMailAddress = $MailFrom =~ /\@/ ? $MailFrom : undef;
+ my $Overrides = RT->Config->Get('OverrideOutgoingMailFrom') || {};
if ($TicketObj) {
my $QueueName = $TicketObj->QueueObj->Name;
- my $QueueAddressOverride = RT->Config->Get('OverrideOutgoingMailFrom')->{$QueueName};
+ my $QueueAddressOverride = $Overrides->{$QueueName};
if ($QueueAddressOverride) {
$OutgoingMailAddress = $QueueAddressOverride;
} else {
- $OutgoingMailAddress = $TicketObj->QueueObj->CorrespondAddress;
+ $OutgoingMailAddress ||= $TicketObj->QueueObj->CorrespondAddress
+ || RT->Config->Get('CorrespondAddress');
}
}
-
- $OutgoingMailAddress ||= RT->Config->Get('OverrideOutgoingMailFrom')->{'Default'};
+ elsif ($Overrides->{'Default'}) {
+ $OutgoingMailAddress = $Overrides->{'Default'};
+ }
push @args, "-f", $OutgoingMailAddress
if $OutgoingMailAddress;
use RT;
use base 'Exporter';
-our @EXPORT = qw(expand_list form_parse form_compose vpush vsplit);
+our @EXPORT = qw(expand_list form_parse form_compose vpush vsplit process_attachments);
sub custom_field_spec {
my $self = shift;
return \@words;
}
+sub process_attachments {
+ my $entity = shift;
+ my @list = @_;
+ return 1 unless @list;
+
+ my $m = $HTML::Mason::Commands::m;
+ my $cgi = $m->cgi_object;
+
+ my $i = 1;
+ foreach my $e ( @list ) {
+
+ my $fh = $cgi->upload("attachment_$i");
+ return (0, "No attachment for $e") unless $fh;
+
+ local $/=undef;
+
+ my $file = $e;
+ $file =~ s#^.*[\\/]##;
+
+ my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 );
+
+ my $buf;
+ while (sysread($fh, $buf, 8192)) {
+ syswrite($tmp_fh, $buf);
+ }
+
+ my $info = $cgi->uploadInfo($fh);
+ my $new_entity = $entity->attach(
+ Path => $tmp_fn,
+ Type => $info->{'Content-Type'} || guess_media_type($tmp_fn),
+ Filename => $file,
+ Disposition => "attachment",
+ );
+ $new_entity->bodyhandle->{'_dirty_hack_to_save_a_ref_tmp_fh'} = $tmp_fh;
+ $i++;
+ }
+ return (1);
+}
+
RT::Base->_ImportOverlays();
1;
$RT::Logger->error("Couldn't make multipart message")
if !$rv || $rv !~ /^(?:DONE|ALREADY)$/;
- foreach ( values %{ $ARGS{'Attachments'} } ) {
+ foreach ( map $ARGS{Attachments}->{$_}, sort keys %{ $ARGS{'Attachments'} } ) {
unless ($_) {
$RT::Logger->error("Couldn't add empty attachemnt");
next;
if ( $args{ARGSRef}->{'UpdateAttachments'} ) {
$Message->make_multipart;
- $Message->add_part($_) foreach values %{ $args{ARGSRef}->{'UpdateAttachments'} };
+ $Message->add_part($_) foreach map $args{ARGSRef}->{UpdateAttachments}{$_},
+ sort keys %{ $args{ARGSRef}->{'UpdateAttachments'} };
}
if ( $args{ARGSRef}->{'AttachTickets'} ) {
while ( my $reminder = $reminder_collection->Next ) {
my $resolve_status = $reminder->QueueObj->Lifecycle->ReminderStatusOnResolve;
if ( $reminder->Status ne $resolve_status && $args->{ 'Complete-Reminder-' . $reminder->id } ) {
- $Ticket->Reminders->Resolve($reminder);
+ my ($status, $msg) = $Ticket->Reminders->Resolve($reminder);
+ push @results, loc("Reminder #[_1]: [_2]", $reminder->id, $msg);
+
}
elsif ( $reminder->Status eq $resolve_status && !$args->{ 'Complete-Reminder-' . $reminder->id } ) {
- $Ticket->Reminders->Open($reminder);
+ my ($status, $msg) = $Ticket->Reminders->Open($reminder);
+ push @results, loc("Reminder #[_1]: [_2]", $reminder->id, $msg);
}
if ( exists( $args->{ 'Reminder-Subject-' . $reminder->id } ) && ( $reminder->Subject ne $args->{ 'Reminder-Subject-' . $reminder->id } )) {
- $reminder->SetSubject( $args->{ 'Reminder-Subject-' . $reminder->id } ) ;
+ my ($status, $msg) = $reminder->SetSubject( $args->{ 'Reminder-Subject-' . $reminder->id } ) ;
+ push @results, loc("Reminder #[_1]: [_2]", $reminder->id, $msg);
}
if ( exists( $args->{ 'Reminder-Owner-' . $reminder->id } ) && ( $reminder->Owner != $args->{ 'Reminder-Owner-' . $reminder->id } )) {
- $reminder->SetOwner( $args->{ 'Reminder-Owner-' . $reminder->id } , "Force" ) ;
+ my ($status, $msg) = $reminder->SetOwner( $args->{ 'Reminder-Owner-' . $reminder->id } , "Force" ) ;
+ push @results, loc("Reminder #[_1]: [_2]", $reminder->id, $msg);
}
if ( exists( $args->{ 'Reminder-Due-' . $reminder->id } ) && $args->{ 'Reminder-Due-' . $reminder->id } ne '' ) {
Value => $args->{ 'Reminder-Due-' . $reminder->id }
);
if ( defined $DateObj->Unix && $DateObj->Unix != $reminder->DueObj->Unix ) {
- $reminder->SetDue( $DateObj->ISO );
+ my ($status, $msg) = $reminder->SetDue( $DateObj->ISO );
+ push @results, loc("Reminder #[_1]: [_2]", $reminder->id, $msg);
}
}
}
}
# complex things
- elsif ( my ( $mainkey, $subkey ) = $args{'Name'} =~ /^(.*?)\.{(.+)}$/ ) {
+ 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';
die "couldn't execute query: ". $dbh->errstr unless defined $rows;
}
- $RT::Logger->info("successfuly deleted $rows sessions");
+ $RT::Logger->info("successfully deleted $rows sessions");
return;
}
next;
}
tied(%session)->delete;
- $RT::Logger->info("successfuly deleted session '$id'");
+ $RT::Logger->info("successfully deleted session '$id'");
}
+ # Apache::Session::Lock::File will clean out locks older than X, but it
+ # leaves around bogus locks if they're too new, even though they're
+ # guaranteed dead. On even just largeish installs, the accumulated number
+ # of them may bump into ext3/4 filesystem limits since Apache::Session
+ # doesn't use a fan-out tree.
my $lock = Apache::Session::Lock::File->new;
$lock->clean( $dir, $older_than );
+ # Take matters into our own hands and clear bogus locks hanging around
+ # regardless of how recent they are.
+ $self->ClearOrphanLockFiles($dir);
+
return;
}
+=head3 ClearOrphanLockFiles
+
+Takes a directory in which to look for L<Apache::Session::Lock::File> locks
+which no longer have a corresponding session file. If not provided, the
+directory is taken from the session configuration data.
+
+=cut
+
+sub ClearOrphanLockFiles {
+ my $class = shift;
+ my $dir = shift || $class->Attributes->{Directory}
+ or return;
+
+ if (opendir my $dh, $dir) {
+ for (readdir $dh) {
+ next unless /^Apache-Session-([0-9a-f]{32})\.lock$/;
+ next if -e "$dir/$1";
+
+ RT->Logger->debug("deleting orphaned session lockfile '$_'");
+
+ unlink "$dir/$_"
+ or warn "Failed to unlink session lockfile $dir/$_: $!";
+ }
+ closedir $dh;
+ } else {
+ warn "Unable to open directory '$dir' for reading: $!";
+ }
+}
+
=head3 ClearByUser
Checks all sessions and if user has more then one session
my $class = $self->Class;
my $attrs = $self->Attributes;
+ my $deleted;
my %seen = ();
foreach my $id( @{ $self->Ids } ) {
my %session;
}
}
tied(%session)->delete;
- $RT::Logger->info("successfuly deleted session '$id'");
+ $RT::Logger->info("successfully deleted session '$id'");
+ $deleted++;
}
+ $self->ClearOrphanLockFiles if $deleted;
}
sub TIEHASH {
eval { tie %session, $class, $id, $attrs };
eval { tie %session, $class, undef, $attrs } if $@;
if ( $@ ) {
- die loc("RT couldn't store your session.") . "\n"
- . loc("This may mean that that the directory '[_1]' isn't writable or a database table is missing or corrupt.",
- $RT::MasonSessionDir)
- . "\n\n"
+ die "RT couldn't store your session. "
+ . "This may mean that that the directory '$RT::MasonSessionDir' isn't writable or a database table is missing or corrupt.\n\n"
. $@;
}
}
my %seen;
- @statuses = grep !$seen{ $_ }++, @statuses;
+ @statuses = grep !$seen{ lc $_ }++, @statuses;
$lifecycle->{''} = \@statuses;
unless ( $lifecycle->{'transitions'}{''} ) {
- $lifecycle->{'transitions'}{''} = [ grep $_ ne 'deleted', @statuses ];
+ $lifecycle->{'transitions'}{''} = [ grep lc $_ ne 'deleted', @statuses ];
}
my @actions;
foreach my $type ( qw(initial active inactive), '' ) {
my %seen;
- @{ $all{ $type } } = grep !$seen{ $_ }++, @{ $all{ $type } };
+ @{ $all{ $type } } = grep !$seen{ lc $_ }++, @{ $all{ $type } };
push @{ $all{''} }, @{ $all{ $type } } if $type;
}
$LIFECYCLES_CACHE{''} = \%all;
use HTML::Entities qw//;
+__PACKAGE__->_accessorize(
+ "batch"
+);
+
sub new {
my $self = shift->SUPER::new(@_);
$self->index(1);
my $self = shift;
my ($name, $section) = @_;
+ $name .= ""; # stringify name, it may be an object
+
$section = defined $section
? '#' . $self->idify($section, 1)
: '';
my $local;
- if ($name =~ /^RT::/) {
+ if ($name =~ /^RT(::(?!Extension::|Authen::)|$)/ or $self->batch->found($name)) {
$local = join "/",
map { $self->encode_entities($_) }
split /::/, $name;
}
- elsif ($name =~ /^rt[-_]/) {
+ elsif ($name =~ /^rt([-_]|$)/) {
$local = $self->encode_entities($name);
}
- elsif ($name eq "RT_Config" or $name eq "RT_Config.pm") {
- $local = "RT_Config";
+ elsif ($name =~ /^(\w+)_Config(\.pm)?$/) {
+ $name = "$1_Config";
+ $local = "$1_Config";
}
# These matches handle links that look like filenames, such as those we
# parse out of F<> tags.
elsif ( $name =~ m{^(?:lib/)(RT/[\w/]+?)\.pm$}
or $name =~ m{^(?:docs/)(.+?)\.pod$})
{
+ $name = join "::", split '/', $1;
$local = join "/",
map { $self->encode_entities($_) }
split /\//, $1;
if ($local) {
# Resolve links correctly by going up
- my $depth = $self->batch_mode_current_level - 1;
- return ($depth ? "../" x $depth : "") . "$local.html$section";
+ my $found = $self->batch->found($name);
+ my $depth = $self->batch_mode_current_level
+ + ($found ? -1 : 1);
+ return ($depth ? "../" x $depth : "") . ($found ? "" : "rt/latest/") . "$local.html$section";
} else {
return;
}
}
+sub batch_mode_page_object_init {
+ my ($self, $batch, $module, $infile, $outfile, $depth) = @_;
+ $self->SUPER::batch_mode_page_object_init(@_[1..$#_]);
+ $self->batch( $batch );
+ return $self;
+}
+
1;
use RT::Pod::Search;
use RT::Pod::HTML;
+my $MOD2PATH;
+
sub new {
my $self = shift->SUPER::new(@_);
$self->verbose(0);
my %page = @_;
local $_ = $page{name};
return 1 if /^(README|UPGRADING)/;
- return 1 if $_ eq "RT_Config";
+ return 1 if /^RT\w*?_Config$/;
return 1 if $_ eq "web_deployment";
return 1 if $page{infile} =~ m{^configure(\.ac)?$};
return 0;
Pod::Simple::HTMLBatch::esc(@_);
}
+sub find_all_pods {
+ my $self = shift;
+ $MOD2PATH = $self->SUPER::find_all_pods(@_);
+ return $MOD2PATH;
+}
+
+sub found {
+ my ($self, $module) = @_;
+ return(exists $MOD2PATH->{$module} and defined $MOD2PATH->{$module});
+}
+
1;
sub _Init {
my $self = shift;
+ $self->{'with_disabled_column'} = 1;
return ( $self->SUPER::_Init(@_) );
}
. length($Body));
$RT::Logger->info( "It started: " . substr( $Body, 0, 60 ) );
$Filename .= ".txt" if $Filename;
- return ("none", "Large attachment dropped", "plain/text", $Filename );
+ return ("none", "Large attachment dropped", "text/plain", $Filename );
}
}
if ( $args{'Base'} and $args{'Target'} ) {
$RT::Logger->debug( "$self tried to create a link. both base and target were specified" );
- return ( 0, $self->loc("Can't specifiy both base and target") );
+ return ( 0, $self->loc("Can't specify both base and target") );
}
elsif ( $args{'Base'} ) {
$args{'Target'} = $self->URI();
if ( $args{'Base'} and $args{'Target'} ) {
$RT::Logger->debug("$self ->_DeleteLink. got both Base and Target");
- return ( 0, $self->loc("Can't specifiy both base and target") );
+ return ( 0, $self->loc("Can't specify both base and target") );
}
elsif ( $args{'Base'} ) {
$args{'Target'} = $self->URI();
sub CustomFieldLookupType {
my $self = shift;
- return ref($self);
+ return ref($self) || $self;
}
my $is_the_same = 1;
if ( defined $args{'Value'} ) {
$is_the_same = 0 unless defined $old_content
- && lc $old_content eq lc $args{'Value'};
+ && $old_content eq $args{'Value'};
} else {
$is_the_same = 0 if defined $old_content;
}
return ( 0, $self->loc( "Failed to load ticket [_1]", $self->Ticket ) );
}
- if ( $ticket->Status eq 'deleted' ) {
+ if ( lc $ticket->Status eq 'deleted' ) {
return ( 0, $self->loc("Can't link to a deleted ticket") );
}
RefersTo => $self->Ticket,
Type => 'reminder',
Queue => $self->TicketObj->Queue,
+ Status => $self->TicketObj->QueueObj->Lifecycle->ReminderStatusOnOpen,
);
$self->TicketObj->_NewTransaction(
Type => 'AddReminder',
sub Label {
my $self = shift;
my $field = shift;
- if ( $field =~ /^(?:CF|CustomField)\.{(.*)}$/ ) {
+ if ( $field =~ /^(?:CF|CustomField)\.\{(.*)\}$/ ) {
my $cf = $1;
return $self->CurrentUser->loc( "Custom field '[_1]'", $cf ) if $cf =~ /\D/;
my $obj = RT::CustomField->new( $self->CurrentUser );
$func = "SUBSTR($func,1,4)";
}
$args{'FUNCTION'} = $func;
- } elsif ( $field =~ /^(?:CF|CustomField)\.{(.*)}$/ ) { #XXX: use CFDecipher method
+ } elsif ( $field =~ /^(?:CF|CustomField)\.\{(.*)\}$/ ) { #XXX: use CFDecipher method
my $cf_name = $1;
my $cf = RT::CustomField->new( $self->CurrentUser );
$cf->Load($cf_name);
[ 10 => sub { return "subject" if $_[1] } ],
[ 20 => sub { return "id" if /^#?\d+$/ } ],
[ 30 => sub { return "requestor" if /\w+@\w+/} ],
+ [ 35 => sub { return "domain" if /^@\w+/} ],
[ 40 => sub {
return "status" if RT::Queue->new( $_[2] )->IsValidStatus( $_ )
}],
return watcher => (!$_[2] and $_[1] eq "me") ? "Watcher.id = '__CurrentUser__'" : "Watcher = '$_[1]'";
}
sub HandleRequestor { return requestor => "Requestor STARTSWITH '$_[1]'"; }
+sub HandleDomain { $_[1] =~ s/^@?/@/; return requestor => "Requestor ENDSWITH '$_[1]'"; }
sub HandleQueue { return queue => "Queue = '$_[1]'"; }
sub HandleQ { return queue => "Queue = '$_[1]'"; }
sub HandleCf { return "cf.$_[3]" => "'CF.{$_[3]}' LIKE '$_[1]'"; }
$self->SUPER::_Init( 'Handle' => $RT::Handle);
}
+sub _Handle { return $RT::Handle }
+
sub CleanSlate {
my $self = shift;
$self->{'_sql_aliases'} = {};
+ delete $self->{'handled_disabled_column'};
+ delete $self->{'find_disabled_rows'};
return $self->SUPER::CleanSlate(@_);
}
}
}
- if ( $obj && $obj->Status eq 'deleted' ) {
+ if ( $obj && lc $obj->Status eq 'deleted' ) {
push @non_fatal_errors,
$self->loc("Linking. Can't link to a deleted ticket");
next;
}
return ( 0, "Can't link to a deleted ticket" )
- if $other_ticket && $other_ticket->Status eq 'deleted';
+ if $other_ticket && lc $other_ticket->Status eq 'deleted';
return $self->_AddLink(%args);
}
QueueCc => [ 'WATCHERFIELD' => 'Cc' => 'Queue', ], #loc_left_pair
QueueAdminCc => [ 'WATCHERFIELD' => 'AdminCc' => 'Queue', ], #loc_left_pair
QueueWatcher => [ 'WATCHERFIELD' => undef => 'Queue', ], #loc_left_pair
- CustomFieldValue => [ 'CUSTOMFIELD', ], #loc_left_pair
- CustomField => [ 'CUSTOMFIELD', ], #loc_left_pair
- CF => [ 'CUSTOMFIELD', ], #loc_left_pair
+ CustomFieldValue => [ 'CUSTOMFIELD' => 'Ticket' ], #loc_left_pair
+ CustomField => [ 'CUSTOMFIELD' => 'Ticket' ], #loc_left_pair
+ CF => [ 'CUSTOMFIELD' => 'Ticket' ], #loc_left_pair
Updated => [ 'TRANSDATE', ], #loc_left_pair
RequestorGroup => [ 'MEMBERSHIPFIELD' => 'Requestor', ], #loc_left_pair
CCGroup => [ 'MEMBERSHIPFIELD' => 'Cc', ], #loc_left_pair
}
$rest{SUBKEY} ||= 'EmailAddress';
- my $groups = $self->_RoleGroupsJoin( Type => $type, Class => $class, New => !$type );
+ my ($groups, $group_members, $users);
+ if ( $rest{'BUNDLE'} ) {
+ ($groups, $group_members, $users) = @{ $rest{'BUNDLE'} };
+ } else {
+ $groups = $self->_RoleGroupsJoin( Type => $type, Class => $class, New => !$type );
+ }
$self->_OpenParen;
if ( $op =~ /^IS(?: NOT)?$/i ) {
# is [not] empty case
- my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
+ $group_members ||= $self->_GroupMembersJoin( GroupsAlias => $groups );
# to avoid joining the table Users into the query, we just join GM
# and make sure we don't match records where group is member of itself
$self->SUPER::Limit(
$users_obj->RowsPerPage(2);
my @users = @{ $users_obj->ItemsArrayRef };
- my $group_members = $self->_GroupMembersJoin( GroupsAlias => $groups );
+ $group_members ||= $self->_GroupMembersJoin( GroupsAlias => $groups );
if ( @users <= 1 ) {
my $uid = 0;
$uid = $users[0]->id if @users;
VALUE => "$group_members.MemberId",
QUOTEVALUE => 0,
);
- my $users = $self->Join(
+ $users ||= $self->Join(
TYPE => 'LEFT',
ALIAS1 => $group_members,
FIELD1 => 'MemberId',
} else {
# positive condition case
- my $group_members = $self->_GroupMembersJoin(
+ $group_members ||= $self->_GroupMembersJoin(
GroupsAlias => $groups, New => 1, Left => 0
);
- my $users = $self->Join(
+ $users ||= $self->Join(
TYPE => 'LEFT',
ALIAS1 => $group_members,
FIELD1 => 'MemberId',
);
}
$self->_CloseParen;
+ return ($groups, $group_members, $users);
}
sub _RoleGroupsJoin {
Try and turn a CF descriptor into (cfid, cfname) object pair.
+Takes an optional second parameter of the CF LookupType, defaults to Ticket CFs.
+
=cut
sub _CustomFieldDecipher {
- my ($self, $string) = @_;
+ my ($self, $string, $lookuptype) = @_;
+ $lookuptype ||= $self->_SingularClass->CustomFieldLookupType;
- my ($queue, $field, $column) = ($string =~ /^(?:(.+?)\.)?{(.+)}(?:\.(Content|LargeContent))?$/);
+ my ($object, $field, $column) = ($string =~ /^(?:(.+?)\.)?\{(.+)\}(?:\.(Content|LargeContent))?$/);
$field ||= ($string =~ /^{(.*?)}$/)[0] || $string;
- my $cf;
- if ( $queue ) {
- my $q = RT::Queue->new( $self->CurrentUser );
- $q->Load( $queue );
+ my ($cf, $applied_to);
+
+ if ( $object ) {
+ my $record_class = RT::CustomField->RecordClassFromLookupType($lookuptype);
+ $applied_to = $record_class->new( $self->CurrentUser );
+ $applied_to->Load( $object );
- if ( $q->id ) {
- # $queue = $q->Name; # should we normalize the queue?
- $cf = $q->CustomField( $field );
+ if ( $applied_to->id ) {
+ RT->Logger->debug("Limiting to CFs identified by '$field' applied to $record_class #@{[$applied_to->id]} (loaded via '$object')");
}
else {
- $RT::Logger->warning("Queue '$queue' doesn't exist, parsed from '$string'");
- $queue = 0;
+ RT->Logger->warning("$record_class '$object' doesn't exist, parsed from '$string'");
+ $object = 0;
+ undef $applied_to;
}
}
- elsif ( $field =~ /\D/ ) {
- $queue = '';
+
+ if ( $field =~ /\D/ ) {
+ $object ||= '';
my $cfs = RT::CustomFields->new( $self->CurrentUser );
- $cfs->Limit( FIELD => 'Name', VALUE => $field );
- $cfs->LimitToLookupType('RT::Queue-RT::Ticket');
+ $cfs->Limit( FIELD => 'Name', VALUE => $field, ($applied_to ? (CASESENSITIVE => 0) : ()) );
+ $cfs->LimitToLookupType($lookuptype);
+
+ if ($applied_to) {
+ $cfs->SetContextObject($applied_to);
+ $cfs->LimitToObjectId($applied_to->id);
+ }
# if there is more then one field the current user can
# see with the same name then we shouldn't return cf object
else {
$cf = RT::CustomField->new( $self->CurrentUser );
$cf->Load( $field );
+ $cf->SetContextObject($applied_to)
+ if $cf->id and $applied_to;
}
- return ($queue, $field, $cf, $column);
+ return ($object, $field, $cf, $column);
}
=head2 _CustomFieldJoin
=cut
+our %JOIN_ALIAS_FOR_LOOKUP_TYPE = (
+ RT::Ticket->CustomFieldLookupType => sub { "main" },
+);
+
sub _CustomFieldJoin {
- my ($self, $cfkey, $cfid, $field) = @_;
+ my ($self, $cfkey, $cfid, $field, $type) = @_;
+ $type ||= RT::Ticket->CustomFieldLookupType;
+
# Perform one Join per CustomField
if ( $self->{_sql_object_cfv_alias}{$cfkey} ||
$self->{_sql_cf_alias}{$cfkey} )
$self->{_sql_cf_alias}{$cfkey} );
}
- my ($TicketCFs, $CFs);
+ my $ObjectAlias = $JOIN_ALIAS_FOR_LOOKUP_TYPE{$type}
+ ? $JOIN_ALIAS_FOR_LOOKUP_TYPE{$type}->($self)
+ : die "We don't know how to join on $type";
+
+ my ($ObjectCFs, $CFs);
if ( $cfid ) {
- $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
+ $ObjectCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
TYPE => 'LEFT',
- ALIAS1 => 'main',
+ ALIAS1 => $ObjectAlias,
FIELD1 => 'id',
TABLE2 => 'ObjectCustomFieldValues',
FIELD2 => 'ObjectId',
);
$self->SUPER::Limit(
- LEFTJOIN => $TicketCFs,
+ LEFTJOIN => $ObjectCFs,
FIELD => 'CustomField',
VALUE => $cfid,
ENTRYAGGREGATOR => 'AND'
LEFTJOIN => $CFs,
ENTRYAGGREGATOR => 'AND',
FIELD => 'LookupType',
- VALUE => 'RT::Queue-RT::Ticket',
+ VALUE => $type,
);
$self->SUPER::Limit(
LEFTJOIN => $CFs,
VALUE => $field,
);
- $TicketCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
+ $ObjectCFs = $self->{_sql_object_cfv_alias}{$cfkey} = $self->Join(
TYPE => 'LEFT',
ALIAS1 => $CFs,
FIELD1 => 'id',
FIELD2 => 'CustomField',
);
$self->SUPER::Limit(
- LEFTJOIN => $TicketCFs,
+ LEFTJOIN => $ObjectCFs,
FIELD => 'ObjectId',
- VALUE => 'main.id',
+ VALUE => "$ObjectAlias.id",
QUOTEVALUE => 0,
ENTRYAGGREGATOR => 'AND',
);
}
+
$self->SUPER::Limit(
- LEFTJOIN => $TicketCFs,
+ LEFTJOIN => $ObjectCFs,
FIELD => 'ObjectType',
- VALUE => 'RT::Ticket',
+ VALUE => RT::CustomField->ObjectTypeFromLookupType($type),
ENTRYAGGREGATOR => 'AND'
);
$self->SUPER::Limit(
- LEFTJOIN => $TicketCFs,
+ LEFTJOIN => $ObjectCFs,
FIELD => 'Disabled',
OPERATOR => '=',
VALUE => '0',
ENTRYAGGREGATOR => 'AND'
);
- return ($TicketCFs, $CFs);
+ return ($ObjectCFs, $CFs);
}
=head2 _CustomFieldLimit
sub _CustomFieldLimit {
my ( $self, $_field, $op, $value, %rest ) = @_;
+ my $meta = $FIELD_METADATA{ $_field };
+ my $class = $meta->[1] || 'Ticket';
+ my $type = "RT::$class"->CustomFieldLookupType;
+
my $field = $rest{'SUBKEY'} || die "No field specified";
# For our sanity, we can only limit on one queue at a time
- my ($queue, $cfid, $cf, $column);
- ($queue, $field, $cf, $column) = $self->_CustomFieldDecipher( $field );
+ my ($object, $cfid, $cf, $column);
+ ($object, $field, $cf, $column) = $self->_CustomFieldDecipher( $field, $type );
$cfid = $cf ? $cf->id : 0 ;
# If we're trying to find custom fields that don't match something, we
my $single_value = !$cf || !$cfid || $cf->SingleValue;
- my $cfkey = $cfid ? $cfid : "$queue.$field";
+ my $cfkey = $cfid ? $cfid : "$type-$object.$field";
if ( $null_op && !$column ) {
# IS[ NOT] NULL without column is the same as has[ no] any CF value,
# we can reuse our default joins for this operation
# with column specified we have different situation
- my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
+ my ($ObjectCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field, $type );
$self->_OpenParen;
$self->_SQLLimit(
- ALIAS => $TicketCFs,
+ ALIAS => $ObjectCFs,
FIELD => 'id',
OPERATOR => $op,
VALUE => $value,
$self->_OpenParen;
if ( $op !~ /NOT|!=|<>/i ) { # positive equation
$self->_CustomFieldLimit(
- 'CF', '<=', $end_ip, %rest,
+ $_field, '<=', $end_ip, %rest,
SUBKEY => $rest{'SUBKEY'}. '.Content',
);
$self->_CustomFieldLimit(
- 'CF', '>=', $start_ip, %rest,
+ $_field, '>=', $start_ip, %rest,
SUBKEY => $rest{'SUBKEY'}. '.LargeContent',
ENTRYAGGREGATOR => 'AND',
);
# estimations and scan less rows
# have to disable this tweak because of ipv6
# $self->_CustomFieldLimit(
-# $field, '>=', '000.000.000.000', %rest,
+# $_field, '>=', '000.000.000.000', %rest,
# SUBKEY => $rest{'SUBKEY'}. '.Content',
# ENTRYAGGREGATOR => 'AND',
# );
# $self->_CustomFieldLimit(
-# $field, '<=', '255.255.255.255', %rest,
+# $_field, '<=', '255.255.255.255', %rest,
# SUBKEY => $rest{'SUBKEY'}. '.LargeContent',
# ENTRYAGGREGATOR => 'AND',
# );
}
else { # negative equation
- $self->_CustomFieldLimit($field, '>', $end_ip, %rest);
+ $self->_CustomFieldLimit($_field, '>', $end_ip, %rest);
$self->_CustomFieldLimit(
- $field, '<', $start_ip, %rest,
+ $_field, '<', $start_ip, %rest,
SUBKEY => $rest{'SUBKEY'}. '.LargeContent',
ENTRYAGGREGATOR => 'OR',
);
}
elsif ( !$negative_op || $single_value ) {
$cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++ if !$single_value && !$range_op;
- my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
+ my ($ObjectCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field, $type );
$self->_OpenParen;
# otherwise search in Content and in LargeContent
if ( $column ) {
$self->_SQLLimit( $fix_op->(
- ALIAS => $TicketCFs,
+ ALIAS => $ObjectCFs,
FIELD => $column,
OPERATOR => $op,
VALUE => $value,
$self->_OpenParen;
$self->_SQLLimit(
- ALIAS => $TicketCFs,
+ ALIAS => $ObjectCFs,
FIELD => 'Content',
OPERATOR => ">=",
VALUE => $daystart,
);
$self->_SQLLimit(
- ALIAS => $TicketCFs,
+ ALIAS => $ObjectCFs,
FIELD => 'Content',
OPERATOR => "<",
VALUE => $dayend,
elsif ( $op eq '=' || $op eq '!=' || $op eq '<>' ) {
if ( length( Encode::encode_utf8($value) ) < 256 ) {
$self->_SQLLimit(
- ALIAS => $TicketCFs,
+ ALIAS => $ObjectCFs,
FIELD => 'Content',
OPERATOR => $op,
VALUE => $value,
else {
$self->_OpenParen;
$self->_SQLLimit(
- ALIAS => $TicketCFs,
+ ALIAS => $ObjectCFs,
FIELD => 'Content',
OPERATOR => '=',
VALUE => '',
ENTRYAGGREGATOR => 'OR'
);
$self->_SQLLimit(
- ALIAS => $TicketCFs,
+ ALIAS => $ObjectCFs,
FIELD => 'Content',
OPERATOR => 'IS',
VALUE => 'NULL',
);
$self->_CloseParen;
$self->_SQLLimit( $fix_op->(
- ALIAS => $TicketCFs,
+ ALIAS => $ObjectCFs,
FIELD => 'LargeContent',
OPERATOR => $op,
VALUE => $value,
}
else {
$self->_SQLLimit(
- ALIAS => $TicketCFs,
+ ALIAS => $ObjectCFs,
FIELD => 'Content',
OPERATOR => $op,
VALUE => $value,
$self->_OpenParen;
$self->_OpenParen;
$self->_SQLLimit(
- ALIAS => $TicketCFs,
+ ALIAS => $ObjectCFs,
FIELD => 'Content',
OPERATOR => '=',
VALUE => '',
ENTRYAGGREGATOR => 'OR'
);
$self->_SQLLimit(
- ALIAS => $TicketCFs,
+ ALIAS => $ObjectCFs,
FIELD => 'Content',
OPERATOR => 'IS',
VALUE => 'NULL',
);
$self->_CloseParen;
$self->_SQLLimit( $fix_op->(
- ALIAS => $TicketCFs,
+ ALIAS => $ObjectCFs,
FIELD => 'LargeContent',
OPERATOR => $op,
VALUE => $value,
if ($negative_op) {
$self->_SQLLimit(
- ALIAS => $TicketCFs,
+ ALIAS => $ObjectCFs,
FIELD => $column || 'Content',
OPERATOR => 'IS',
VALUE => 'NULL',
}
else {
$cfkey .= '.'. $self->{'_sql_multiple_cfs_index'}++;
- my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field );
+ my ($ObjectCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, $cfid, $field, $type );
# reverse operation
$op =~ s/!|NOT\s+//i;
# otherwise search in Content and in LargeContent
if ( $column ) {
$self->SUPER::Limit( $fix_op->(
- LEFTJOIN => $TicketCFs,
- ALIAS => $TicketCFs,
+ LEFTJOIN => $ObjectCFs,
+ ALIAS => $ObjectCFs,
FIELD => $column,
OPERATOR => $op,
VALUE => $value,
}
else {
$self->SUPER::Limit(
- LEFTJOIN => $TicketCFs,
- ALIAS => $TicketCFs,
+ LEFTJOIN => $ObjectCFs,
+ ALIAS => $ObjectCFs,
FIELD => 'Content',
OPERATOR => $op,
VALUE => $value,
}
$self->_SQLLimit(
%rest,
- ALIAS => $TicketCFs,
+ ALIAS => $ObjectCFs,
FIELD => 'id',
OPERATOR => 'IS',
VALUE => 'NULL',
}
push @res, { %$row, ALIAS => $users, FIELD => $subkey };
} elsif ( defined $meta->[0] && $meta->[0] eq 'CUSTOMFIELD' ) {
- my ($queue, $field, $cf_obj, $column) = $self->_CustomFieldDecipher( $subkey );
- my $cfkey = $cf_obj ? $cf_obj->id : "$queue.$field";
+ my ($object, $field, $cf_obj, $column) = $self->_CustomFieldDecipher( $subkey );
+ my $cfkey = $cf_obj ? $cf_obj->id : "$object.$field";
$cfkey .= ".ordering" if !$cf_obj || ($cf_obj->MaxValues||0) != 1;
- my ($TicketCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, ($cf_obj ?$cf_obj->id :0) , $field );
+ my ($ObjectCFs, $CFs) = $self->_CustomFieldJoin( $cfkey, ($cf_obj ?$cf_obj->id :0) , $field );
# this is described in _CustomFieldLimit
$self->_SQLLimit(
ALIAS => $CFs,
}
my $CFvs = $self->Join(
TYPE => 'LEFT',
- ALIAS1 => $TicketCFs,
+ ALIAS1 => $ObjectCFs,
FIELD1 => 'CustomField',
TABLE2 => 'CustomFieldValues',
FIELD2 => 'CustomField',
LEFTJOIN => $CFvs,
FIELD => 'Name',
QUOTEVALUE => 0,
- VALUE => $TicketCFs . ".Content",
+ VALUE => $ObjectCFs . ".Content",
ENTRYAGGREGATOR => 'AND'
);
push @res, { %$row, ALIAS => $CFvs, FIELD => 'SortOrder' };
- push @res, { %$row, ALIAS => $TicketCFs, FIELD => 'Content' };
+ push @res, { %$row, ALIAS => $ObjectCFs, FIELD => 'Content' };
} elsif ( $field eq "Custom" && $subkey eq "Ownership") {
# PAW logic is "reversed"
my $order = "ASC";
my @bundle;
my $ea = '';
+ # Bundling of joins is implemented by dynamically tracking a parallel query
+ # tree in %sub_tree as the TicketSQL is parsed. Don't be fooled by
+ # _close_bundle(), @bundle, and %can_bundle; they are completely unused for
+ # quite a long time and removed in RT 4.2. For now they stay, a useless
+ # relic.
+ #
+ # Only positive, OR'd watcher conditions are bundled currently. Each key
+ # in %sub_tree is a watcher type (Requestor, Cc, AdminCc) or the generic
+ # "Watcher" for any watcher type. Owner is not bundled because it is
+ # denormalized into a Tickets column and doesn't need a join. AND'd
+ # conditions are not bundled since a record may have multiple watchers
+ # which independently match the conditions, thus necessitating two joins.
+ #
+ # The values of %sub_tree are arrayrefs made up of:
+ #
+ # * Open parentheses "(" pushed on by the OpenParen callback
+ # * Arrayrefs of bundled join aliases pushed on by the Condition callback
+ # * Entry aggregators (AND/OR) pushed on by the EntryAggregator callback
+ #
+ # The CloseParen callback takes care of backing off the query trees until
+ # outside of the just-closed parenthetical, thus restoring the tree state
+ # an equivalent of before the parenthetical was entered.
+ #
+ # The Condition callback handles starting a new subtree or extending an
+ # existing one, determining if bundling the current condition with any
+ # subtree is possible, and pruning any dangling entry aggregators from
+ # trees.
+ #
+
+ my %sub_tree;
+ my $depth = 0;
+
my %callback;
$callback{'OpenParen'} = sub {
$self->_close_bundle(@bundle); @bundle = ();
- $self->_OpenParen
+ $self->_OpenParen;
+ $depth++;
+ push @$_, '(' foreach values %sub_tree;
};
$callback{'CloseParen'} = sub {
$self->_close_bundle(@bundle); @bundle = ();
$self->_CloseParen;
+ $depth--;
+ foreach my $list ( values %sub_tree ) {
+ if ( $list->[-1] eq '(' ) {
+ pop @$list;
+ pop @$list if $list->[-1] =~ /^(?:AND|OR)$/i;
+ }
+ else {
+ pop @$list while $list->[-2] ne '(';
+ $list->[-1] = pop @$list;
+ }
+ }
+ };
+ $callback{'EntryAggregator'} = sub {
+ $ea = $_[0] || '';
+ push @$_, $ea foreach grep @$_ && $_->[-1] ne '(', values %sub_tree;
};
- $callback{'EntryAggregator'} = sub { $ea = $_[0] || '' };
$callback{'Condition'} = sub {
my ($key, $op, $value) = @_;
+ my ($negative_op, $null_op, $inv_op, $range_op)
+ = $self->ClassifySQLOperation( $op );
# key has dot then it's compound variant and we have subkey
my $subkey = '';
($key, $subkey) = ($1, $2) if $key =~ /^([^\.]+)\.(.+)$/;
}
else {
$self->_close_bundle(@bundle); @bundle = ();
- $sub->( $self, $key, $op, $value,
+ my @res; my $bundle_with;
+ if ( $class eq 'WATCHERFIELD' && $key ne 'Owner' && !$negative_op && (!$null_op || $subkey) ) {
+ if ( !$sub_tree{$key} ) {
+ $sub_tree{$key} = [ ('(')x$depth, \@res ];
+ } else {
+ $bundle_with = $self->_check_bundling_possibility( $string, @{ $sub_tree{$key} } );
+ if ( $sub_tree{$key}[-1] eq '(' ) {
+ push @{ $sub_tree{$key} }, \@res;
+ }
+ }
+ }
+
+ # Remove our aggregator from subtrees where our condition didn't get added
+ pop @$_ foreach grep @$_ && $_->[-1] =~ /^(?:AND|OR)$/i, values %sub_tree;
+
+ # A reference to @res may be pushed onto $sub_tree{$key} from
+ # above, and we fill it here.
+ @res = $sub->( $self, $key, $op, $value,
SUBCLAUSE => '', # don't need anymore
ENTRYAGGREGATOR => $ea,
SUBKEY => $subkey,
+ BUNDLE => $bundle_with,
);
}
$self->{_sql_looking_at}{lc $key} = 1;
$self->_close_bundle(@bundle); @bundle = ();
}
+sub _check_bundling_possibility {
+ my $self = shift;
+ my $string = shift;
+ my @list = reverse @_;
+ while (my $e = shift @list) {
+ next if $e eq '(';
+ if ( lc($e) eq 'and' ) {
+ return undef;
+ }
+ elsif ( lc($e) eq 'or' ) {
+ return shift @list;
+ }
+ else {
+ # should not happen
+ $RT::Logger->error(
+ "Joins optimization failed when parsing '$string'. It's bug in RT, contact Best Practical"
+ );
+ die "Internal error. Contact your system administrator.";
+ }
+ }
+ return undef;
+}
+
=head2 ClausesToSQL
=cut
$self->{_sql_query} = $query;
eval { $self->_parser( $query ); };
if ( $@ ) {
- $RT::Logger->error( $@ );
- return (0, $@);
+ my $error = "$@";
+ $RT::Logger->error("Couldn't parse query: $error");
+ return (0, $error);
}
# We only want to look at EffectiveId's (mostly) for these searches.
}
if ( $args{'Quote'} ) {
+ $content = $self->ApplyQuoteWrap(content => $content,
+ cols => $args{'Wrap'} );
- # What's the longest line like?
- my $max = 0;
- foreach ( split ( /\n/, $content ) ) {
- $max = length if length > $max;
- }
-
- if ( $max > 76 ) {
- require Text::Wrapper;
- my $wrapper = Text::Wrapper->new(
- columns => $args{'Wrap'},
- body_start => ( $max > 70 * 3 ? ' ' : '' ),
- par_start => ''
- );
- $content = $wrapper->wrap($content);
- }
-
- $content =~ s/^/> /gm;
$content = $self->QuoteHeader . "\n$content\n\n";
}
return $self->loc("On [_1], [_2] wrote:", $self->CreatedAsString, $self->CreatorObj->Name);
}
+=head2 ApplyQuoteWrap PARAMHASH
+
+Wrapper to calculate wrap criteria and apply quote wrapping if needed.
+
+=cut
+
+sub ApplyQuoteWrap {
+ my $self = shift;
+ my %args = @_;
+ my $content = $args{content};
+
+ # What's the longest line like?
+ my $max = 0;
+ foreach ( split ( /\n/, $args{content} ) ) {
+ $max = length if length > $max;
+ }
+
+ if ( $max > 76 ) {
+ require Text::Quoted;
+ require Text::Wrapper;
+
+ my $structure = Text::Quoted::extract($args{content});
+ $content = $self->QuoteWrap(content_ref => $structure,
+ cols => $args{cols},
+ max => $max );
+ }
+
+ $content =~ s/^/> /gm; # use regex since string might be multi-line
+ return $content;
+}
+
+=head2 QuoteWrap PARAMHASH
+
+Wrap the contents of transactions based on Wrap settings, maintaining
+the quote character from the original.
+
+=cut
+
+sub QuoteWrap {
+ my $self = shift;
+ my %args = @_;
+ my $ref = $args{content_ref};
+ my $final_string;
+
+ if ( ref $ref eq 'ARRAY' ){
+ foreach my $array (@$ref){
+ $final_string .= $self->QuoteWrap(content_ref => $array,
+ cols => $args{cols},
+ max => $args{max} );
+ }
+ }
+ elsif ( ref $ref eq 'HASH' ){
+ return $ref->{quoter} . "\n" if $ref->{empty}; # Blank line
+
+ my $col = $args{cols} - (length $ref->{quoter});
+ my $wrapper = Text::Wrapper->new( columns => $col );
+
+ # Wrap on individual lines to honor incoming line breaks
+ # Otherwise deliberate separate lines (like a list or a sig)
+ # all get combined incorrectly into single paragraphs.
+
+ my @lines = split /\n/, $ref->{text};
+ my $wrap = join '', map { $wrapper->wrap($_) } @lines;
+ my $quoter = $ref->{quoter};
+
+ # Only add the space if actually quoting
+ $quoter .= ' ' if length $quoter;
+ $wrap =~ s/^/$quoter/mg; # use regex since string might be multi-line
+
+ return $wrap;
+ }
+ else{
+ $RT::Logger->warning("Can't apply quoting with $ref");
+ return;
+ }
+ return $final_string;
+}
+
=head2 Addresses
if ($resolver) {
$self->{'resolver'} = $resolver;
} else {
+ RT->Logger->warning("Failed to create new resolver object for scheme '$scheme': $@")
+ if $@ !~ m{Can't locate RT/URI/\Q$scheme\E};
$self->{'resolver'} = RT::URI::base->new($self->CurrentUser);
}
# values. Instead we set values, eval code, check pid
# on failure and reset values only in our original
# process
+ my ($oldv_dbh, $oldv_rth);
my $dbh = $RT::Handle->dbh;
+ $oldv_dbh = $dbh->{'InactiveDestroy'} if $dbh;
$dbh->{'InactiveDestroy'} = 1 if $dbh;
+ $oldv_rth = $RT::Handle->{'DisconnectHandleOnDestroy'};
$RT::Handle->{'DisconnectHandleOnDestroy'} = 0;
my ($reader, $writer);
my $err = $@;
$err =~ s/^Stack:.*$//ms;
if ( $our_pid == $$ ) {
- $dbh->{'InactiveDestroy'} = 0 if $dbh;
- $RT::Handle->{'DisconnectHandleOnDestroy'} = 1;
+ $dbh->{'InactiveDestroy'} = $oldv_dbh if $dbh;
+ $RT::Handle->{'DisconnectHandleOnDestroy'} = $oldv_rth;
die "System Error: $err";
} else {
print $writer "System Error: $err";
my ($response) = $reader->getline;
warn $response if $response;
- $dbh->{'InactiveDestroy'} = 0 if $dbh;
- $RT::Handle->{'DisconnectHandleOnDestroy'} = 1;
+ $dbh->{'InactiveDestroy'} = $oldv_dbh if $dbh;
+ $RT::Handle->{'DisconnectHandleOnDestroy'} = $oldv_rth;
return $want? (@res) : $res[0];
}
print "\t-p, --print\t"
. loc("Print the resulting digest messages to STDOUT; don't mail them. Do not mark them as sent")
. "\n";
+ print "\t-v, --verbose\t" . loc("Give output even on messages successfully sent") . "\n";
print "\t-h, --help\t" . loc("Print this message") . "\n";
if ( $error eq 'help' ) {
}
}
-my ( $frequency, $print, $help ) = ( '', '', '' );
+my ( $frequency, $print, $verbose, $help ) = ( '', '', '', '' );
GetOptions(
'mode=s' => \$frequency,
'print' => \$print,
+ 'verbose' => \$verbose,
'help' => \$help,
);
my ( $contents_list, $contents_body ) = build_digest_for_user( $user, $all_digest->{$user} );
# Now we have a content head and a content body. We can send a message.
if ( send_digest( $user, $contents_list, $contents_body ) ) {
- print "Sent message to $user\n";
+ print "Sent message to $user\n" if $verbose;
mark_transactions_sent( $frequency, $user, values %{$sent_transactions->{$user}} ) unless ($print);
} else {
print "Failed to send message to $user\n";
my $status = eval { $dbh->do( $query, undef, $$text, $attachment->id ) };
unless ( $status ) {
if ( $dbh->err == 7 && $dbh->state eq '54000' ) {
- warn "Attachment @{[$attachment->id]} cannot be indexed, as it contains too many unique words";
+ warn "Attachment @{[$attachment->id]} cannot be indexed. Most probably it contains too many unique words. Error: ". $dbh->errstr;
} elsif ( $dbh->err == 7 && $dbh->state eq '22021' ) {
- warn "Attachment @{[$attachment->id]} cannot be indexed, as it contains invalid UTF8 bytes";
+ warn "Attachment @{[$attachment->id]} cannot be indexed. Most probably it contains invalid UTF8 bytes. Error: ". $dbh->errstr;
} else {
die "error: ". $dbh->errstr;
}
my %args = (
dba => 'postgres',
+ package => 'RT',
);
GetOptions(
\%args,
'action=s',
'force', 'debug',
- 'dba=s', 'dba-password=s', 'prompt-for-dba-password',
+ 'dba=s', 'dba-password=s', 'prompt-for-dba-password', 'package=s',
'datafile=s', 'datadir=s', 'skip-create', 'root-password-file=s',
'help|h',
);
my $db_type = RT->Config->Get('DatabaseType') || '';
my $db_host = RT->Config->Get('DatabaseHost') || '';
+my $db_port = RT->Config->Get('DatabasePort') || '';
my $db_name = RT->Config->Get('DatabaseName') || '';
my $db_user = RT->Config->Get('DatabaseUser') || '';
my $db_pass = RT->Config->Get('DatabasePassword') || '';
}
}
+my $version_word_regex = join '|', RT::Handle->version_words;
+my $version_dir = qr/^\d+\.\d+\.\d+(?:$version_word_regex)?\d*$/;
+
print "Working with:\n"
- ."Type:\t$db_type\nHost:\t$db_host\nName:\t$db_name\n"
+ ."Type:\t$db_type\nHost:\t$db_host\nPort:\t$db_port\nName:\t$db_name\n"
."User:\t$db_user\nDBA:\t$dba_user" . ($args{'skip-create'} ? ' (No DBA)' : '') . "\n";
foreach my $action ( @actions ) {
unless ( $args{'force'} ) {
print <<END;
-About to drop $db_type database $db_name on $db_host.
+About to drop $db_type database $db_name on $db_host (port '$db_port').
WARNING: This will erase all data in $db_name.
END
return (0, "Couldn't read dir '$base_dir' with upgrade data")
unless -d $base_dir || -r _;
- my $version_word_regex = join '|', RT::Handle->version_words;
my $upgrading_from = undef;
do {
if ( defined $upgrading_from ) {
print "Doesn't match #.#.#: ";
} else {
- print "Enter RT version you're upgrading from: ";
+ print "Enter $args{package} version you're upgrading from: ";
}
$upgrading_from = scalar <STDIN>;
chomp $upgrading_from;
$upgrading_from =~ s/\s+//g;
- } while $upgrading_from !~ /^\d+\.\d+\.\d+(?:$version_word_regex)?\d*$/;
+ } while $upgrading_from !~ /$version_dir/;
my $upgrading_to = $RT::VERSION;
return (0, "The current version $upgrading_to is lower than $upgrading_from")
if ( defined $custom_upgrading_to ) {
print "Doesn't match #.#.#: ";
} else {
- print "\nEnter RT version if you want to stop upgrade at some point,\n";
+ print "\nEnter $args{package} version if you want to stop upgrade at some point,\n";
print " or leave it blank if you want apply above upgrades: ";
}
$custom_upgrading_to = scalar <STDIN>;
chomp $custom_upgrading_to;
$custom_upgrading_to =~ s/\s+//g;
last unless $custom_upgrading_to;
- } while $custom_upgrading_to !~ /^\d+\.\d+\.\d+(?:$version_word_regex)?\d*$/;
+ } while $custom_upgrading_to !~ /$version_dir/;
if ( $custom_upgrading_to ) {
return (
my ($base_dir, $from, $to) = @_;
opendir( my $dh, $base_dir ) or die "couldn't open dir: $!";
- my @versions = grep -d "$base_dir/$_" && /\d+\.\d+\.\d+/, readdir $dh;
+ my @versions = grep -d "$base_dir/$_" && /$version_dir/, readdir $dh;
closedir $dh;
+ die "\nERROR: No upgrade data found in '$base_dir'! Perhaps you specified the wrong --datadir?\n"
+ unless @versions;
+
return
grep defined $to ? RT::Handle::cmp_version($_, $to) <= 0 : 1,
grep RT::Handle::cmp_version($_, $from) > 0,
sub get_dba_password {
print "In order to create or update your RT database,"
. " this script needs to connect to your "
- . " $db_type instance on $db_host as $dba_user\n";
+ . " $db_type instance on $db_host (port '$db_port') as $dba_user\n";
print "Please specify that user's database password below. If the user has no database\n";
print "password, just press return.\n\n";
print "Password: ";
jQuery(event.target).val(ui.item.value);
jQuery(event.target).closest("form").submit();
}
- });
+ }).addClass("autocompletes-user");
});
</script>
% }
form.find('input[name=UserOp]').val('=');
form.submit();
}
- });
+ }).addClass("autocompletes-user");
});
</script>
</form>
%#
%# END BPS TAGGED BLOCK }}}
<& /Elements/Header, Title => loc('Create an article in class...') &>
-<& /Elements/Tabs, Title => loc('Create an article in class...') &>
+<& /Elements/Tabs &>
<ul>
% my $Classes = RT::Classes->new($session{'CurrentUser'});
% $Classes->LimitToEnabled();
# collection is ordered or not
if ( @OrderBy && ($AllowSorting || !$Collection->{'order_by'}) ) {
if ( $OrderBy[0] =~ /\|/ ) {
- @OrderBy = grep length($_), split /\|/, $OrderBy[0];
+ @OrderBy = split /\|/, $OrderBy[0];
@Order = split /\|/,$Order[0];
}
+ @OrderBy = grep length, @OrderBy;
$Collection->OrderByCols(
map { { FIELD => $OrderBy[$_], ORDER => $Order[$_] } }
( 0 .. $#OrderBy )
my $arg = $DECODED_ARGS->{ $name };
my $checked = '';
if ( $arg && ref $arg ) {
- $checked = 'checked="checked"' if grep $_ == $id, @$arg;
+ $checked = 'checked="checked"' if grep $_ == $id, grep { defined and length } @$arg;
}
elsif ( $arg ) {
$checked = 'checked="checked"' if $arg == $id;
%# END BPS TAGGED BLOCK }}}
% while ( $Values and my $value = $Values->Next ) {
%# XXX - let user download the file(s) here?
-<input type="checkbox" class="checkbox" name="<%$NamePrefix%><%$CustomField->Id%>-DeleteValueIds" class="CF-<%$CustomField->id%>-Edit" value="<% $value->Id %>" /><a href="<%RT->Config->Get('WebPath')%>/Download/CustomFieldValue/<% $value->Id %>/<% $value->Content |un %>"><% $value->Content %></a><br />
+<input type="checkbox" name="<%$NamePrefix%><%$CustomField->Id%>-DeleteValueIds" class="checkbox CF-<%$CustomField->id%>-Edit" value="<% $value->Id %>" /><a href="<%RT->Config->Get('WebPath')%>/Download/CustomFieldValue/<% $value->Id %>/<% $value->Content |un %>"><% $value->Content %></a><br />
% }
% if (!$MaxValues || !$Values || $Values->Count < $MaxValues) {
<input type="file" name="<% $NamePrefix %><% $CustomField->Id %>-Upload" class="CF-<%$CustomField->id%>-Edit" />
\'<a href="',
$_->$mode_uri->Resolver->HREF,
\'">',
- ( $_->$mode_uri->IsLocal ? $_->$local_type : $_->$mode ),
+ ( $_->$mode_uri->IsLocal && $_->$local_type ? $_->$local_type : $_->$mode_uri->Resolver->AsString ),
\'</a><br />',
} @{ $_[0]->Links($other_mode,$type)->ItemsArrayRef }
}
<optgroup label="<% $lifecycle %>">
% }
% foreach my $status (@{$statuses_by_lifecycle{$lifecycle}}) {
-% next if ($SkipDeleted && $status eq 'deleted');
+% next if ($SkipDeleted && lc $status eq 'deleted');
% my $selected = defined $Default && $status eq $Default ? 'selected="selected"' : '';
<option value="<% $status %>" <% $selected |n %>><% loc($status) %></option>
% }
$_->{'Format'} =~ s/__(Web(?:Path|Base|BaseURL))__/scalar RT->Config->Get($1)/ge;
# extract-message-catalog would "$1", so we avoid quotes for loc calls
$_->{'Format'} =~ s/__loc\(["']?(\w+)["']?\)__/my $f = "$1"; loc($f)/ge;
- if ( $_->{'Query'} =~ /__Bookmarked__/ ) {
- $_->{'Rows'} = 999;
- }
- elsif ( $_->{'Query'} =~ /__Bookmarks__/ ) {
+ if ( $_->{'Query'} =~ /__Bookmarks__/ ) {
$_->{'Rows'} = 999;
# DEPRECATED: will be here for a while up to 3.10/4.0
my $query_string = sub {
my %args = @_;
my $u = URI->new();
- $u->query_form(%args);
+ $u->query_form(map { $_ => $args{$_} } sort keys %args);
return $u->query;
};
%#
%# COPYRIGHT:
%#
-%# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC
+%# This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
%# <sales@bestpractical.com>
%#
%# (Except where explicitly superseded by other copyright notices)
%#
%# COPYRIGHT:
%#
-%# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC
+%# This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
%# <sales@bestpractical.com>
%#
%# (Except where explicitly superseded by other copyright notices)
-%# BEGIN BPS TAGGED BLOCK {{{
-%#
-%# COPYRIGHT:
-%#
-%# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC
-%# <sales@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 }}}
/**
* Farbtastic Color Picker 1.2
* © 2008 Steven Wittens
if (queryargs.length)
options.source += "?" + queryargs.join("&");
- jQuery(input).autocomplete(options);
+ jQuery(input)
+ .addClass('autocompletes-user')
+ .autocomplete(options)
+ .data("autocomplete")
+ ._renderItem = function(ul, item) {
+ var rendered = jQuery("<a/>");
+
+ if (item.html == null)
+ rendered.text( item.label );
+ else
+ rendered.html( item.html );
+
+ return jQuery("<li/>")
+ .data( "item.autocomplete", item )
+ .append( rendered )
+ .appendTo( ul );
+ };
}
});
}
else {
my ($get, $set, $key, $val, $n, $s);
-
+ my $updated;
foreach $key (keys %data) {
$val = $data{$key};
$key = lc $key;
$k = $changes;
}
}
+ else {
+ $updated ||= 1;
+ }
}
- push(@comments, "# Group $id updated.") unless $n == 0;
+ push(@comments, "# Group $id updated.") if $updated;
}
DONE:
}
else {
my ($get, $set, $key, $val, $n, $s);
-
+ my $updated;
foreach $key (keys %data) {
$val = $data{$key};
$key = lc $key;
$k = $changes;
}
}
+ else {
+ $updated ||= 1;
+ }
}
- push(@comments, "# Queue $id updated.") unless $n == 0;
+ push(@comments, "# Queue $id updated.") if $updated;
}
DONE:
use MIME::Entity;
use LWP::MediaTypes;
use RT::Interface::REST;
-use File::Temp qw(tempfile);
-my @tmp_files;
$RT::Logger->debug("Got ticket id=$id for comment");
$RT::Logger->debug("Got args @{[keys(%changes)]}.");
goto OUTPUT;
}
-my $cgi = $m->cgi_object;
my $ent = MIME::Entity->build(
Type => "multipart/mixed",
'X-RT-Interface' => 'REST',
);
-$ent->attach(Data => $changes{Text}) if $changes{Text};
+$ent->attach(
+ 'Content-Type' => $changes{'Content-Type'} || 'text/plain',
+ Data => $changes{Text},
+) if $changes{Text};
-my $i = 1;
-foreach my $att (@atts) {
- local $/=undef;
- my $file = $att;
- $file =~ s#^.*[\\/]##;
- my $fh = $cgi->upload("attachment_$i");
- if ($fh) {
- my $buf;
- my ($w, $tmp) = tempfile();
- my $info = $cgi->uploadInfo($fh);
- push @tmp_files, $tmp;
-
- while (sysread($fh, $buf, 8192)) {
- syswrite($w, $buf);
- }
-
- $ent->attach(
- Path => $tmp,
- Type => $info->{'Content-Type'} || guess_media_type($tmp),
- Filename => $file,
- Disposition => "attachment"
- );
- }
- else {
+{
+ my ($status, $msg) = process_attachments($ent, @atts);
+ unless ( $status ) {
$e = 1;
- $c = "# No attachment for $att.";
+ $c = "# $msg";
goto OUTPUT;
}
-
- $i++;
}
unless ($ticket->CurrentUserHasRight('ModifyTicket') ||
OUTPUT:
-unlink @tmp_files;
return [ $c, $o, $k, $e ];
</%INIT>
my @people = qw(Requestors Cc AdminCc);
my @create = qw(Queue Requestor Subject Cc AdminCc Owner Status Priority
InitialPriority FinalPriority TimeEstimated TimeWorked
- TimeLeft Starts Started Due Resolved);
+ TimeLeft Starts Started Due Resolved Content-Type);
my @simple = qw(Subject Status Priority Disabled TimeEstimated TimeWorked
TimeLeft InitialPriority FinalPriority);
my %dates = map {lc $_ => $_} @dates;
return [ "# Ticket $id does not exist.", [], {}, 1 ];
}
elsif ( %data ) {
- if ( $data{status} && $data{status} eq 'deleted' && ! grep { $_ ne 'id' && $_ ne 'status' } keys %data ) {
+ if ( $data{status} && lc $data{status} eq 'deleted' && ! grep { $_ ne 'id' && $_ ne 'status' } keys %data ) {
if ( !$ticket->CurrentUserHasRight('DeleteTicket') ) {
return [ "# You are not allowed to delete ticket $id.", [], {}, 1 ];
}
return [
"# Required: id, Queue",
[ qw(id Queue Requestor Subject Cc AdminCc Owner Status Priority
- InitialPriority FinalPriority TimeEstimated Starts Due Text) ],
+ InitialPriority FinalPriority TimeEstimated Starts Due Attachment Text) ],
{
id => "ticket/new",
Queue => $queue->Name,
TimeEstimated => 0,
Starts => $starts->ISO,
Due => $due->ISO,
+ Attachment => '',
Text => "",
},
0
else {
# We'll create a new ticket, and fall through to set fields that
# can't be set in the call to Create().
- my (%v, $text);
+ my (%v, $text, @atts);
foreach my $k (keys %data) {
# flexibly parse any dates
elsif (lc $k eq 'text') {
$text = delete $data{$k};
}
+ elsif (lc $k eq 'attachment') {
+ push @atts, @{ vsplit(delete $data{$k}) };
+ }
elsif ( $k !~ /^(?:id|requestors)$/i ) {
$e = 1;
push @$o, $k;
# people fields allow multiple values
$v{$_} = vsplit($v{$_}) foreach ( grep $create{lc $_}, @people );
- if ($text) {
+ if ($text || @atts) {
$v{MIMEObj} =
MIME::Entity->build(
+ Type => "multipart/mixed",
From => $session{CurrentUser}->EmailAddress,
Subject => $v{Subject},
- Data => $text,
'X-RT-Interface' => 'REST',
);
+ $v{MIMEObj}->attach(
+ Data => $text,
+ 'Content-Type' => $v{'Content-Type'} || 'text/plain',
+ ) if $text;
+ my ($status, $msg) = process_attachments($v{'MIMEObj'}, @atts);
+ unless ($status) {
+ push(@comments, "# $msg");
+ goto DONE;
+ }
+ $v{MIMEObj}->make_singlepart;
}
my($tid,$trid,$terr) = $ticket->Create(%v);
}
else {
my ($get, $set, $key, $val, $n, $s);
+ my $updated;
foreach $key (keys %data) {
$val = $data{$key};
$s =~ s/\\'/'/g;
push @new, $s;
}
- elsif ( $a =~ /^q{/ ) {
+ elsif ( $a =~ /^q\{/ ) {
my $s = $a;
- while ( $a !~ /}$/ ) {
+ while ( $a !~ /\}$/ ) {
( $a, $b ) = split /\s*,\s*/, $b, 2;
$s .= ',' . $a;
}
- $s =~ s/^q{//;
- $s =~ s/}//;
+ $s =~ s/^q\{//;
+ $s =~ s/\}//;
push @new, $s;
}
else {
}
}
}
- elsif ($key ne 'id' && $key ne 'type' && $key ne 'creator') {
+ elsif ($key ne 'id' && $key ne 'type' && $key ne 'creator' && $key ne 'content-type' ) {
$n = 0;
$s = "Unknown field.";
}
$k = $changes;
}
}
+ else {
+ $updated ||= 1;
+ }
}
- push(@comments, "# Ticket ".$ticket->id." updated.") unless $n == 0;
+ push(@comments, "# Ticket ".$ticket->id." updated.") if $updated;
}
DONE:
}
else {
my ($get, $set, $key, $val, $n, $s);
-
+ my $updated;
foreach $key (keys %data) {
$val = $data{$key};
$key = lc $key;
$k = $changes;
}
}
+ else {
+ $updated ||= 1;
+ }
}
- push(@comments, "# User $id updated.") unless $n == 0;
+ push(@comments, "# User $id updated.") if $updated;
}
DONE:
use MIME::Entity;
use LWP::MediaTypes;
use RT::Interface::REST;
-use File::Temp qw(tempfile);
-my @tmp_files;
my $ticket = RT::Ticket->new($session{CurrentUser});
my $object = $r->path_info;
);
$ent->attach(Data => $k->{Text}) if $k->{Text};
-my $i = 1;
-foreach my $att (@atts) {
- local $/=undef;
- my $file = $att;
- $file =~ s#^.*[\\/]##;
-
- my $fh = $cgi->upload("attachment_$i");
- if ($fh) {
- my $buf;
- my ($w, $tmp) = tempfile();
- push @tmp_files, $tmp;
- my $info = $cgi->uploadInfo();
-
- while (sysread($fh, $buf, 8192)) {
- syswrite($w, $buf);
- }
-
- $ent->attach(
- Path => $tmp,
- Type => $info->{'Content-Type'} || guess_media_type($tmp),
- Filename => $file,
- Disposition => "attachment"
- );
- }
- else {
+{
+ my ($res, $msg) = process_attachments($ent, @atts);
+ unless ( $res ) {
$status = "400 Bad Request";
- $output = "No attachment for $att.\n";
+ $output = "$msg\n";
goto OUTPUT;
}
-
- $i++;
}
$ticket->Load($object);
OUTPUT:
-unlink @tmp_files;
</%INIT>
RT/<% $RT::VERSION %> <% $status %>
# 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+|'\w*CF\.\{.*?\}')$/
&& ( ref $ARGS{$arg} eq "ARRAY"
? grep $_ ne '', @{ $ARGS{$arg} }
: $ARGS{$arg} ne '' );
$value = "'$value'";
}
- if ($keyword =~ /^'CF\.{(.*)}'/) {
- my $cf = $1;
+ if ($keyword =~ /^'(\w*CF)\.\{(.*)\}'/) {
+ my ($field, $cf) = ($1, $2);
$cf =~ s/(['\\])/\\$1/g;
- $keyword = "'CF.{$cf}'";
+ $keyword = "'$field.{$cf}'";
}
my $clause = {
#Iterate through each ticket we've been handed
my @linkresults;
-my %queues;
$Tickets->RedoSearch();
#Update the links
$ARGS{'id'} = $Ticket->id;
- $queues{ $Ticket->QueueObj->Id }++;
my @updateresults = ProcessUpdateMessage(
TicketObj => $Ticket,
my $TxnCFs = RT::CustomFields->new( $session{CurrentUser} );
$TxnCFs->LimitToLookupType( RT::Transaction->CustomFieldLookupType );
-$TxnCFs->LimitToGlobalOrObjectId( sort keys %queues );
+$TxnCFs->LimitToGlobalOrObjectId( keys %$seen_queues );
</%INIT>
<%args>
# FIXME: should be factored with RT::Report::Tickets::Label :(
my $PrimaryGroupByLabel;
-if ( $PrimaryGroupBy =~ /^(?:CF|CustomField)\.{(.*)}$/ ) {
+if ( $PrimaryGroupBy =~ /^(?:CF|CustomField)\.\{(.*)\}$/ ) {
my $cf = $1;
if ( $cf =~ /\D/ ) {
$PrimaryGroupByLabel = loc( "custom field '[_1]'", $cf );
<& ConditionRow, Condition => $_ &>
% }
<%INIT>
-my $CustomFields = RT::CustomFields->new( $session{'CurrentUser'});
-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);
- $CustomFields->LimitToQueue($queue->Id) if $queue->Id;
-}
-$CustomFields->LimitToGlobal;
$m->callback(
CallbackName => 'MassageCustomFields',
CustomFields => $CustomFields,
my @lines;
while ( my $CustomField = $CustomFields->Next ) {
my %line;
- $line{'Name'} = "'CF.{" . $CustomField->Name . "}'";
+ $line{'Name'} = "'$TicketSQLField.{" . $CustomField->Name . "}'";
$line{'Field'} = $CustomField->Name;
# Op
<%ARGS>
%queues => ()
+$CustomFields
+$TicketSQLField => 'CF'
</%ARGS>
<table width="100%" cellspacing="0" cellpadding="0" border="0">
-
+% $m->callback( %ARGS, CallbackName => "BeforeBasics" );
<& PickBasics, queues => \%queues &>
-<& PickCFs, queues => \%queues &>
+<& PickTicketCFs, queues => \%queues &>
+% $m->callback( %ARGS, CallbackName => "AfterCFs" );
<tr class="separator"><td colspan="3"><hr /></td></tr>
<tr>
--- /dev/null
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
+%# <sales@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 }}}
+<%ARGS>
+%queues => ()
+</%ARGS>
+<%init>
+my $CustomFields = RT::CustomFields->new( $session{'CurrentUser'});
+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);
+ $CustomFields->LimitToQueue($queue->Id) if $queue->Id;
+}
+$CustomFields->LimitToGlobal;
+</%init>
+<& PickCFs, %ARGS, TicketSQLField => 'CF', CustomFields => $CustomFields &>
% $m->callback( ARGSRef => \%ARGS, CallbackName => 'BeforeResults' );
+% unless ($ok) {
+% $msg =~ s{ at .*? line .*}{}s;
+<&| /Widgets/TitleBox, title => loc("Error"), class => "error" &>
+<&|/l_unsafe, "<i>".$m->interp->apply_escapes($msg, "h")."</i>" &>There was an error parsing your search query: [_1]. Your RT admin can find more information in the error logs.</&>
+</&>
+% } else {
+
<& /Elements/CollectionList,
Query => $Query,
TotalFound => $ticketcount,
BaseURL => $BaseURL
&>
+% }
% $m->callback( ARGSRef => \%ARGS, CallbackName => 'AfterResults' );
% my %hiddens = (Query => $Query, Format => $Format, Rows => $Rows, OrderBy => $OrderBy, Order => $Order, HideResults => $HideResults, Page => $Page, SavedChartSearchId => $SavedChartSearchId );
% my @strong = qw(<strong> </strong>);
-<p><&|/l_unsafe, @strong &>Search for tickets by entering [_1]id[_2] numbers, subject words [_1]"in quotes"[_2], [_1]queues[_2] by name, Owners by [_1]username[_2], Requestors by [_1]email address[_2], and ticket [_1]statuses[_2].</&></p>
+<p><&|/l_unsafe, @strong &>Search for tickets by entering [_1]id[_2] numbers, subject words [_1]"in quotes"[_2], [_1]queues[_2] by name, Owners by [_1]username[_2], Requestors by [_1]email address[_2], and ticket [_1]statuses[_2]. Searching for [_1]@domainname.com[_2] will return tickets with requestors from that domain.</&></p>
<p><&|/l&>Any word not recognized by RT is searched for in ticket subjects.</&></p>
<tr><td class="label"><&|/l&>Attached file</&>:</td>
<td>
<&|/l&>Check box to delete</&><br />
-% foreach my $attach_name (keys %{$session{'Attachments'}}) {
+% foreach my $attach_name (sort keys %{$session{'Attachments'}}) {
<input type="checkbox" class="checkbox" name="DeleteAttach-<%$attach_name%>" value="1" /><%$attach_name%><br />
% } # end of foreach
</td>
% }
% }
-% if ($Ticket->Status ne "deleted") {
+% if (lc $Ticket->Status ne "deleted") {
<&|/l&>New reminder:</&>
<& SELF:NewReminder, Ticket => $Ticket &>
% }
-% return($Ticket->Status ne "deleted" or $visible);
+% return(lc $Ticket->Status ne "deleted" or $visible);
<%method NewReminder>
<%args>
$Ticket
ticket => $Ticket,
);
- require HTML::Quoted;
- $content = HTML::Quoted->extract($content) unless length $name;
+ unless (length $name) {
+ eval {
+ require HTML::Quoted;
+ $content = HTML::Quoted->extract($content)
+ };
+ if ($@) {
+ RT->Logger->error(
+ "HTML::Quoted couldn't process attachment #@{[$message->id]}: $@."
+ . " This is a bug, please report it to rt-bugs\@bestpractical.com.");
+ }
+ }
$m->comp(
'ShowMessageStanza',
# It's a text type we don't have special handling for
else {
unless ( length $name ) {
- eval { require Text::Quoted; $content = Text::Quoted::extract($content); };
- if ($@) { $RT::Logger->warning( "Text::Quoted failed: $@" ) }
+ eval {
+ require Text::Quoted;
+ # XXX: Deprecate ->can check in 4.2 and simply bump version requirement.
+ Text::Quoted::set_quote_characters(undef) # only use >
+ if Text::Quoted->can("set_quote_characters");
+ $content = Text::Quoted::extract($content);
+ };
+ if ($@) {
+ RT->Logger->error(
+ "Text::Quoted couldn't process attachment #@{[$message->id]}: $@."
+ . " This is a bug, please report it to rt-bugs\@bestpractical.com.");
+ }
}
$m->comp(