Upgrade 4.0.17 clean.
authorMikal Kolbein Gule <m.k.gule@usit.uio.no>
Wed, 14 Aug 2013 09:11:48 +0000 (11:11 +0200)
committerMikal Kolbein Gule <m.k.gule@usit.uio.no>
Wed, 14 Aug 2013 09:11:48 +0000 (11:11 +0200)
173 files changed:
bin/rt
docs/UPGRADING-4.0
docs/backups.pod
docs/initialdata.pod
etc/RT_Config.pm
etc/upgrade/3.1.0/acl.Oracle [new file with mode: 0644]
etc/upgrade/3.1.0/acl.Pg [new file with mode: 0644]
etc/upgrade/3.1.0/acl.SQLite [new file with mode: 0644]
etc/upgrade/3.1.0/acl.mysql [new file with mode: 0644]
etc/upgrade/3.1.0/content [new file with mode: 0644]
etc/upgrade/3.1.0/schema.Oracle [new file with mode: 0644]
etc/upgrade/3.1.0/schema.Pg [new file with mode: 0644]
etc/upgrade/3.1.0/schema.SQLite [new file with mode: 0644]
etc/upgrade/3.1.0/schema.mysql [new file with mode: 0644]
etc/upgrade/3.1.15/content [new file with mode: 0644]
etc/upgrade/3.1.17/content [new file with mode: 0644]
etc/upgrade/3.3.0/acl.Oracle [new file with mode: 0644]
etc/upgrade/3.3.0/acl.Pg [new file with mode: 0644]
etc/upgrade/3.3.0/acl.SQLite [new file with mode: 0644]
etc/upgrade/3.3.0/acl.mysql [new file with mode: 0644]
etc/upgrade/3.3.0/content [new file with mode: 0644]
etc/upgrade/3.3.0/schema.Oracle [new file with mode: 0644]
etc/upgrade/3.3.0/schema.Pg [new file with mode: 0644]
etc/upgrade/3.3.0/schema.mysql [new file with mode: 0644]
etc/upgrade/3.3.11/acl.Oracle [new file with mode: 0644]
etc/upgrade/3.3.11/acl.Pg [new file with mode: 0644]
etc/upgrade/3.3.11/acl.SQLite [new file with mode: 0644]
etc/upgrade/3.3.11/acl.mysql [new file with mode: 0644]
etc/upgrade/3.3.11/content [new file with mode: 0644]
etc/upgrade/3.3.11/schema.Oracle [new file with mode: 0644]
etc/upgrade/3.3.11/schema.Pg [new file with mode: 0644]
etc/upgrade/3.3.11/schema.SQLite [new file with mode: 0644]
etc/upgrade/3.3.11/schema.mysql [new file with mode: 0644]
etc/upgrade/3.5.1/content [new file with mode: 0644]
etc/upgrade/3.7.1/content [new file with mode: 0644]
etc/upgrade/3.7.10/content [new file with mode: 0644]
etc/upgrade/3.7.15/content [new file with mode: 0644]
etc/upgrade/3.7.19/content [new file with mode: 0644]
etc/upgrade/3.7.3/schema.Oracle [new file with mode: 0644]
etc/upgrade/3.7.3/schema.Pg [new file with mode: 0644]
etc/upgrade/3.7.3/schema.mysql [new file with mode: 0644]
etc/upgrade/3.7.81/schema.Oracle [new file with mode: 0644]
etc/upgrade/3.7.81/schema.mysql [new file with mode: 0644]
etc/upgrade/3.7.82/content [new file with mode: 0644]
etc/upgrade/3.7.85/content [new file with mode: 0644]
etc/upgrade/3.7.86/content [new file with mode: 0644]
etc/upgrade/3.7.87/content [new file with mode: 0644]
etc/upgrade/3.8-branded-queues-extension [new file with mode: 0644]
etc/upgrade/3.8-ical-extension [new file with mode: 0644]
etc/upgrade/3.8.0/content [new file with mode: 0644]
etc/upgrade/3.8.1/content [new file with mode: 0644]
etc/upgrade/3.8.2/content [new file with mode: 0644]
etc/upgrade/3.8.3/content [new file with mode: 0644]
etc/upgrade/3.8.3/schema.Pg [new file with mode: 0644]
etc/upgrade/3.8.4/content [new file with mode: 0644]
etc/upgrade/3.8.6/content [new file with mode: 0644]
etc/upgrade/3.8.8/content [new file with mode: 0644]
etc/upgrade/3.8.9/content [new file with mode: 0644]
etc/upgrade/3.9.1/content [new file with mode: 0644]
etc/upgrade/3.9.2/content [new file with mode: 0644]
etc/upgrade/3.9.3/schema.Oracle [new file with mode: 0644]
etc/upgrade/3.9.3/schema.Pg [new file with mode: 0644]
etc/upgrade/3.9.3/schema.SQLite [new file with mode: 0644]
etc/upgrade/3.9.3/schema.mysql [new file with mode: 0644]
etc/upgrade/3.9.5/backcompat [new file with mode: 0644]
etc/upgrade/3.9.5/schema.Oracle [new file with mode: 0644]
etc/upgrade/3.9.5/schema.Pg [new file with mode: 0644]
etc/upgrade/3.9.5/schema.SQLite [new file with mode: 0644]
etc/upgrade/3.9.5/schema.mysql [new file with mode: 0644]
etc/upgrade/3.9.6/schema.Oracle [new file with mode: 0644]
etc/upgrade/3.9.6/schema.Pg [new file with mode: 0644]
etc/upgrade/3.9.6/schema.SQLite [new file with mode: 0644]
etc/upgrade/3.9.6/schema.mysql [new file with mode: 0644]
etc/upgrade/3.9.7/content [new file with mode: 0644]
etc/upgrade/3.9.7/schema.Oracle [new file with mode: 0644]
etc/upgrade/3.9.7/schema.Pg [new file with mode: 0644]
etc/upgrade/3.9.7/schema.SQLite [new file with mode: 0644]
etc/upgrade/3.9.7/schema.mysql [new file with mode: 0644]
etc/upgrade/3.9.8/content [new file with mode: 0644]
etc/upgrade/3.9.8/schema.Oracle [new file with mode: 0644]
etc/upgrade/3.9.8/schema.Pg [new file with mode: 0644]
etc/upgrade/3.9.8/schema.SQLite [new file with mode: 0644]
etc/upgrade/3.9.8/schema.mysql [new file with mode: 0644]
etc/upgrade/4.0.0rc2/schema.mysql [new file with mode: 0644]
etc/upgrade/4.0.0rc4/schema.Oracle [new file with mode: 0644]
etc/upgrade/4.0.0rc4/schema.Pg [new file with mode: 0644]
etc/upgrade/4.0.0rc4/schema.mysql [new file with mode: 0644]
etc/upgrade/4.0.0rc7/content [new file with mode: 0644]
etc/upgrade/4.0.1/acl.Pg [new file with mode: 0644]
etc/upgrade/4.0.1/content [new file with mode: 0644]
etc/upgrade/4.0.12/schema.Oracle [new file with mode: 0644]
etc/upgrade/4.0.12/schema.Pg [new file with mode: 0644]
etc/upgrade/4.0.12/schema.mysql [new file with mode: 0644]
etc/upgrade/4.0.13/schema.Oracle [new file with mode: 0644]
etc/upgrade/4.0.13/schema.Pg [new file with mode: 0644]
etc/upgrade/4.0.13/schema.mysql [new file with mode: 0644]
etc/upgrade/4.0.3/content [new file with mode: 0644]
etc/upgrade/4.0.4/content [new file with mode: 0644]
etc/upgrade/4.0.6/content [new file with mode: 0644]
etc/upgrade/4.0.6/schema.mysql [new file with mode: 0644]
etc/upgrade/4.0.9/content [new file with mode: 0644]
etc/upgrade/generate-rtaddressregexp [new file with mode: 0644]
etc/upgrade/sanity-check-stylesheets.pl [new file with mode: 0644]
etc/upgrade/shrink_cgm_table.pl [new file with mode: 0644]
etc/upgrade/shrink_transactions_table.pl [new file with mode: 0644]
etc/upgrade/split-out-cf-categories [new file with mode: 0644]
etc/upgrade/upgrade-articles [new file with mode: 0644]
etc/upgrade/upgrade-mysql-schema.pl [new file with mode: 0644]
etc/upgrade/vulnerable-passwords [new file with mode: 0644]
lib/RT/Action/SendEmail.pm
lib/RT/Attribute.pm
lib/RT/Attributes.pm
lib/RT/Classes.pm
lib/RT/Config.pm
lib/RT/CustomField.pm
lib/RT/CustomFieldValues/Groups.pm
lib/RT/CustomFields.pm
lib/RT/EmailParser.pm
lib/RT/Generated.pm
lib/RT/Handle.pm
lib/RT/Interface/Email.pm
lib/RT/Interface/REST.pm
lib/RT/Interface/Web.pm
lib/RT/Interface/Web/Session.pm
lib/RT/Lifecycle.pm
lib/RT/Pod/HTML.pm
lib/RT/Pod/HTMLBatch.pm
lib/RT/Principals.pm
lib/RT/Record.pm
lib/RT/Reminders.pm
lib/RT/Report/Tickets.pm
lib/RT/Search/Googleish.pm
lib/RT/SearchBuilder.pm
lib/RT/Ticket.pm
lib/RT/Tickets.pm
lib/RT/Tickets_SQL.pm
lib/RT/Transaction.pm
lib/RT/URI.pm
lib/RT/Util.pm
sbin/rt-email-digest
sbin/rt-fulltext-indexer
sbin/rt-setup-database
share/html/Admin/Elements/SelectNewGroupMembers
share/html/Admin/Users/index.html
share/html/Articles/Article/PreCreate.html
share/html/Elements/CollectionList
share/html/Elements/ColumnMap
share/html/Elements/EditCustomFieldBinary
share/html/Elements/RT__Ticket/ColumnMap
share/html/Elements/SelectStatus
share/html/Elements/ShowSearch
share/html/Elements/Tabs
share/html/Helpers/Autocomplete/autohandler
share/html/Helpers/autohandler
share/html/NoAuth/css/base/farbtastic.css
share/html/NoAuth/js/userautocomplete.js
share/html/REST/1.0/Forms/group/default
share/html/REST/1.0/Forms/queue/default
share/html/REST/1.0/Forms/ticket/comment
share/html/REST/1.0/Forms/ticket/default
share/html/REST/1.0/Forms/user/default
share/html/REST/1.0/ticket/comment
share/html/Search/Build.html
share/html/Search/Bulk.html
share/html/Search/Chart.html
share/html/Search/Elements/PickCFs
share/html/Search/Elements/PickCriteria
share/html/Search/Elements/PickTicketCFs [new file with mode: 0644]
share/html/Search/Results.html
share/html/Search/Simple.html
share/html/Ticket/Elements/AddAttachments
share/html/Ticket/Elements/Reminders
share/html/Ticket/Elements/ShowTransactionAttachments

diff --git a/bin/rt b/bin/rt
index e5911bd..a6df962 100755 (executable)
--- a/bin/rt
+++ b/bin/rt
@@ -472,7 +472,7 @@ sub show {
 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 = ();
@@ -486,6 +486,7 @@ sub edit {
         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());
         }
@@ -655,17 +656,45 @@ sub edit {
         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) {
@@ -738,7 +767,7 @@ sub setcommand {
 
 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) {
@@ -747,7 +776,7 @@ sub comment {
         if (/^-e$/) {
             $edit = 1;
         }
-        elsif (/^-[abcmw]$/) {
+        elsif (/^-(?:[abcmw]|ct)$/) {
             unless (@ARGV) {
                 whine "No argument specified with $_.";
                 $bad = 1; last;
@@ -760,6 +789,9 @@ sub comment {
                 }
                 push @files, shift @ARGV;
             }
+            elsif (/-ct/) {
+                $content_type = shift @ARGV;
+            }
             elsif (/-([bc])/) {
                 my $a = $_ eq "-b" ? \@bcc : \@cc;
                 @$a = split /\s*,\s*/, shift @ARGV;
@@ -771,7 +803,6 @@ sub comment {
                     while (<STDIN>) { $msg .= $_ }
                 }
             }
-
             elsif (/-w/) { $wtime = shift @ARGV }
         }
         elsif (!$id && m|^(?:ticket/)?($idlist)$|) {
@@ -793,7 +824,7 @@ sub comment {
 
     my $form = [
         "",
-        [ "Ticket", "Action", "Cc", "Bcc", "Attachment", "TimeWorked", "Text" ],
+        [ "Ticket", "Action", "Cc", "Bcc", "Attachment", "TimeWorked", "Content-Type", "Text" ],
         {
             Ticket     => $id,
             Action     => $action,
@@ -801,6 +832,7 @@ sub comment {
             Bcc        => [ @bcc ],
             Attachment => [ @files ],
             TimeWorked => $wtime || '',
+            'Content-Type' => $content_type || 'text/plain',
             Text       => $msg || '',
             Status => ''
         }
@@ -809,30 +841,19 @@ sub comment {
     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;
@@ -1466,6 +1487,43 @@ sub read_passwd {
     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";
@@ -1525,15 +1583,15 @@ sub vsplit {
                 }
                 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 {
@@ -2273,12 +2331,14 @@ Text:
         -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
@@ -2310,6 +2370,7 @@ Text:
     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.
index 687dfbc..3e5b74a 100644 (file)
@@ -189,3 +189,53 @@ these types before insertion.
 
 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
index 6fce6bc..0928790 100644 (file)
@@ -33,7 +33,7 @@ RT. :)
 
     ( 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>
@@ -54,7 +54,7 @@ but lets you take backups without putting load on your production server.
 
     ( 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
 
@@ -102,7 +102,7 @@ recreate those.
 
 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!
 
index 6445fb0..c649b62 100644 (file)
@@ -90,7 +90,7 @@ the admin interface.  B<Do not> omit the C<< Domain => 'UserDefined' >> line.
 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
index e020794..fdd8874 100644 (file)
@@ -500,6 +500,15 @@ world, you can set C<$MailCommand> to 'testfile' which writes all mail
 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");
@@ -512,6 +521,12 @@ Correspond mail address of the ticket's queue.
 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
diff --git a/etc/upgrade/3.1.0/acl.Oracle b/etc/upgrade/3.1.0/acl.Oracle
new file mode 100644 (file)
index 0000000..73c16ae
--- /dev/null
@@ -0,0 +1,4 @@
+sub acl {
+    return ();
+}
+1;
diff --git a/etc/upgrade/3.1.0/acl.Pg b/etc/upgrade/3.1.0/acl.Pg
new file mode 100644 (file)
index 0000000..9c88782
--- /dev/null
@@ -0,0 +1,19 @@
+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;
diff --git a/etc/upgrade/3.1.0/acl.SQLite b/etc/upgrade/3.1.0/acl.SQLite
new file mode 100644 (file)
index 0000000..73c16ae
--- /dev/null
@@ -0,0 +1,4 @@
+sub acl {
+    return ();
+}
+1;
diff --git a/etc/upgrade/3.1.0/acl.mysql b/etc/upgrade/3.1.0/acl.mysql
new file mode 100644 (file)
index 0000000..73c16ae
--- /dev/null
@@ -0,0 +1,4 @@
+sub acl {
+    return ();
+}
+1;
diff --git a/etc/upgrade/3.1.0/content b/etc/upgrade/3.1.0/content
new file mode 100644 (file)
index 0000000..3117daf
--- /dev/null
@@ -0,0 +1,2 @@
+# nothing to do
+1;
diff --git a/etc/upgrade/3.1.0/schema.Oracle b/etc/upgrade/3.1.0/schema.Oracle
new file mode 100644 (file)
index 0000000..a8aae18
--- /dev/null
@@ -0,0 +1,17 @@
+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);
diff --git a/etc/upgrade/3.1.0/schema.Pg b/etc/upgrade/3.1.0/schema.Pg
new file mode 100644 (file)
index 0000000..08a964c
--- /dev/null
@@ -0,0 +1,25 @@
+
+
+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);
+
+
+
diff --git a/etc/upgrade/3.1.0/schema.SQLite b/etc/upgrade/3.1.0/schema.SQLite
new file mode 100644 (file)
index 0000000..1dd466f
--- /dev/null
@@ -0,0 +1,21 @@
+--- {{{ 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);
+
+--- }}}
+
diff --git a/etc/upgrade/3.1.0/schema.mysql b/etc/upgrade/3.1.0/schema.mysql
new file mode 100644 (file)
index 0000000..e4f02c2
--- /dev/null
@@ -0,0 +1,19 @@
+
+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);
+
diff --git a/etc/upgrade/3.1.15/content b/etc/upgrade/3.1.15/content
new file mode 100644 (file)
index 0000000..d23125a
--- /dev/null
@@ -0,0 +1,7 @@
+@Scrips = (
+    {  ScripCondition => 'On Owner Change',
+       ScripAction    => 'Notify Owner',
+       Template       => 'Transaction' },
+);
+
+1;
diff --git a/etc/upgrade/3.1.17/content b/etc/upgrade/3.1.17/content
new file mode 100644 (file)
index 0000000..1d648d8
--- /dev/null
@@ -0,0 +1,22 @@
+@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;
diff --git a/etc/upgrade/3.3.0/acl.Oracle b/etc/upgrade/3.3.0/acl.Oracle
new file mode 100644 (file)
index 0000000..73c16ae
--- /dev/null
@@ -0,0 +1,4 @@
+sub acl {
+    return ();
+}
+1;
diff --git a/etc/upgrade/3.3.0/acl.Pg b/etc/upgrade/3.3.0/acl.Pg
new file mode 100644 (file)
index 0000000..bd2e36c
--- /dev/null
@@ -0,0 +1,20 @@
+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;
diff --git a/etc/upgrade/3.3.0/acl.SQLite b/etc/upgrade/3.3.0/acl.SQLite
new file mode 100644 (file)
index 0000000..73c16ae
--- /dev/null
@@ -0,0 +1,4 @@
+sub acl {
+    return ();
+}
+1;
diff --git a/etc/upgrade/3.3.0/acl.mysql b/etc/upgrade/3.3.0/acl.mysql
new file mode 100644 (file)
index 0000000..73c16ae
--- /dev/null
@@ -0,0 +1,4 @@
+sub acl {
+    return ();
+}
+1;
diff --git a/etc/upgrade/3.3.0/content b/etc/upgrade/3.3.0/content
new file mode 100644 (file)
index 0000000..0afc604
--- /dev/null
@@ -0,0 +1 @@
+1;
diff --git a/etc/upgrade/3.3.0/schema.Oracle b/etc/upgrade/3.3.0/schema.Oracle
new file mode 100644 (file)
index 0000000..f81feeb
--- /dev/null
@@ -0,0 +1,65 @@
+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; 
+
+
diff --git a/etc/upgrade/3.3.0/schema.Pg b/etc/upgrade/3.3.0/schema.Pg
new file mode 100644 (file)
index 0000000..427eae7
--- /dev/null
@@ -0,0 +1,74 @@
+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; 
diff --git a/etc/upgrade/3.3.0/schema.mysql b/etc/upgrade/3.3.0/schema.mysql
new file mode 100644 (file)
index 0000000..d8b0499
--- /dev/null
@@ -0,0 +1,60 @@
+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;
diff --git a/etc/upgrade/3.3.11/acl.Oracle b/etc/upgrade/3.3.11/acl.Oracle
new file mode 100644 (file)
index 0000000..73c16ae
--- /dev/null
@@ -0,0 +1,4 @@
+sub acl {
+    return ();
+}
+1;
diff --git a/etc/upgrade/3.3.11/acl.Pg b/etc/upgrade/3.3.11/acl.Pg
new file mode 100644 (file)
index 0000000..73c16ae
--- /dev/null
@@ -0,0 +1,4 @@
+sub acl {
+    return ();
+}
+1;
diff --git a/etc/upgrade/3.3.11/acl.SQLite b/etc/upgrade/3.3.11/acl.SQLite
new file mode 100644 (file)
index 0000000..73c16ae
--- /dev/null
@@ -0,0 +1,4 @@
+sub acl {
+    return ();
+}
+1;
diff --git a/etc/upgrade/3.3.11/acl.mysql b/etc/upgrade/3.3.11/acl.mysql
new file mode 100644 (file)
index 0000000..73c16ae
--- /dev/null
@@ -0,0 +1,4 @@
+sub acl {
+    return ();
+}
+1;
diff --git a/etc/upgrade/3.3.11/content b/etc/upgrade/3.3.11/content
new file mode 100644 (file)
index 0000000..0afc604
--- /dev/null
@@ -0,0 +1 @@
+1;
diff --git a/etc/upgrade/3.3.11/schema.Oracle b/etc/upgrade/3.3.11/schema.Oracle
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/etc/upgrade/3.3.11/schema.Pg b/etc/upgrade/3.3.11/schema.Pg
new file mode 100644 (file)
index 0000000..6ab5d65
--- /dev/null
@@ -0,0 +1,11 @@
+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;
diff --git a/etc/upgrade/3.3.11/schema.SQLite b/etc/upgrade/3.3.11/schema.SQLite
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/etc/upgrade/3.3.11/schema.mysql b/etc/upgrade/3.3.11/schema.mysql
new file mode 100644 (file)
index 0000000..eff8478
--- /dev/null
@@ -0,0 +1,5 @@
+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;
diff --git a/etc/upgrade/3.5.1/content b/etc/upgrade/3.5.1/content
new file mode 100644 (file)
index 0000000..02d6a0c
--- /dev/null
@@ -0,0 +1,36 @@
+@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:&nbsp;' ",
+        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;
diff --git a/etc/upgrade/3.7.1/content b/etc/upgrade/3.7.1/content
new file mode 100644 (file)
index 0000000..fdd5061
--- /dev/null
@@ -0,0 +1,14 @@
+@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',
+    },
+);
+
+
diff --git a/etc/upgrade/3.7.10/content b/etc/upgrade/3.7.10/content
new file mode 100644 (file)
index 0000000..d19f9e6
--- /dev/null
@@ -0,0 +1,49 @@
+
+@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";
+  }
+}}
+    },
+);
diff --git a/etc/upgrade/3.7.15/content b/etc/upgrade/3.7.15/content
new file mode 100644 (file)
index 0000000..9d97c35
--- /dev/null
@@ -0,0 +1,12 @@
+
+@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 }
+}
+    },
+);
+
diff --git a/etc/upgrade/3.7.19/content b/etc/upgrade/3.7.19/content
new file mode 100644 (file)
index 0000000..31ab1c8
--- /dev/null
@@ -0,0 +1,37 @@
+
+{ 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;
diff --git a/etc/upgrade/3.7.3/schema.Oracle b/etc/upgrade/3.7.3/schema.Oracle
new file mode 100644 (file)
index 0000000..6136efa
--- /dev/null
@@ -0,0 +1,5 @@
+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;
diff --git a/etc/upgrade/3.7.3/schema.Pg b/etc/upgrade/3.7.3/schema.Pg
new file mode 100644 (file)
index 0000000..5d0312e
--- /dev/null
@@ -0,0 +1 @@
+ALTER TABLE customfields ALTER COLUMN pattern TYPE VARCHAR(65536);
diff --git a/etc/upgrade/3.7.3/schema.mysql b/etc/upgrade/3.7.3/schema.mysql
new file mode 100644 (file)
index 0000000..51c376d
--- /dev/null
@@ -0,0 +1 @@
+ALTER TABLE CustomFields CHANGE Pattern Pattern TEXT NULL;
diff --git a/etc/upgrade/3.7.81/schema.Oracle b/etc/upgrade/3.7.81/schema.Oracle
new file mode 100644 (file)
index 0000000..02da4ec
--- /dev/null
@@ -0,0 +1,2 @@
+CREATE INDEX CachedGroupMembers3 on CachedGroupMembers (MemberId, ImmediateParentId);
+
diff --git a/etc/upgrade/3.7.81/schema.mysql b/etc/upgrade/3.7.81/schema.mysql
new file mode 100644 (file)
index 0000000..02da4ec
--- /dev/null
@@ -0,0 +1,2 @@
+CREATE INDEX CachedGroupMembers3 on CachedGroupMembers (MemberId, ImmediateParentId);
+
diff --git a/etc/upgrade/3.7.82/content b/etc/upgrade/3.7.82/content
new file mode 100644 (file)
index 0000000..a1c555f
--- /dev/null
@@ -0,0 +1,13 @@
+@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' },
+    },
+);
+
diff --git a/etc/upgrade/3.7.85/content b/etc/upgrade/3.7.85/content
new file mode 100644 (file)
index 0000000..49ab286
--- /dev/null
@@ -0,0 +1,11 @@
+@Templates = ( 
+              
+              {   Queue       => '0',
+                  Name        => 'Email Digest',    # loc
+                  Description => 'Email template for periodic notification digests',  # loc
+                  Content => q[Subject: RT Email Digest
+
+{ $Argument }
+],
+               },
+);
diff --git a/etc/upgrade/3.7.86/content b/etc/upgrade/3.7.86/content
new file mode 100644 (file)
index 0000000..94912d6
--- /dev/null
@@ -0,0 +1,23 @@
+@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;
+    },
+);
diff --git a/etc/upgrade/3.7.87/content b/etc/upgrade/3.7.87/content
new file mode 100644 (file)
index 0000000..0c677c4
--- /dev/null
@@ -0,0 +1,28 @@
+@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')
+      : ''
+}
+}
+},
+);
+
diff --git a/etc/upgrade/3.8-branded-queues-extension b/etc/upgrade/3.8-branded-queues-extension
new file mode 100644 (file)
index 0000000..4d1cfdf
--- /dev/null
@@ -0,0 +1,95 @@
+#!/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;
+
diff --git a/etc/upgrade/3.8-ical-extension b/etc/upgrade/3.8-ical-extension
new file mode 100644 (file)
index 0000000..3b81231
--- /dev/null
@@ -0,0 +1,96 @@
+#!/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;
diff --git a/etc/upgrade/3.8.0/content b/etc/upgrade/3.8.0/content
new file mode 100644 (file)
index 0000000..4fa5ac7
--- /dev/null
@@ -0,0 +1,22 @@
+@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;
+    },
+);
+
diff --git a/etc/upgrade/3.8.1/content b/etc/upgrade/3.8.1/content
new file mode 100644 (file)
index 0000000..1667efa
--- /dev/null
@@ -0,0 +1,24 @@
+@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;
+    },
+);
+
+
diff --git a/etc/upgrade/3.8.2/content b/etc/upgrade/3.8.2/content
new file mode 100644 (file)
index 0000000..0eef401
--- /dev/null
@@ -0,0 +1,186 @@
+@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");
+            }
+        }
+    },
+);
diff --git a/etc/upgrade/3.8.3/content b/etc/upgrade/3.8.3/content
new file mode 100644 (file)
index 0000000..b8052ac
--- /dev/null
@@ -0,0 +1,91 @@
+@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' 
+        },
+    );
+}
+}
+
diff --git a/etc/upgrade/3.8.3/schema.Pg b/etc/upgrade/3.8.3/schema.Pg
new file mode 100644 (file)
index 0000000..bbe5536
--- /dev/null
@@ -0,0 +1,3 @@
+
+CREATE UNIQUE INDEX GroupMembers1 ON GroupMembers(GroupId, MemberId);
+
diff --git a/etc/upgrade/3.8.4/content b/etc/upgrade/3.8.4/content
new file mode 100644 (file)
index 0000000..14ecba4
--- /dev/null
@@ -0,0 +1,59 @@
+
+@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;
+        }
+    },
+);
+
+
diff --git a/etc/upgrade/3.8.6/content b/etc/upgrade/3.8.6/content
new file mode 100644 (file)
index 0000000..a9793c6
--- /dev/null
@@ -0,0 +1,10 @@
+@Templates = (
+    {  Queue       => 0,
+       Name        => "Forward Ticket",    # loc
+       Description => "Heading of a forwarded Ticket", # loc
+       Content => q{
+
+This is a forward of ticket #{ $Ticket->id }
+}
+    },
+);
diff --git a/etc/upgrade/3.8.8/content b/etc/upgrade/3.8.8/content
new file mode 100644 (file)
index 0000000..cad77e9
--- /dev/null
@@ -0,0 +1,38 @@
+@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;
+        }
+    },
+);
+
diff --git a/etc/upgrade/3.8.9/content b/etc/upgrade/3.8.9/content
new file mode 100644 (file)
index 0000000..898c19e
--- /dev/null
@@ -0,0 +1,63 @@
+@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);
+            }
+        }
+    },
+);
diff --git a/etc/upgrade/3.9.1/content b/etc/upgrade/3.9.1/content
new file mode 100644 (file)
index 0000000..acdc0ad
--- /dev/null
@@ -0,0 +1,68 @@
+@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");
+            }
+        }
+    },
+);
+
diff --git a/etc/upgrade/3.9.2/content b/etc/upgrade/3.9.2/content
new file mode 100644 (file)
index 0000000..d0dbbfd
--- /dev/null
@@ -0,0 +1,48 @@
+@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();
+        }
+    },
+);
+
diff --git a/etc/upgrade/3.9.3/schema.Oracle b/etc/upgrade/3.9.3/schema.Oracle
new file mode 100644 (file)
index 0000000..4ee50c4
--- /dev/null
@@ -0,0 +1,2 @@
+ALTER TABLE ACL DROP COLUMN DelegatedBy;
+ALTER TABLE ACL DROP COLUMN DelegatedFrom;
diff --git a/etc/upgrade/3.9.3/schema.Pg b/etc/upgrade/3.9.3/schema.Pg
new file mode 100644 (file)
index 0000000..4ee50c4
--- /dev/null
@@ -0,0 +1,2 @@
+ALTER TABLE ACL DROP COLUMN DelegatedBy;
+ALTER TABLE ACL DROP COLUMN DelegatedFrom;
diff --git a/etc/upgrade/3.9.3/schema.SQLite b/etc/upgrade/3.9.3/schema.SQLite
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/etc/upgrade/3.9.3/schema.mysql b/etc/upgrade/3.9.3/schema.mysql
new file mode 100644 (file)
index 0000000..4ee50c4
--- /dev/null
@@ -0,0 +1,2 @@
+ALTER TABLE ACL DROP COLUMN DelegatedBy;
+ALTER TABLE ACL DROP COLUMN DelegatedFrom;
diff --git a/etc/upgrade/3.9.5/backcompat b/etc/upgrade/3.9.5/backcompat
new file mode 100644 (file)
index 0000000..611ab51
--- /dev/null
@@ -0,0 +1 @@
+RT::ACE                LastUpdated LastUpdatedBy Creator Created
diff --git a/etc/upgrade/3.9.5/schema.Oracle b/etc/upgrade/3.9.5/schema.Oracle
new file mode 100644 (file)
index 0000000..065776d
--- /dev/null
@@ -0,0 +1,20 @@
+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;
diff --git a/etc/upgrade/3.9.5/schema.Pg b/etc/upgrade/3.9.5/schema.Pg
new file mode 100644 (file)
index 0000000..cea2c44
--- /dev/null
@@ -0,0 +1,20 @@
+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;
diff --git a/etc/upgrade/3.9.5/schema.SQLite b/etc/upgrade/3.9.5/schema.SQLite
new file mode 100644 (file)
index 0000000..c23f89b
--- /dev/null
@@ -0,0 +1,19 @@
+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;
diff --git a/etc/upgrade/3.9.5/schema.mysql b/etc/upgrade/3.9.5/schema.mysql
new file mode 100644 (file)
index 0000000..fe5018c
--- /dev/null
@@ -0,0 +1,20 @@
+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;
diff --git a/etc/upgrade/3.9.6/schema.Oracle b/etc/upgrade/3.9.6/schema.Oracle
new file mode 100644 (file)
index 0000000..ecec972
--- /dev/null
@@ -0,0 +1 @@
+ALTER TABLE Tickets MODIFY Status VARCHAR2(64);
diff --git a/etc/upgrade/3.9.6/schema.Pg b/etc/upgrade/3.9.6/schema.Pg
new file mode 100644 (file)
index 0000000..f4f909e
--- /dev/null
@@ -0,0 +1 @@
+ALTER TABLE Tickets ALTER Status TYPE varchar(64);
diff --git a/etc/upgrade/3.9.6/schema.SQLite b/etc/upgrade/3.9.6/schema.SQLite
new file mode 100644 (file)
index 0000000..2e7b6d3
--- /dev/null
@@ -0,0 +1,69 @@
+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;
diff --git a/etc/upgrade/3.9.6/schema.mysql b/etc/upgrade/3.9.6/schema.mysql
new file mode 100644 (file)
index 0000000..b0a9eaf
--- /dev/null
@@ -0,0 +1 @@
+ALTER TABLE Tickets Modify Status varchar(64);
diff --git a/etc/upgrade/3.9.7/content b/etc/upgrade/3.9.7/content
new file mode 100644 (file)
index 0000000..504ddf1
--- /dev/null
@@ -0,0 +1,82 @@
+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;
+            }
+        }
+    },
+);
diff --git a/etc/upgrade/3.9.7/schema.Oracle b/etc/upgrade/3.9.7/schema.Oracle
new file mode 100644 (file)
index 0000000..3c75c91
--- /dev/null
@@ -0,0 +1,6 @@
+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;
diff --git a/etc/upgrade/3.9.7/schema.Pg b/etc/upgrade/3.9.7/schema.Pg
new file mode 100644 (file)
index 0000000..1704fa6
--- /dev/null
@@ -0,0 +1,6 @@
+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;
diff --git a/etc/upgrade/3.9.7/schema.SQLite b/etc/upgrade/3.9.7/schema.SQLite
new file mode 100644 (file)
index 0000000..1704fa6
--- /dev/null
@@ -0,0 +1,6 @@
+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;
diff --git a/etc/upgrade/3.9.7/schema.mysql b/etc/upgrade/3.9.7/schema.mysql
new file mode 100644 (file)
index 0000000..4cbed6c
--- /dev/null
@@ -0,0 +1,6 @@
+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;
diff --git a/etc/upgrade/3.9.8/content b/etc/upgrade/3.9.8/content
new file mode 100644 (file)
index 0000000..24242fd
--- /dev/null
@@ -0,0 +1,22 @@
+@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");
+    }
+};
diff --git a/etc/upgrade/3.9.8/schema.Oracle b/etc/upgrade/3.9.8/schema.Oracle
new file mode 100644 (file)
index 0000000..4f82373
--- /dev/null
@@ -0,0 +1,65 @@
+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
+);
diff --git a/etc/upgrade/3.9.8/schema.Pg b/etc/upgrade/3.9.8/schema.Pg
new file mode 100644 (file)
index 0000000..d12e27a
--- /dev/null
@@ -0,0 +1,62 @@
+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)
+);
+
diff --git a/etc/upgrade/3.9.8/schema.SQLite b/etc/upgrade/3.9.8/schema.SQLite
new file mode 100644 (file)
index 0000000..29ed7e8
--- /dev/null
@@ -0,0 +1,55 @@
+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
+);
diff --git a/etc/upgrade/3.9.8/schema.mysql b/etc/upgrade/3.9.8/schema.mysql
new file mode 100644 (file)
index 0000000..e7ed84d
--- /dev/null
@@ -0,0 +1,58 @@
+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;
diff --git a/etc/upgrade/4.0.0rc2/schema.mysql b/etc/upgrade/4.0.0rc2/schema.mysql
new file mode 100644 (file)
index 0000000..9431213
--- /dev/null
@@ -0,0 +1,10 @@
+
+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;
+
diff --git a/etc/upgrade/4.0.0rc4/schema.Oracle b/etc/upgrade/4.0.0rc4/schema.Oracle
new file mode 100644 (file)
index 0000000..0df9d65
--- /dev/null
@@ -0,0 +1 @@
+ALTER TABLE Users MODIFY Password VARCHAR2(256);
diff --git a/etc/upgrade/4.0.0rc4/schema.Pg b/etc/upgrade/4.0.0rc4/schema.Pg
new file mode 100644 (file)
index 0000000..7280810
--- /dev/null
@@ -0,0 +1 @@
+ALTER TABLE Users ALTER Password TYPE varchar(256);
diff --git a/etc/upgrade/4.0.0rc4/schema.mysql b/etc/upgrade/4.0.0rc4/schema.mysql
new file mode 100644 (file)
index 0000000..2f562bd
--- /dev/null
@@ -0,0 +1 @@
+ALTER TABLE Users MODIFY Password varchar(256);
diff --git a/etc/upgrade/4.0.0rc7/content b/etc/upgrade/4.0.0rc7/content
new file mode 100644 (file)
index 0000000..d0d210b
--- /dev/null
@@ -0,0 +1,21 @@
+@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;
+    },
+);
diff --git a/etc/upgrade/4.0.1/acl.Pg b/etc/upgrade/4.0.1/acl.Pg
new file mode 100644 (file)
index 0000000..6b0e7bb
--- /dev/null
@@ -0,0 +1,39 @@
+
+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;
diff --git a/etc/upgrade/4.0.1/content b/etc/upgrade/4.0.1/content
new file mode 100644 (file)
index 0000000..9b74ff1
--- /dev/null
@@ -0,0 +1,83 @@
+@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 );
+            }
+        }
+    },
+);
+
diff --git a/etc/upgrade/4.0.12/schema.Oracle b/etc/upgrade/4.0.12/schema.Oracle
new file mode 100644 (file)
index 0000000..4d2c375
--- /dev/null
@@ -0,0 +1 @@
+UPDATE Tickets SET Type = LOWER(Type) WHERE LOWER(Type) IN ('ticket', 'approval', 'reminder');
diff --git a/etc/upgrade/4.0.12/schema.Pg b/etc/upgrade/4.0.12/schema.Pg
new file mode 100644 (file)
index 0000000..4d2c375
--- /dev/null
@@ -0,0 +1 @@
+UPDATE Tickets SET Type = LOWER(Type) WHERE LOWER(Type) IN ('ticket', 'approval', 'reminder');
diff --git a/etc/upgrade/4.0.12/schema.mysql b/etc/upgrade/4.0.12/schema.mysql
new file mode 100644 (file)
index 0000000..4d2c375
--- /dev/null
@@ -0,0 +1 @@
+UPDATE Tickets SET Type = LOWER(Type) WHERE LOWER(Type) IN ('ticket', 'approval', 'reminder');
diff --git a/etc/upgrade/4.0.13/schema.Oracle b/etc/upgrade/4.0.13/schema.Oracle
new file mode 100644 (file)
index 0000000..6ab7020
--- /dev/null
@@ -0,0 +1,2 @@
+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';
diff --git a/etc/upgrade/4.0.13/schema.Pg b/etc/upgrade/4.0.13/schema.Pg
new file mode 100644 (file)
index 0000000..8283f52
--- /dev/null
@@ -0,0 +1,2 @@
+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';
diff --git a/etc/upgrade/4.0.13/schema.mysql b/etc/upgrade/4.0.13/schema.mysql
new file mode 100644 (file)
index 0000000..03b54b5
--- /dev/null
@@ -0,0 +1,2 @@
+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';
diff --git a/etc/upgrade/4.0.3/content b/etc/upgrade/4.0.3/content
new file mode 100644 (file)
index 0000000..3e06c89
--- /dev/null
@@ -0,0 +1,23 @@
+@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', },
+
+);
diff --git a/etc/upgrade/4.0.4/content b/etc/upgrade/4.0.4/content
new file mode 100644 (file)
index 0000000..fdfcb3e
--- /dev/null
@@ -0,0 +1,16 @@
+@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;
+        }
+    },
+);
+
diff --git a/etc/upgrade/4.0.6/content b/etc/upgrade/4.0.6/content
new file mode 100644 (file)
index 0000000..dc1a009
--- /dev/null
@@ -0,0 +1,17 @@
+@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/;
+        }
+    },
+);
diff --git a/etc/upgrade/4.0.6/schema.mysql b/etc/upgrade/4.0.6/schema.mysql
new file mode 100644 (file)
index 0000000..ab32007
--- /dev/null
@@ -0,0 +1 @@
+ALTER TABLE Attributes MODIFY Content LONGBLOB;
diff --git a/etc/upgrade/4.0.9/content b/etc/upgrade/4.0.9/content
new file mode 100644 (file)
index 0000000..f2abf62
--- /dev/null
@@ -0,0 +1,52 @@
+@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();
+        }
+    },
+);
diff --git a/etc/upgrade/generate-rtaddressregexp b/etc/upgrade/generate-rtaddressregexp
new file mode 100644 (file)
index 0000000..3e517c4
--- /dev/null
@@ -0,0 +1,109 @@
+#!/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||''}++;
+    }
+}
diff --git a/etc/upgrade/sanity-check-stylesheets.pl b/etc/upgrade/sanity-check-stylesheets.pl
new file mode 100644 (file)
index 0000000..6ae1cc6
--- /dev/null
@@ -0,0 +1,87 @@
+# 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);
+    }
+}
diff --git a/etc/upgrade/shrink_cgm_table.pl b/etc/upgrade/shrink_cgm_table.pl
new file mode 100644 (file)
index 0000000..bb6c8d4
--- /dev/null
@@ -0,0 +1,124 @@
+#!/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;
+}
+
diff --git a/etc/upgrade/shrink_transactions_table.pl b/etc/upgrade/shrink_transactions_table.pl
new file mode 100644 (file)
index 0000000..b4f07f0
--- /dev/null
@@ -0,0 +1,124 @@
+#!/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;
+}
+
diff --git a/etc/upgrade/split-out-cf-categories b/etc/upgrade/split-out-cf-categories
new file mode 100644 (file)
index 0000000..f2751bb
--- /dev/null
@@ -0,0 +1,171 @@
+#!/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;
diff --git a/etc/upgrade/upgrade-articles b/etc/upgrade/upgrade-articles
new file mode 100644 (file)
index 0000000..ed347e8
--- /dev/null
@@ -0,0 +1,262 @@
+#!/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";
+    }
+}
diff --git a/etc/upgrade/upgrade-mysql-schema.pl b/etc/upgrade/upgrade-mysql-schema.pl
new file mode 100644 (file)
index 0000000..98eb7b4
--- /dev/null
@@ -0,0 +1,463 @@
+#!/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));
+}
+
diff --git a/etc/upgrade/vulnerable-passwords b/etc/upgrade/vulnerable-passwords
new file mode 100644 (file)
index 0000000..6a904c8
--- /dev/null
@@ -0,0 +1,142 @@
+#!/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;
+}
index ab0f307..0ff7e6d 100644 (file)
@@ -382,7 +382,7 @@ sub AddAttachments {
 
 =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
@@ -397,14 +397,15 @@ sub AddAttachment {
               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 . "/"
index a0edb15..4350df0 100644 (file)
@@ -148,7 +148,7 @@ sub Create {
                  @_);
 
     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'));
index 9c18c1a..997e376 100644 (file)
@@ -210,7 +210,10 @@ sub LimitToObject {
     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);
 
 }
index 60122c7..8949d9b 100644 (file)
@@ -54,6 +54,15 @@ use base 'RT::SearchBuilder';
 
 sub Table {'Classes'}
 
+=head2 _Init
+
+=cut
+
+ sub _Init {
+    my $self = shift;
+    $self->{'with_disabled_column'} = 1;
+    return ($self->SUPER::_Init(@_));
+ }
 
 =head2 Next
 
index e06945a..b1319c0 100644 (file)
@@ -1220,6 +1220,7 @@ sub SetFromConfig {
             $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
index a69c6f4..0211815 100644 (file)
@@ -51,7 +51,7 @@ package RT::CustomField;
 use strict;
 use warnings;
 
-
+use Scalar::Util 'blessed';
 
 use base 'RT::Record';
 
@@ -1205,14 +1205,57 @@ sub FriendlyLookupType {
     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;
index feeeadb..34722bb 100644 (file)
@@ -53,10 +53,42 @@ use warnings;
 
 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;
 
index 017018e..1ea75da 100644 (file)
@@ -141,6 +141,25 @@ sub LimitToParentType  {
     $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
 
@@ -155,19 +174,11 @@ sub 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 {
index d4a261e..2644575 100644 (file)
@@ -110,7 +110,7 @@ sub SmartParseMIMEEntityFromScalar {
             # 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) {
index a4295c5..408300e 100644 (file)
@@ -50,7 +50,7 @@ package RT;
 use warnings;
 use strict;
 
-our $VERSION = '4.0.13';
+our $VERSION = '4.0.17';
 
 
 
index b449d20..ca6f2e4 100644 (file)
@@ -114,6 +114,7 @@ sub Connect {
     $self->SUPER::Connect(
         User => RT->Config->Get('DatabaseUser'),
         Password => RT->Config->Get('DatabasePassword'),
+        DisconnectHandleOnDestroy => 1,
         %args,
     );
 
@@ -161,7 +162,6 @@ sub BuildDSN {
         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'};
@@ -1203,6 +1203,32 @@ sub _LogSQLStatement {
     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();
index 56ab06e..f259a76 100644 (file)
@@ -431,21 +431,24 @@ sub SendEmail {
         # 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;
index 5f8ff99..3a85c2d 100644 (file)
@@ -52,7 +52,7 @@ use warnings;
 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;
@@ -296,6 +296,45 @@ sub vsplit {
     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;
index f6c04c8..a7996a8 100644 (file)
@@ -1755,7 +1755,7 @@ sub CreateTicket {
         $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;
@@ -1988,7 +1988,8 @@ sub ProcessUpdateMessage {
 
     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'} ) {
@@ -2591,18 +2592,23 @@ sub ProcessTicketReminders {
         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 '' ) {
@@ -2612,7 +2618,8 @@ sub ProcessTicketReminders {
                     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);
                 }
             }
         }
@@ -3143,7 +3150,7 @@ sub GetColumnMapEntry {
     }
 
     # 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';
index 4edd9bd..266f9ed 100644 (file)
@@ -192,7 +192,7 @@ sub _ClearOldDB {
         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;
 }
 
@@ -222,15 +222,53 @@ sub _ClearOldDir {
             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
@@ -243,6 +281,7 @@ sub ClearByUser {
     my $class = $self->Class;
     my $attrs = $self->Attributes;
 
+    my $deleted;
     my %seen = ();
     foreach my $id( @{ $self->Ids } ) {
         my %session;
@@ -259,8 +298,10 @@ sub ClearByUser {
             }
         }
         tied(%session)->delete;
-        $RT::Logger->info("successfuly deleted session '$id'");
+        $RT::Logger->info("successfully deleted session '$id'");
+        $deleted++;
     }
+    $self->ClearOrphanLockFiles if $deleted;
 }
 
 sub TIEHASH {
@@ -276,10 +317,8 @@ 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"
           . $@;
     }
 
index c905282..3731cdb 100644 (file)
@@ -707,11 +707,11 @@ sub FillCache {
         }
 
         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;
@@ -767,7 +767,7 @@ sub FillCache {
 
     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;
index 6896063..4cde8d6 100644 (file)
@@ -54,6 +54,10 @@ use base 'Pod::Simple::XHTML';
 
 use HTML::Entities qw//;
 
+__PACKAGE__->_accessorize(
+    "batch"
+);
+
 sub new {
     my $self = shift->SUPER::new(@_);
     $self->index(1);
@@ -118,27 +122,31 @@ sub resolve_local_link {
     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;
@@ -146,11 +154,20 @@ sub resolve_local_link {
 
     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;
index f41a43a..1c63dcb 100644 (file)
@@ -57,6 +57,8 @@ use List::MoreUtils qw/all/;
 use RT::Pod::Search;
 use RT::Pod::HTML;
 
+my $MOD2PATH;
+
 sub new {
     my $self = shift->SUPER::new(@_);
     $self->verbose(0);
@@ -84,7 +86,7 @@ sub classify {
         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;
@@ -176,4 +178,15 @@ sub esc {
     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;
index 9cf8cbb..d0a939a 100644 (file)
@@ -78,6 +78,7 @@ sub Table { 'Principals'}
 
 sub _Init {
     my $self = shift;
+    $self->{'with_disabled_column'} = 1;
     return ( $self->SUPER::_Init(@_) );
 }
 
index 6f28120..5b14fd7 100644 (file)
@@ -783,7 +783,7 @@ sub _EncodeLOB {
                                    . 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 );
             }
         }
 
@@ -1304,7 +1304,7 @@ sub _AddLink {
 
     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();
@@ -1382,7 +1382,7 @@ sub _DeleteLink {
 
     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();
@@ -1616,7 +1616,7 @@ Returns the path RT uses to figure out which custom fields apply to this object.
 
 sub CustomFieldLookupType {
     my $self = shift;
-    return ref($self);
+    return ref($self) || $self;
 }
 
 
@@ -1719,7 +1719,7 @@ sub _AddCustomFieldValue {
             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;
             }
index 42f4e1d..be716a0 100644 (file)
@@ -122,7 +122,7 @@ sub Add {
         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") );
     }
 
@@ -134,6 +134,7 @@ sub Add {
         RefersTo => $self->Ticket,
         Type => 'reminder',
         Queue => $self->TicketObj->Queue,
+        Status => $self->TicketObj->QueueObj->Lifecycle->ReminderStatusOnOpen,
     );
     $self->TicketObj->_NewTransaction(
         Type => 'AddReminder',
index b73bbaa..d811e03 100644 (file)
@@ -111,7 +111,7 @@ sub Groupings {
 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 );
@@ -239,7 +239,7 @@ sub _FieldToFunction {
             $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);
index f8465f0..1c678d3 100644 (file)
@@ -197,6 +197,7 @@ our @GUESS = (
     [ 10 => sub { return "subject" if $_[1] } ],
     [ 20 => sub { return "id" if /^#?\d+$/ } ],
     [ 30 => sub { return "requestor" if /\w+@\w+/} ],
+    [ 35 => sub { return "domain" if /^@\w+/} ],
     [ 40 => sub {
           return "status" if RT::Queue->new( $_[2] )->IsValidStatus( $_ )
       }],
@@ -260,6 +261,7 @@ sub HandleWatcher     {
     return watcher => (!$_[2] and $_[1] eq "me") ? "Watcher.id = '__CurrentUser__'" : "Watcher = '$_[1]'";
 }
 sub HandleRequestor { return requestor => "Requestor STARTSWITH '$_[1]'";  }
+sub HandleDomain    { $_[1] =~ s/^@?/@/; return requestor => "Requestor ENDSWITH '$_[1]'";  }
 sub HandleQueue     { return queue     => "Queue = '$_[1]'";      }
 sub HandleQ         { return queue     => "Queue = '$_[1]'";      }
 sub HandleCf        { return "cf.$_[3]" => "'CF.{$_[3]}' LIKE '$_[1]'"; }
index 0ace421..51b894f 100644 (file)
@@ -86,9 +86,13 @@ sub _Init  {
     $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(@_);
 }
 
index ad0cf77..5acdb31 100644 (file)
@@ -634,7 +634,7 @@ sub Create {
                 }
             }
 
-            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;
@@ -2591,7 +2591,7 @@ sub AddLink {
     }
 
     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);
 }
index 7331f1f..480b8b3 100644 (file)
@@ -142,9 +142,9 @@ our %FIELD_METADATA = (
     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
@@ -943,13 +943,18 @@ sub _WatcherLimit {
     }
     $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(
@@ -987,7 +992,7 @@ sub _WatcherLimit {
         $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;
@@ -1012,7 +1017,7 @@ sub _WatcherLimit {
                 VALUE      => "$group_members.MemberId",
                 QUOTEVALUE => 0,
             );
-            my $users = $self->Join(
+            $users ||= $self->Join(
                 TYPE            => 'LEFT',
                 ALIAS1          => $group_members,
                 FIELD1          => 'MemberId',
@@ -1038,10 +1043,10 @@ sub _WatcherLimit {
     } 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',
@@ -1058,6 +1063,7 @@ sub _WatcherLimit {
         );
     }
     $self->_CloseParen;
+    return ($groups, $group_members, $users);
 }
 
 sub _RoleGroupsJoin {
@@ -1308,33 +1314,44 @@ sub _WatcherMembershipLimit {
 
 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
@@ -1347,9 +1364,11 @@ sub _CustomFieldDecipher {
     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
@@ -1358,8 +1377,14 @@ Factor out the Join of custom fields so we can use it for sorting too
 
 =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} )
@@ -1368,17 +1393,21 @@ sub _CustomFieldJoin {
                  $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'
@@ -1410,7 +1439,7 @@ sub _CustomFieldJoin {
             LEFTJOIN        => $CFs,
             ENTRYAGGREGATOR => 'AND',
             FIELD           => 'LookupType',
-            VALUE           => 'RT::Queue-RT::Ticket',
+            VALUE           => $type,
         );
         $self->SUPER::Limit(
             LEFTJOIN        => $CFs,
@@ -1419,7 +1448,7 @@ sub _CustomFieldJoin {
             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',
@@ -1427,28 +1456,29 @@ sub _CustomFieldJoin {
             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
@@ -1467,12 +1497,16 @@ use Regexp::Common::net::CIDR;
 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
@@ -1568,16 +1602,16 @@ sub _CustomFieldLimit {
 
     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,
@@ -1600,11 +1634,11 @@ sub _CustomFieldLimit {
         $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',
             ); 
@@ -1612,20 +1646,20 @@ sub _CustomFieldLimit {
             # 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',
             );  
@@ -1637,7 +1671,7 @@ sub _CustomFieldLimit {
     } 
     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;
 
@@ -1648,7 +1682,7 @@ sub _CustomFieldLimit {
         # otherwise search in Content and in LargeContent
         if ( $column ) {
             $self->_SQLLimit( $fix_op->(
-                ALIAS      => $TicketCFs,
+                ALIAS      => $ObjectCFs,
                 FIELD      => $column,
                 OPERATOR   => $op,
                 VALUE      => $value,
@@ -1674,7 +1708,7 @@ sub _CustomFieldLimit {
                     $self->_OpenParen;
 
                     $self->_SQLLimit(
-                        ALIAS    => $TicketCFs,
+                        ALIAS    => $ObjectCFs,
                         FIELD    => 'Content',
                         OPERATOR => ">=",
                         VALUE    => $daystart,
@@ -1682,7 +1716,7 @@ sub _CustomFieldLimit {
                     );
 
                     $self->_SQLLimit(
-                        ALIAS    => $TicketCFs,
+                        ALIAS    => $ObjectCFs,
                         FIELD    => 'Content',
                         OPERATOR => "<",
                         VALUE    => $dayend,
@@ -1695,7 +1729,7 @@ sub _CustomFieldLimit {
             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,
@@ -1706,14 +1740,14 @@ sub _CustomFieldLimit {
                 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',
@@ -1721,7 +1755,7 @@ sub _CustomFieldLimit {
                     );
                     $self->_CloseParen;
                     $self->_SQLLimit( $fix_op->(
-                        ALIAS           => $TicketCFs,
+                        ALIAS           => $ObjectCFs,
                         FIELD           => 'LargeContent',
                         OPERATOR        => $op,
                         VALUE           => $value,
@@ -1732,7 +1766,7 @@ sub _CustomFieldLimit {
             }
             else {
                 $self->_SQLLimit(
-                    ALIAS    => $TicketCFs,
+                    ALIAS    => $ObjectCFs,
                     FIELD    => 'Content',
                     OPERATOR => $op,
                     VALUE    => $value,
@@ -1743,14 +1777,14 @@ sub _CustomFieldLimit {
                 $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',
@@ -1758,7 +1792,7 @@ sub _CustomFieldLimit {
                 );
                 $self->_CloseParen;
                 $self->_SQLLimit( $fix_op->(
-                    ALIAS           => $TicketCFs,
+                    ALIAS           => $ObjectCFs,
                     FIELD           => 'LargeContent',
                     OPERATOR        => $op,
                     VALUE           => $value,
@@ -1792,7 +1826,7 @@ sub _CustomFieldLimit {
 
             if ($negative_op) {
                 $self->_SQLLimit(
-                    ALIAS           => $TicketCFs,
+                    ALIAS           => $ObjectCFs,
                     FIELD           => $column || 'Content',
                     OPERATOR        => 'IS',
                     VALUE           => 'NULL',
@@ -1806,7 +1840,7 @@ sub _CustomFieldLimit {
     }
     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;
@@ -1815,8 +1849,8 @@ sub _CustomFieldLimit {
         # 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,
@@ -1825,8 +1859,8 @@ sub _CustomFieldLimit {
         }
         else {
             $self->SUPER::Limit(
-                LEFTJOIN   => $TicketCFs,
-                ALIAS      => $TicketCFs,
+                LEFTJOIN   => $ObjectCFs,
+                ALIAS      => $ObjectCFs,
                 FIELD      => 'Content',
                 OPERATOR   => $op,
                 VALUE      => $value,
@@ -1835,7 +1869,7 @@ sub _CustomFieldLimit {
         }
         $self->_SQLLimit(
             %rest,
-            ALIAS      => $TicketCFs,
+            ALIAS      => $ObjectCFs,
             FIELD      => 'id',
             OPERATOR   => 'IS',
             VALUE      => 'NULL',
@@ -1946,10 +1980,10 @@ sub OrderByCols {
             }
             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,
@@ -1971,7 +2005,7 @@ sub OrderByCols {
            }
            my $CFvs = $self->Join(
                TYPE   => 'LEFT',
-               ALIAS1 => $TicketCFs,
+               ALIAS1 => $ObjectCFs,
                FIELD1 => 'CustomField',
                TABLE2 => 'CustomFieldValues',
                FIELD2 => 'CustomField',
@@ -1980,12 +2014,12 @@ sub OrderByCols {
                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";
index 608862a..cd18cb1 100644 (file)
@@ -171,19 +171,69 @@ sub _parser {
     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 =~ /^([^\.]+)\.(.+)$/;
@@ -225,10 +275,28 @@ sub _parser {
         }
         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;
@@ -238,6 +306,29 @@ sub _parser {
     $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
@@ -292,8 +383,9 @@ sub FromSQL {
     $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.
index da766c0..28c8d9e 100644 (file)
@@ -363,24 +363,9 @@ sub Content {
     }
 
     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";
     }
 
@@ -400,6 +385,84 @@ sub QuoteHeader {
     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
 
index c0958ca..ff19365 100644 (file)
@@ -199,6 +199,8 @@ sub _GetResolver {
     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); 
     }
 
index 38c3c20..ee63e3a 100644 (file)
@@ -65,8 +65,11 @@ sub safe_run_child (&) {
     # 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);
@@ -90,8 +93,8 @@ sub safe_run_child (&) {
         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";
@@ -104,8 +107,8 @@ sub safe_run_child (&) {
     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];
 }
 
index ad81f71..0097f14 100755 (executable)
@@ -95,6 +95,7 @@ sub usage {
     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' ) {
@@ -105,10 +106,11 @@ sub usage {
     }
 }
 
-my ( $frequency, $print, $help ) = ( '', '', '' );
+my ( $frequency, $print, $verbose, $help ) = ( '', '', '', '' );
 GetOptions(
     'mode=s' => \$frequency,
     'print'  => \$print,
+    'verbose' => \$verbose,
     'help'   => \$help,
 );
 
@@ -134,7 +136,7 @@ sub run {
         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";
index 85b49c3..da1c546 100755 (executable)
@@ -375,9 +375,9 @@ sub process_pg {
     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;
         }
index 54d84f1..de2e9a3 100755 (executable)
@@ -83,12 +83,13 @@ $| = 1; # unbuffer all output.
 
 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',
 );
@@ -164,6 +165,7 @@ foreach my $key(qw(Type Host Name User Password)) {
 
 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') || '';
@@ -189,8 +191,11 @@ if ($args{'skip-create'}) {
     }
 }
 
+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 ) {
@@ -218,7 +223,7 @@ sub action_drop {
     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
@@ -306,18 +311,17 @@ sub action_upgrade {
     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")
@@ -345,14 +349,14 @@ sub action_upgrade {
             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 (
@@ -408,9 +412,12 @@ sub get_versions_from_to {
     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,
@@ -427,7 +434,7 @@ sub error {
 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: ";
index a3a6492..2463b5f 100644 (file)
@@ -57,7 +57,7 @@ jQuery(function(){
             jQuery(event.target).val(ui.item.value);
             jQuery(event.target).closest("form").submit();
         }
-    });
+    }).addClass("autocompletes-user");
 });
 </script>
 % }
index de9a55d..54eae16 100644 (file)
@@ -70,7 +70,7 @@ jQuery(function(){
             form.find('input[name=UserOp]').val('=');
             form.submit();
         }
-    });
+    }).addClass("autocompletes-user");
 });
 </script>
 </form>
index d1060e6..28b0048 100644 (file)
@@ -46,7 +46,7 @@
 %#
 %# 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();
index 7e4ba15..33e2de2 100644 (file)
@@ -68,9 +68,10 @@ if ( $Rows ) {
 # 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 )
index 330aced..0022ba2 100644 (file)
@@ -145,7 +145,7 @@ my $COLUMN_MAP = {
             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;
index c74bfd0..3222554 100644 (file)
@@ -47,7 +47,7 @@
 %# 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" />
index 06637ee..8b68b61 100644 (file)
@@ -68,7 +68,7 @@ my $LinkCallback = sub {
             \'<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 }
     }
index 2c4ea7e..7dda16e 100644 (file)
@@ -54,7 +54,7 @@
 <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>
 %     }
index 8bdbd8a..7d7df64 100644 (file)
@@ -125,10 +125,7 @@ foreach ( $SearchArg, $ProcessedSearchArg ) {
     $_->{'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
index addaeee..48fef90 100644 (file)
@@ -55,7 +55,7 @@ $request_path =~ s!/{2,}!/!g;
 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;
 };
 
index 73d9966..0a17de8 100644 (file)
@@ -2,7 +2,7 @@
 %#
 %# 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)
index fd683a4..9de0ded 100644 (file)
@@ -2,7 +2,7 @@
 %#
 %# 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)
index 8d9e8e0..d0601ec 100644 (file)
@@ -1,50 +1,3 @@
-%# 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
index 3c3f5fe..a62d08e 100644 (file)
@@ -105,6 +105,22 @@ jQuery(function() {
         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 );
+            };
     }
 });
index 8867bf9..8a7a524 100644 (file)
@@ -156,7 +156,7 @@ if (%data == 0) {
 }
 else {
     my ($get, $set, $key, $val, $n, $s);
-
+    my $updated;
     foreach $key (keys %data) {
         $val = $data{$key};
         $key = lc $key;
@@ -192,9 +192,12 @@ else {
                 $k = $changes;
             }
         }
+        else {
+            $updated ||= 1;
+        }
     }
 
-    push(@comments, "# Group $id updated.") unless $n == 0;
+    push(@comments, "# Group $id updated.") if $updated;
 }
 
 DONE:
index 58bb899..9aa42f8 100644 (file)
@@ -146,7 +146,7 @@ if ( keys %data == 0) {
 }
 else {
     my ($get, $set, $key, $val, $n, $s);
-
+    my $updated;
     foreach $key (keys %data) {
         $val = $data{$key};
         $key = lc $key;
@@ -175,9 +175,12 @@ else {
                 $k = $changes;
             }
         }
+        else {
+            $updated ||= 1;
+        }
     }
 
-    push(@comments, "# Queue $id updated.") unless $n == 0;
+    push(@comments, "# Queue $id updated.") if $updated;
 }
 
 DONE:
index b50135f..d00a949 100644 (file)
@@ -55,8 +55,6 @@ $id
 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)]}.");
@@ -89,44 +87,23 @@ if (!$changes{Text} && @atts == 0) {
     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') ||
@@ -154,6 +131,5 @@ if ($changes{Status}) {
 
 OUTPUT:
 
-unlink @tmp_files;
 return [ $c, $o, $k, $e ];
 </%INIT>
index 0bced1e..f6b4d84 100644 (file)
@@ -67,7 +67,7 @@ my @dates  = qw(Created Starts Started Due Resolved Told LastUpdated);
 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;
@@ -82,7 +82,7 @@ if ($id ne 'new') {
         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 ];
             }
@@ -110,7 +110,7 @@ else {
         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,
@@ -126,6 +126,7 @@ else {
                 TimeEstimated    => 0,
                 Starts           => $starts->ISO,
                 Due              => $due->ISO,
+                Attachment       => '',
                 Text             => "",
             },
             0
@@ -134,7 +135,7 @@ else {
     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
@@ -167,6 +168,9 @@ else {
             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;
@@ -183,14 +187,24 @@ else {
         # people fields allow multiple values
         $v{$_} = vsplit($v{$_}) foreach ( grep $create{lc $_}, @people );
 
-        if ($text) {
+        if ($text || @atts) {
             $v{MIMEObj} =
                 MIME::Entity->build(
+                    Type => "multipart/mixed",
                     From => $session{CurrentUser}->EmailAddress,
                     Subject => $v{Subject},
-                    Data => $text,
                     'X-RT-Interface' => 'REST',
                 );
+            $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);    
@@ -279,6 +293,7 @@ if (!keys(%data)) {
 }
 else {
     my ($get, $set, $key, $val, $n, $s);
+    my $updated;
 
     foreach $key (keys %data) {
         $val = $data{$key};
@@ -416,14 +431,14 @@ else {
                             $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;
    &nb