Master to 4.2.8
authorMikal Kolbein Gule <m.k.gule@usit.uio.no>
Thu, 2 Oct 2014 21:36:46 +0000 (23:36 +0200)
committerMikal Kolbein Gule <m.k.gule@usit.uio.no>
Thu, 2 Oct 2014 21:36:46 +0000 (23:36 +0200)
367 files changed:
bin/rt
bin/rt-mailgate
docs/README
docs/UPGRADING-4.0
docs/UPGRADING-4.2
docs/authentication.pod
docs/backups.pod
docs/customizing/approvals.pod
docs/customizing/lifecycles.pod
docs/customizing/search_result_columns.pod [new file with mode: 0644]
docs/customizing/templates.pod
docs/extending/clickable_links.pod
docs/extending/external_custom_fields.pod
docs/full_text_indexing.pod
docs/hacking.pod
docs/initialdata.pod
docs/reminders.pod
docs/schema.dot
docs/web_deployment.pod
docs/writing_extensions.pod [new file with mode: 0644]
etc/RT_Config.pm
etc/acl.Pg
etc/initialdata
etc/schema.mysql
etc/upgrade/3.8.9/content
etc/upgrade/3.9.3/schema.Oracle
etc/upgrade/3.9.3/schema.Pg
etc/upgrade/3.9.3/schema.mysql
etc/upgrade/3.9.5/schema.Oracle
etc/upgrade/3.9.5/schema.Pg
etc/upgrade/3.9.5/schema.mysql
etc/upgrade/3.9.7/schema.Oracle
etc/upgrade/3.9.7/schema.Pg
etc/upgrade/3.9.7/schema.mysql
etc/upgrade/3.9.8/schema.Pg
etc/upgrade/3.9.8/schema.SQLite
etc/upgrade/3.9.8/schema.mysql
etc/upgrade/4.0-customfield-checkbox-extension [new file with mode: 0644]
etc/upgrade/4.1.1/schema.Oracle
etc/upgrade/4.1.1/schema.Pg
etc/upgrade/4.1.1/schema.SQLite
etc/upgrade/4.1.1/schema.mysql
etc/upgrade/4.1.14/schema.Oracle
etc/upgrade/4.1.14/schema.Pg
etc/upgrade/4.1.14/schema.mysql
etc/upgrade/4.1.17/content
etc/upgrade/4.1.19/schema.Oracle
etc/upgrade/4.1.19/schema.Pg
etc/upgrade/4.1.19/schema.mysql
etc/upgrade/4.1.20/content
etc/upgrade/4.1.5/content
etc/upgrade/4.2.4/content [new file with mode: 0644]
etc/upgrade/4.2.6/content [new file with mode: 0644]
etc/upgrade/4.2.6/schema.mysql [new file with mode: 0644]
etc/upgrade/4.2.7/content [new file with mode: 0644]
etc/upgrade/4.2.8/content [new file with mode: 0644]
etc/upgrade/shrink_transactions_table.pl
etc/upgrade/time-worked-history.pl
lib/RT.pm
lib/RT/Action/CreateTickets.pm
lib/RT/Action/EscalatePriority.pm
lib/RT/Action/LinearEscalate.pm
lib/RT/Action/NotifyOwnerOrAdminCc.pm [new file with mode: 0644]
lib/RT/Action/SendEmail.pm
lib/RT/Action/SendForward.pm
lib/RT/Articles.pm
lib/RT/Attachment.pm
lib/RT/Attribute.pm
lib/RT/Base.pm
lib/RT/Condition/BeforeDue.pm
lib/RT/Condition/Overdue.pm
lib/RT/Config.pm
lib/RT/Crypt.pm
lib/RT/Crypt/GnuPG.pm
lib/RT/Crypt/SMIME.pm
lib/RT/CurrentUser.pm
lib/RT/CustomField.pm
lib/RT/CustomFieldValues/External.pm
lib/RT/CustomFields.pm
lib/RT/Dashboard/Mailer.pm
lib/RT/Date.pm
lib/RT/EmailParser.pm
lib/RT/Generated.pm
lib/RT/Graph/Tickets.pm
lib/RT/Handle.pm
lib/RT/I18N.pm
lib/RT/I18N/cs.pm
lib/RT/I18N/fr.pm
lib/RT/I18N/ru.pm
lib/RT/Interface/CLI.pm
lib/RT/Interface/Email.pm
lib/RT/Interface/Email/Auth/Crypt.pm
lib/RT/Interface/REST.pm
lib/RT/Interface/Web.pm
lib/RT/Interface/Web/Handler.pm
lib/RT/Interface/Web/Middleware/StaticHeaders.pm [new file with mode: 0644]
lib/RT/Interface/Web/Session.pm
lib/RT/Lifecycle.pm
lib/RT/Migrate/Importer.pm
lib/RT/Migrate/Importer/File.pm
lib/RT/Migrate/Incremental.pm
lib/RT/Migrate/Serializer.pm
lib/RT/ObjectCustomFieldValue.pm
lib/RT/ObjectCustomFieldValues.pm
lib/RT/ObjectScrip.pm
lib/RT/PlackRunner.pm
lib/RT/Queue.pm
lib/RT/Record.pm
lib/RT/Record/Role/Roles.pm
lib/RT/Record/Role/Status.pm
lib/RT/Report/Tickets.pm
lib/RT/Ruleset.pm
lib/RT/SQL.pm
lib/RT/Scrip.pm
lib/RT/ScripAction.pm
lib/RT/ScripCondition.pm
lib/RT/Search/Simple.pm
lib/RT/SearchBuilder.pm
lib/RT/SearchBuilder/AddAndSort.pm
lib/RT/Shredder.pm
lib/RT/Shredder/ACE.pm
lib/RT/Shredder/Attachment.pm
lib/RT/Shredder/CachedGroupMember.pm
lib/RT/Shredder/CustomField.pm
lib/RT/Shredder/CustomFieldValue.pm
lib/RT/Shredder/Group.pm
lib/RT/Shredder/GroupMember.pm
lib/RT/Shredder/Link.pm
lib/RT/Shredder/ObjectCustomFieldValue.pm
lib/RT/Shredder/POD.pm
lib/RT/Shredder/Plugin.pm
lib/RT/Shredder/Plugin/Attachments.pm
lib/RT/Shredder/Plugin/Base.pm
lib/RT/Shredder/Plugin/SQLDump.pm
lib/RT/Shredder/Plugin/Summary.pm
lib/RT/Shredder/Plugin/Users.pm
lib/RT/Shredder/Principal.pm
lib/RT/Shredder/Queue.pm
lib/RT/Shredder/Record.pm
lib/RT/Shredder/Scrip.pm
lib/RT/Shredder/ScripAction.pm
lib/RT/Shredder/ScripCondition.pm
lib/RT/Shredder/Template.pm
lib/RT/Shredder/Ticket.pm
lib/RT/Shredder/Transaction.pm
lib/RT/Shredder/User.pm
lib/RT/Squish/JS.pm
lib/RT/StyleGuide.pod
lib/RT/Template.pm
lib/RT/Test.pm
lib/RT/Test/GnuPG.pm
lib/RT/Test/Web.pm
lib/RT/Ticket.pm
lib/RT/Tickets.pm
lib/RT/Transaction.pm
lib/RT/URI/fsck_com_article.pm
lib/RT/User.pm
lib/RT/Users.pm
lib/RT/Util.pm
sbin/rt-dump-metadata
sbin/rt-email-digest
sbin/rt-fulltext-indexer
sbin/rt-serializer
sbin/rt-server
sbin/rt-server.fcgi
sbin/rt-setup-database
sbin/rt-setup-fulltext-index
sbin/rt-test-dependencies
sbin/rt-validate-aliases
sbin/rt-validator
sbin/standalone_httpd
share/html/Admin/Articles/Classes/Modify.html
share/html/Admin/Articles/Classes/Objects.html
share/html/Admin/Articles/Classes/index.html
share/html/Admin/CustomFields/Modify.html
share/html/Admin/CustomFields/Objects.html
share/html/Admin/CustomFields/index.html
share/html/Admin/Elements/EditCustomFieldValuesSource
share/html/Admin/Elements/EditCustomFields
share/html/Admin/Elements/EditQueueWatchers
share/html/Admin/Elements/EditRights
share/html/Admin/Elements/EditScrips
share/html/Admin/Elements/EditTemplates
share/html/Admin/Elements/MembershipsPage
share/html/Admin/Elements/Portal
share/html/Admin/Elements/SelectStage
share/html/Admin/Groups/index.html
share/html/Admin/Queues/Modify.html
share/html/Admin/Queues/index.html
share/html/Admin/Scrips/Create.html
share/html/Admin/Scrips/Elements/SelectTemplate
share/html/Admin/Scrips/Modify.html
share/html/Admin/Scrips/Objects.html
share/html/Admin/Scrips/index.html
share/html/Admin/Tools/Configuration.html
share/html/Admin/Tools/Theme.html
share/html/Admin/Users/Keys.html
share/html/Admin/Users/Modify.html
share/html/Admin/Users/index.html
share/html/Approvals/Display.html
share/html/Approvals/Elements/Approve
share/html/Approvals/Elements/PendingMyApproval
share/html/Approvals/Elements/ShowDependency
share/html/Approvals/index.html
share/html/Articles/Article/History.html
share/html/Articles/Elements/NeedsSetup
share/html/Articles/Elements/ShowTopicLink
share/html/Dashboards/Queries.html
share/html/Elements/AddLinks
share/html/Elements/BulkCustomFields
share/html/Elements/BulkLinks
share/html/Elements/CollectionAsTable/Header
share/html/Elements/CollectionAsTable/ParseFormat
share/html/Elements/EditCustomField
share/html/Elements/EditCustomFieldAutocomplete
share/html/Elements/EditCustomFieldBinary
share/html/Elements/EditCustomFieldCombobox
share/html/Elements/EditCustomFieldCustomGroupings
share/html/Elements/EditCustomFieldDate
share/html/Elements/EditCustomFieldDateTime
share/html/Elements/EditCustomFieldFreeform
share/html/Elements/EditCustomFieldImage
share/html/Elements/EditCustomFieldSelect
share/html/Elements/EditCustomFieldText
share/html/Elements/EditCustomFieldWikitext
share/html/Elements/EditCustomFields
share/html/Elements/Error
share/html/Elements/Header
share/html/Elements/HeaderJavascript
share/html/Elements/Login
share/html/Elements/Logo
share/html/Elements/MakeClicky
share/html/Elements/MessageBox
share/html/Elements/MyRT
share/html/Elements/RT__Class/ColumnMap
share/html/Elements/RT__CustomField/ColumnMap
share/html/Elements/RT__Group/ColumnMap
share/html/Elements/RT__Queue/ColumnMap
share/html/Elements/RT__Scrip/ColumnMap
share/html/Elements/RT__Ticket/ColumnMap
share/html/Elements/RT__User/ColumnMap
share/html/Elements/Refresh
share/html/Elements/SelectOwnerDropdown
share/html/Elements/ShowCustomFieldCustomGroupings
share/html/Elements/ShowCustomFields
share/html/Elements/ShowHistory
share/html/Elements/ShowLink
share/html/Elements/ShowLinks
share/html/Elements/ShowPrincipal
share/html/Elements/ShowReminders
share/html/Elements/ShowTransaction
share/html/Elements/ShowTransactionAttachments
share/html/Elements/SimpleSearch
share/html/Elements/TSVExport [new file with mode: 0644]
share/html/Elements/Tabs
share/html/Elements/ValidateCustomFields
share/html/Helpers/Autocomplete/Groups
share/html/Helpers/Autocomplete/Owners
share/html/Helpers/Autocomplete/Tickets
share/html/Helpers/Autocomplete/Users
share/html/Helpers/TicketHistory
share/html/Install/DatabaseDetails.html
share/html/NoAuth/iCal/dhandler
share/html/Prefs/MyRT.html
share/html/Prefs/Search.html
share/html/Prefs/SearchOptions.html
share/html/REST/1.0/Forms/ticket/comment
share/html/REST/1.0/Forms/ticket/default
share/html/REST/1.0/ticket/comment
share/html/Search/Build.html
share/html/Search/Bulk.html
share/html/Search/Chart
share/html/Search/Chart.html
share/html/Search/Elements/BuildFormatString
share/html/Search/Elements/Chart
share/html/Search/Elements/EditSearches
share/html/Search/Elements/PickObjectCFs
share/html/Search/Elements/PickTicketCFs
share/html/Search/Elements/ResultsRSSView
share/html/Search/Results.html
share/html/Search/Results.tsv
share/html/SelfService/Closed.html
share/html/SelfService/Elements/MyRequests
share/html/SelfService/index.html
share/html/Ticket/Create.html
share/html/Ticket/Display.html
share/html/Ticket/Elements/ClickToShowHistory
share/html/Ticket/Elements/DelayShowHistory
share/html/Ticket/Elements/EditTransactionCustomFields
share/html/Ticket/Elements/PreviewScrips
share/html/Ticket/Elements/Reminders
share/html/Ticket/Elements/ShowDates
share/html/Ticket/Elements/ShowGroupMembers
share/html/Ticket/Elements/ShowRequestor
share/html/Ticket/Elements/ShowSummary
share/html/Ticket/Elements/ShowTime
share/html/Ticket/Elements/ShowUpdateStatus
share/html/Ticket/Forward.html
share/html/Ticket/Graphs/Elements/ShowGraph
share/html/Ticket/ModifyAll.html
share/html/Ticket/ModifyDates.html
share/html/Ticket/ModifyPeople.html
share/html/Ticket/Update.html
share/html/User/Elements/Portlets/CreateTicket
share/html/User/Prefs.html
share/html/User/Search.html
share/html/User/Summary.html
share/html/Widgets/Form/Select
share/html/Widgets/TitleBoxStart
share/html/index.html
share/html/m/_elements/header
share/html/m/_elements/ticket_list
share/html/m/ticket/create
share/html/m/ticket/history
share/html/m/ticket/show
share/po/ar.po
share/po/bg.po
share/po/ca.po
share/po/cs.po
share/po/da.po
share/po/de.po
share/po/el.po
share/po/en.po
share/po/en_GB.po
share/po/es.po
share/po/et.po
share/po/eu.po
share/po/fa.po [new file with mode: 0644]
share/po/fi.po
share/po/fr.po
share/po/hr.po
share/po/hu.po
share/po/id.po
share/po/is.po
share/po/it.po
share/po/ja.po
share/po/lt.po
share/po/lv.po
share/po/nb.po
share/po/nl.po
share/po/nn.po
share/po/oc.po
share/po/pl.po
share/po/pt.po
share/po/pt_BR.po
share/po/pt_PT.po
share/po/rt.pot
share/po/ru.po
share/po/sk.po
share/po/sl.po
share/po/sr.po
share/po/sv.po
share/po/tr.po
share/po/zh_CN.po
share/po/zh_TW.po
share/static/css/base/history.css
share/static/css/base/ticket.css
share/static/css/rudder/boxes.css
share/static/css/rudder/forms.css
share/static/css/rudder/nav.css
share/static/css/rudder/ticket-lists.css
share/static/images/bpslogo.png
share/static/images/eyedropper.png
share/static/js/autocomplete.js
share/static/js/cascaded.js
share/static/js/forms.js
share/static/js/util.js

diff --git a/bin/rt b/bin/rt
index d342b64..a73bb42 100755 (executable)
--- a/bin/rt
+++ b/bin/rt
@@ -322,6 +322,7 @@ sub list {
     }
     if ( ! $rawprint and ! exists $data{format} ) {
         $data{format} = 'l';
+        $data{fields} = 'subject,status,queue,created,told,owner,requestors';
     }
     if ( $reverse_sort and $data{orderby} =~ /^-/ ) {
         $data{orderby} =~ s/^-/+/;
@@ -792,6 +793,7 @@ sub comment {
     my ($action) = @_;
     my (%data, $id, @files, @bcc, @cc, $msg, $content_type, $wtime, $edit);
     my $bad = 0;
+    my $status = '';
 
     while (@ARGV) {
         $_ = shift @ARGV;
@@ -799,7 +801,7 @@ sub comment {
         if (/^-e$/) {
             $edit = 1;
         }
-        elsif (/^-(?:[abcmw]|ct)$/) {
+        elsif (/^-(?:[abcmws]|ct)$/) {
             unless (@ARGV) {
                 whine "No argument specified with $_.";
                 $bad = 1; last;
@@ -815,6 +817,9 @@ sub comment {
             elsif (/-ct/) {
                 $content_type = shift @ARGV;
             }
+            elsif (/-s/) {
+                $status = shift @ARGV;
+            }
             elsif (/-([bc])/) {
                 my $a = $_ eq "-b" ? \@bcc : \@cc;
                 @$a = split /\s*,\s*/, shift @ARGV;
@@ -857,9 +862,12 @@ sub comment {
             TimeWorked => $wtime || '',
             'Content-Type' => $content_type || 'text/plain',
             Text       => $msg || '',
-            Status => ''
+            Status => $status
         }
     ];
+    if ($status ne '') {
+      push(@{$form->[1]}, "Status");
+    }
 
     my $text = Form::compose([ $form ]);
 
@@ -2402,6 +2410,8 @@ Text:
                         than once to attach multiple files.)
         -c <addrs>      A comma-separated list of Cc addresses.
         -b <addrs>      A comma-separated list of Bcc addresses.
+        -s <status>     Set a new status for the ticket (default will
+                        leave the status unchanged)
         -w <time>       Specify the time spent working on this ticket.
         -e              Starts an editor before the submission, even if
                         arguments from the command line were sufficient.
index 4e5cc34..5e72899 100755 (executable)
@@ -151,6 +151,7 @@ sub get_useragent {
     my $self = shift;
     my $opts = shift;
     my $ua   = LWP::UserAgent->new();
+    $ua->agent("rt-mailgate/4.2.8 ");
     $ua->cookie_jar( { file => $opts->{'jar'} } ) if $opts->{'jar'};
 
     $ua->ssl_opts( verify_hostname => $opts->{'verify-ssl'} );
@@ -218,6 +219,14 @@ sub upload_message {
 
     $ua->timeout( exists( $opts->{'timeout'} ) ? $opts->{'timeout'} : 180 );
     my $r = $ua->post( $full_url, $post_params, Content_Type => 'form-data' );
+
+    # Follow 3 redirects
+    my $n = 0;
+    while ($n++ < 3 and $r->is_redirect) {
+        $full_url = $r->header( "Location" );
+        $r = $ua->post( $full_url, $post_params, Content_Type => 'form-data' );
+    }
+
     $self->check_failure($r);
 
     my $content = $r->content;
@@ -400,12 +409,9 @@ equivalent.
 =head1 SETUP
 
 Much of the set up of the mail gateway depends on your MTA and mail
-routing configuration. However, you will need first of all to create an
-RT user for the mail gateway and assign it a password; this helps to
-ensure that mail coming into the web server did originate from the
-gateway.
+routing configuration.
 
-Next, you need to route mail to C<rt-mailgate> for the queues you're
+You need to route mail to C<rt-mailgate> for the queues you're
 monitoring. For instance, if you're using F</etc/aliases> and you have a
 "bugs" queue, you will want something like this:
 
index 4bc7114..8c0b65d 100644 (file)
@@ -28,7 +28,7 @@ o   A supported SQL database
 
 o   Apache version 1.3.x or 2.x (http://httpd.apache.org)
         with mod_perl -- (http://perl.apache.org)
-        or with FastCGI -- (www.fastcgi.com)
+        or with FastCGI -- (http://www.fastcgi.com)
         or other webserver with FastCGI support
 
         RT's FastCGI handler needs to access RT's configuration file.
@@ -66,10 +66,10 @@ GENERAL INSTALLATION
 
     If you are upgrading from a previous version of RT, please review
     the upgrade notes for the appropriate versions, which can be found
-    in docs/UPGRADING-* If you are coming from 3.8.6 to 4.0.x you should
-    review both the UPGRADING-3.8 and UPGRADING-4.0 file.  Similarly, if
-    you were coming from 3.6.7, you would want to review UPGRADING-3.6,
-    UPGRADING-3.8 and UPGRADING-4.0
+    in docs/UPGRADING-* If you are coming from 4.0.x to 4.2.x you should
+    review both the UPGRADING-4.0 and UPGRADING-4.2 file.  Similarly, if
+    you were coming from 3.8.x, you would want to review UPGRADING-3.8,
+    UPGRADING-4.0 and UPGRADING-4.2
 
     It is particularly important that you read the warnings at the top of
     UPGRADING-4.0 for some common issues.
@@ -92,15 +92,6 @@ GENERAL INSTALLATION
     Some modules require user input or environment variables to install
     correctly, so it may be necessary to install them manually.
 
-    If you are installing with CPAN module older than 1.84, you will
-    need to start CPAN (by running perl -MCPAN -e shell) and upgrade the
-    CPAN shell with:
-
-        install CPAN
-
-    If you are unsure of your CPAN version, it will be printed when you
-    run the shell.
-
     If you are having trouble installing GD, refer to "Installing GD libraries"
     in docs/charts.pod.  Ticket relationship graphing requires the graphviz
     library which you should install using your distribution's package manager.
@@ -166,14 +157,11 @@ GENERAL INSTALLATION
 
       You should back up your database before running this command.
       When you run it, you will be prompted for your previous version of
-      RT (such as 3.6.4) so that the appropriate set of database
+      RT (such as 3.8.1) so that the appropriate set of database
       upgrades can be applied.
 
-      Finally, clear the Mason cache dir:
-
-          rm -fr /opt/rt4/var/mason_data/obj
-
-      You may then start your web server again.
+      If 'make upgrade-database' completes without error, your upgrade
+      has been successful and you may restart your webserver.
 
  7) Configure the web server, as described in docs/web_deployment.pod,
     and the email gateway, as described below.
@@ -200,14 +188,12 @@ GENERAL INSTALLATION
     To generate email digest messages, you must arrange for the provided
     utility to be run once daily, and once weekly. You may also want to
     arrange for the rt-email-dashboards utility to be run hourly.  For
-    example, if your task scheduler is cron, you can configure it as
-    follows:
-
-        crontab -e    # as the RT administrator (probably root)
-        # insert the following lines:
-        0 0 * * * /opt/rt4/sbin/rt-email-digest -m daily
-        0 0 * * 0 /opt/rt4/sbin/rt-email-digest -m weekly
-        0 * * * * /opt/rt4/sbin/rt-email-dashboards
+    example, if your task scheduler is cron, you can configure it by
+    adding the following lines as /etc/cron.d/rt:
+
+        0 0 * * * root /opt/rt4/sbin/rt-email-digest -m daily
+        0 0 * * 0 root /opt/rt4/sbin/rt-email-digest -m weekly
+        0 * * * * root /opt/rt4/sbin/rt-email-dashboards
 
 10) Configure the RT email gateway.  To let email flow to your RT
     server, you need to add a few lines of configuration to your mail
index 63dd2ee..766964f 100644 (file)
@@ -32,6 +32,9 @@ If you deploy RT with mod_perl, Apache will no longer start with C<SetHandler>
 set to `perl-script`. F<docs/web_deployment.pod> contains the
 new configuration.
 
+RT::Extension::CustomField::Checkbox has been integrated into core, so you
+MUST uninstall it before upgrading. In addition, you must run
+etc/upgrade/4.0-customfield-checkbox-extension script to convert old data.
 
 =head2 RT_SiteConfig.pm
 
index dd21c25..ccd8575 100644 (file)
@@ -310,4 +310,39 @@ equivalent results in RT 4.2.
 
 =back
 
+=head1 UPGRADING FROM 4.2.3 AND EARLIER
+
+RT 4.2.4's upgrade scripts contain two fixes to normalize upgraded RTs
+with those installed new from a release of RT 4.2.
+
+We neglected to add the "Open Inactive Tickets" action mentioned earlier
+in this documents. It was available to fresh installs but not on
+upgrades. This Scrip Action is now created if needed.
+
+RT expects the ___Approvals queue to have a special value in the
+Disabled column so that it is hidden B<but> tickets can still be created
+(normal disabled Queues disallow ticket creation).  Users who enabled
+and then disabled the Queue on earlier releases will have the incorrect
+Disabled value, so we fix that.  A similar problem applies to the
+lifecycle, which must be set to the internal "approvals" lifecycle --
+which is not listed as an option.  RT 4.2.4 also includes enhancements
+to the Queue admin page for ___Approvals to prevent editing things which
+might cause problems.
+
+=head1 UPGRADING FROM 4.2.5 AND EARLIER
+
+RT 4.2.6 includes a new Scrip Action "Notify Owner or AdminCc". This
+action will send the given correspondence to the Owner, if not Nobody,
+otherwise it will notify the AdminCcs. If using this, you will likely
+want to modify or remove the Notify Owner and AdminCcs scrip to avoid
+duplicate notifications. This Scrip Action is not used in any default
+Scrips at this time.
+
+=head1 UPGRADING FROM 4.2.6 AND EARLIER
+
+The C<$LogoImageHeight> and C<$LogoImageWidth> configuration options
+have been overridden by CSS since 4.0.0, and thus did not affect
+display.  They have been removed, and setting them will trigger an
+informational message that setting them is ineffective.
+
 =cut
index a24b422..d62a3d6 100644 (file)
@@ -60,7 +60,31 @@ In order to keep user data in sync, this type of external auth is almost always
 used in combination with one or both of L</RT::Authen::ExternalAuth> and
 L</RT::Extension::LDAPImport>.
 
-=head3 Configuration options
+=head3 Apache configuration
+
+When configuring Apache to protect RT, remember that the RT mail gateway
+uses the web interface to upload the incoming email messages.  You will
+thus need to provide an exception for the mail gateway endpoint.
+
+An example of using LDAP authentication and HTTP Basic auth:
+
+    <Location />
+        Require valid-user
+        AuthType Basic
+        AuthName "RT access"
+        AuthBasicProvider ldap
+        AuthLDAPURL \
+            "ldap://ldap.example.com/dc=example,dc=com"
+    </Location>
+    <Location /REST/1.0/NoAuth/mail-gateway>
+        Order deny,allow
+        Deny from all
+        Allow from localhost
+        Satisfy any
+    </Location>
+
+
+=head3 RT configuration options
 
 All of the following options control the behaviour of RT's built-in external
 authentication which relies on the web server.  They are documented in detail
index 0928790..efcfc81 100644 (file)
@@ -6,7 +6,7 @@ absolutely necessary to ensure you can recover quickly from an incident.
 Make sure you take backups.  Make sure they I<work>.
 
 There are many issues that can cause broken backups, such as a
-max_attachment_size too low for MySQL (in either the client or server), or
+C<max_allowed_packet> too low for MySQL (in either the client or server), or
 encoding issues, or running out of disk space.
 
 Make sure your backup cronjobs notify someone if they fail instead of failing
@@ -31,14 +31,10 @@ RT. :)
 
 =head3 MySQL
 
-    ( mysqldump rt4 --tables sessions --no-data; \
+    ( mysqldump rt4 --tables sessions --no-data --single-transaction; \
       mysqldump rt4 --ignore-table rt4.sessions --single-transaction ) \
         | 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>
-option to the second C<mysqldump> command.
-
 The dump will be much faster if you can connect to the MySQL server over
 localhost.  This will use a local socket instead of the network.
 
index 3d16252..c42dede 100644 (file)
@@ -9,7 +9,7 @@ This document walks through the steps to set up a
 "Change requests" queue with approvals. You should try
 this in a test instance first. If you don't have a test RT
 instance, you should read through the entire document first,
-change the details as needed for you approval scenario, and then
+change the details as needed for your approval scenario, and then
 set up approvals.
 
 =head2 Overview
index 29ab96b..f6612f0 100644 (file)
@@ -6,6 +6,42 @@ But there can be any number of workflows where these status values
 don't completely fit. RT allows you to add new custom status values and
 define their behavior with a feature called Lifecycles.
 
+=head1 Adding a New Status
+
+Because Statuses are controlled via lifecycles, you must manipulate the entire
+lifecycle configuration to add a status. In earlier versions of RT new statuses
+could be added by adding a new element to an array in RT's config file. But
+because lifecyles are built around statuses, the entire lifecycle configuration
+must be modified even if you only need new statuses.
+
+=head2 Copy Lifecycle Config
+
+First, copy the C<%Lifecycles> hash from C<RT_Config.pm> and paste it into
+C<RT_SiteConfig.pm>.
+
+=head2 Add Status Value
+
+Add the status to the set where your new status belongs. This example adds
+C<approved> to the active statuses:
+
+    active => [ 'open', 'approved', 'stalled' ],
+
+=head2 Update Transitions
+
+Now the transitions section must be updated so that the new status can
+transition to the existing statuses and also so the existing statuses can
+transition to the new status.
+
+    new      => [qw(    open approved stalled resolved rejected deleted)],
+    open     => [qw(new      approved stalled resolved rejected deleted)],
+    approved => [qw(new open          stalled resolved rejected deleted)],
+    stalled  => [qw(new open approved         rejected resolved deleted)],
+    resolved => [qw(new open approved stalled          rejected deleted)],
+    rejected => [qw(new open approved stalled resolved          deleted)],
+    deleted  => [qw(new open approved stalled rejected resolved        )],
+
+=head1 Order Processing Example
+
 This guide demonstrates lifecycles using an order fulfillment
 system as a real-world example. You can find full lifecycles
 documentation in L<RT_Config/Lifecycles>.
@@ -21,12 +57,6 @@ The detailed configuration options are discussed below. Once you add it
 and restart the server, the new lifecycle will be available on the
 queue configuration page.
 
-If you want to modify the default lifecycle, you can copy it from
-C<RT_Config.pm>, paste it into C<RT_SiteConfig.pm> and make your
-changes.
-
-=head1 Order Processing Example
-
 To show how you might use custom lifecycles, we're going to configure
 an RT lifecycle to process orders of some sort. In our order example,
 each ticket in the queue is considered a separate order and the orders
diff --git a/docs/customizing/search_result_columns.pod b/docs/customizing/search_result_columns.pod
new file mode 100644 (file)
index 0000000..7eef416
--- /dev/null
@@ -0,0 +1,180 @@
+=head1 RT Search Results
+
+Ticket search results in RT are presented as a table with multiple heading
+rows, one for each element of ticket metadata you have selected. Each
+row in the table represents one ticket and the appropriate metadata is
+displayed in each column. You can see similar listings when you search
+for other objects in RT like users, queues, templates, etc.
+
+For tickets, the Query Builder allows you to modify the column layout using
+the Sorting and Display Columns sections at the bottom of the page. With
+them you can add and remove data elements to sort by, change the sort order,
+and add and remove which columns you want to see.
+
+Although the Add Columns box has an extensive list of available columns, there
+are times when you need a value not listed. Sometimes what you want is a
+value calculated based on existing ticket values, like finding the difference
+between two date fields. RT provides a way to add this sort of customization
+using something called a Column Map.
+
+=head2 Level of Difficulty
+
+The customizations described in this section require administrative access
+to the RT server and the RT filesystem, typically root or sudo level access.
+The customizations involve adding new code to RT, which is written in the
+L<Perl|http://www.perl.org/> programming language and uses the
+L<Mason|http://www.masonbook.com/> templating system. If you follow the example
+closely, you should be able to set up simple column maps with a basic
+understanding of these. For more complicated configurations, you may need
+to do more research to understand the Perl and Mason syntax.
+
+=head2 Column Maps
+
+Each column in a ticket listing gets run through a bit of code called a
+Column Map that allows you to perform transformations on the value before
+it is displayed. In some cases, the value is just passed through. In others,
+like DueRelative, a date is transformed to a relative time like "2 days ago."
+You can tap into this functionality to add your own transformations or even
+generate completely new values.
+
+To add to the existing Column Maps, you can use RT's callback
+mechanism. This allows you to add code to RT without modifying the core files,
+making upgrades much easier. As an example, we'll add a Column Map to the
+ticket display and explain the necessary callbacks. You can read more about
+callbacks in general in the L<writing_extensions/Callbacks> documentation.
+
+For our example, let's assume we want to display a response time column that
+shows the difference between when a ticket is created and when someone
+starts working on it (started date). The two initial values are already
+available on the ticket, but it would be convenient to display the
+calculated value in our search.
+
+=head2 Column Map Callback
+
+First we need to determine where to put our callback. RT's core Column Map code
+for tickets is here:
+
+    share/html/Elements/RT__Ticket/ColumnMap
+
+We'll look there first, both to see some sample Column Maps and also to look
+for an appropriate callback to use to add our own. Looking in that file,
+we see C<$COLUMN_MAP>, which is a large hashref with entries for each of the
+items you see in the Add Columns section of the Query Builder. That's where
+we need to add our new Column Map.
+
+Looking in the C<init> section, we find a callback with a C<CallbackName>
+"Once" and it passes the C<$COLUMN_MAP> reference as an argument, so that's
+the callback we need.
+
+Following the callback documentation, we determine we can put our callback
+here:
+
+    local/html/Callbacks/MyRT/Elements/RT__Ticket/ColumnMap/Once
+
+where F<Once> is the name of the file where we'll put our code.
+
+In the F<Once> file, we'll put the following code:
+
+    <%init>
+    $COLUMN_MAP->{'TimeToFirstResponse'} = {
+            title     => 'First Response', # loc
+            attribute => 'First Response',
+            value     => sub {
+                my $ticket = shift;
+                return $ticket->StartedObj->DiffAsString($ticket->CreatedObj);
+            }
+    };
+    </%init>
+    <%args>
+    $COLUMN_MAP
+    </%args>
+
+Starting with the C<args> section, the value we're interested in is
+the C<$COLUMN_MAP> hash reference. Since it's a reference, it's pointing
+to the actual data structure constructed in the core RT code. This means
+we can add more entries and RT will have access to them.
+
+=head2 Column Map Parameters
+
+As you can see in the examples in the core F<ColumnMap> file, each entry
+has a key and a hashref with several other parameters. The key needs to be a
+unique value. If you using an existing value, you'll overwrite the original
+values.
+
+The parameters in the hashref are as follows:
+
+=over
+
+=item title
+
+The title is what will be used in the header row to identify this value.
+The C<# loc> is some special markup that allows RT to replace the value
+with translations in other languages, if they are available.
+
+=item attribute
+
+This defines the value you can use to reference your new column map
+from an RT Format configuration. You can edit formats in the Query
+Builder's Advanced section. If you're not familiar with formats, it's
+usually safe to set the attribute to the same value as C<title>. It should
+be descriptive and unique.
+
+=item value
+
+This is where you can put code to transform or calculate the value that
+will be displayed. This sets the value you see in the search results
+for this column.
+
+=back
+
+=cut
+
+Each of these can be a value like a simple string or an anonymous
+subroutine with code that runs to calculate the value.
+
+If you write a subroutine, as we do for C<value> in our example, RT will
+pass the current object as the first parameter to the sub. Since
+we're creating a column map for tickets, as RT processes the ticket for
+each row in the search results, the ticket object for that ticket is made
+available as the first parameter to our subroutine.
+
+This allows us to then call methods on the L<RT::Ticket> object to access
+and process the value. In our case, we can get the L<RT::Date> objects for
+the two dates and use the L<RT::Date/DiffAsString> method to calculate and
+return the difference.
+
+When writing code to calculate values, remember that it will be run for each
+row in search results. You should avoid doing things that are too time
+intensive in that code, like calling a web service to fetch a value.
+
+=head2 Adding to Display Columns
+
+Now that we have our column map created, there is one more callback to add
+to make it available for all of our users in the Add Columns section in
+the Query Builder. This file builds the list of fields available:
+
+    share/html/Search/Elements/BuildFormatString
+
+Looking there, we see the default callback (the callback without an
+explicit C<CallbackName>) passes the C<@fields> array, so that will work.
+Create the file:
+
+    local/html/Callbacks/MyRT/Search/Elements/BuildFormatString/Default
+
+And put the following code in the F<Default> file:
+
+    <%INIT>
+    push @{$Fields}, 'TimeToFirstResponse';
+    </%INIT>
+    <%ARGS>
+    $Fields => undef
+    </%ARGS>
+
+This puts the hash key we chose for our column map in the fields list so it
+will be available in the list of available fields.
+
+=head2 Last Steps
+
+Once you have the code in place, stop the RT web server, clear the Mason
+cache, and restart the server. Watch the RT logs for any errors, and
+navigate to the Query Build to use your new column map.
index d61542d..331534c 100644 (file)
@@ -129,7 +129,9 @@ use.  Among them:
 
 =item $TicketCF(Name)
 
-For example, C<$TicketCFDepartment>.
+For example, C<$TicketCFDepartment>.  For CFs with more complicated
+names, all non-word characters (anything that is not letters, numbers,
+or underscores) are stripped to determine the appropriate variable name.
 
 =item $TransactionType
 
index b7a525d..d52ea59 100644 (file)
@@ -90,11 +90,20 @@ add action types.  Values are subroutine references which will get
 called when needed.  They should return the modified string. Note that
 subroutine B<must escape> HTML.
 
-=item handler
+=item handle
 
 A subroutine reference; modify it only if you have to. This can be used
 to add pre- or post-processing around all actions.
 
+=item cache
+
+An undefined variable that should be replaced with a subroutine
+reference. This subroutine will be called twice, once with the arguments
+fetch => content_ref and once with store => content_ref. In the fetch
+case, if a cached copy is found, return the cached content, otherwise
+return a false value. When passed store, you should populate your cache
+with the content. The return value is ignored in this case.
+
 =back
 
 =head2 Actions' arguments
index f32bda7..5e70d3e 100644 (file)
@@ -57,6 +57,10 @@ web interface. See L</Configuration>.
 This method should return an array reference of hash references.  The
 hash references should contain keys for C<name>, C<description>, and
 C<sortorder>. C<name> is most important one; the others are optional.
+You can also optionally provide a key for C<category> and use the
+"Categories are based on" option on the custom field configuration
+page to make the values displayed for this custom field vary based
+on the value selected in the "based on" custom field.
 
 =back
 
@@ -77,9 +81,9 @@ Here's a simple static example:
       # return reference to array ([])
       return [
           # each element of the array is a reference to hash that describe a value
-          # possible keys are name, description and sortorder
-          { name => 'value1', description => 'external value', sortorder => 1 },
-          { name => 'value2', description => 'another external value', sortorder => 2 },
+          # possible keys are name, description, sortorder, and category
+          { name => 'value1', description => 'external value', sortorder => 1, category => 'Other CF' },
+          { name => 'value2', description => 'another external value', sortorder => 2, category => 'Other CF' },
           # values without description are also valid, the default description is empty string
           { name => 'value3', sortorder => 3 },
           # you can skip sortorder too, but note that the default sortorder is 0 (zero)
index 0952b4e..6b0025d 100644 (file)
@@ -48,8 +48,16 @@ To keep the index up-to-date, you will need to run:
 ...at regular intervals.  By default, this will only tokenize up to 100
 tickets at a time; you can adjust this upwards by passing
 C<--limit 500>.  Larger batch sizes will take longer and
-consume more memory.  Care should be taken to ensure that multiple
-instances of C<rt-fulltext-indexer> are not run at the same time.
+consume more memory.
+
+If there is already an instances of C<rt-fulltext-indexer> running, new
+ones will exit abnormally (with exit code 1) and the error message
+"rt-fulltext-indexer is already running."  You can suppress this message
+and end those processes normally (with exit code 0) using the C<--quiet>
+option; this is particularly useful when running the command via
+C<cron>:
+
+    sbin/rt-fulltext-indexer --quiet
 
 =head1 MYSQL
 
@@ -165,6 +173,15 @@ C<--memory> option:
 
     rt-fulltext-indexer --memory 10M
 
+If there is already an instance of C<rt-fulltext-indexer> running, new
+ones will exit abnormally (with exit code 1) and the error message
+"rt-fulltext-indexer is already running."  You can suppress this message
+and end those processes normally (with exit code 0) using the C<--quiet>
+option; this is particularly useful when running the command via
+C<cron>:
+
+    sbin/rt-fulltext-indexer --quiet
+
 Instead of being run via C<cron>, this may instead be run via a
 DBMS_JOB; read the B<Managing DML Operations for a CONTEXT Index>
 chapter of Oracle's B<Text Application Developer's Guide> for details
index a3280c9..23ce51e 100644 (file)
@@ -153,8 +153,14 @@ C<#loc_left_pair> is used for declaring that the I<key> of a
 particular C<< key => value >> pair is translatable. This is of
 very limited usefulness.
 
-C<#loc_right_pair> does NOT exist. C<#loc> works in such cases since
-its parser does not extend beyond the string at the end of a line.
+C<#loc_right_pair> does NOT exist. C<#loc> works in such cases since its
+parser does not extend beyond the string at the end of a line.  However,
+if the string is I<not> at the end of the line, C<#loc{word}> declares
+that the value associated with the key I<word> (earlier on the same
+line) is to be loc'd.  This is useful for inline hashes:
+
+    # Note the string "baz" is to be loc'd
+    foo => { bar => "baz", troz => "zort" },  # loc{bar}
 
 =head1 Development tips
 
index 0ed73f4..ac55dcf 100644 (file)
@@ -80,55 +80,19 @@ For a full list of fields, read the documentation for L<RT::User/Create>.
     push @Groups, {
         Name        => 'Example Employees',
         Description => 'All of the employees of my company',
+        Members     => { Users =>  [ qw/ alexmv trs falcone / ],
+                         Groups => [ qw/ extras / ] },
     };
 
 Creates a new L<RT::Group> for each hashref.  In almost all cases you'll want
 to follow the example above to create a group just as if you had done it from
 the admin interface.
 
-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
-added as a member.
-
-Unfortunately you can't specify the I<members> of a group at this time.  As a
-workaround, you can push a subref into C<@Final> which adds members to your new
-groups.  An example, using a convenience function to avoid repeating yourself:
-
-    push @Final, sub {
-        add_members('My New Group Name' => qw(trs alex ruslan));
-        add_members('My Second Group'   => qw(jesse kevin sunnavy jim));
-    };
-
-    sub add_members {
-        my $group_name = shift;
-        my @members    = @_;
-
-        my $group = RT::Group->new( RT->SystemUser );
-        $group->LoadUserDefinedGroup($group_name);
-
-        if ($group->id) {
-            for my $name (@members) {
-                my $member = RT::User->new( RT->SystemUser );
-                $member->LoadByCols( Name => $name );
-
-                unless ($member->Id) {
-                    RT->Logger->error("Unable to find user '$name'");
-                    next;
-                }
-
-                my ($ok, $msg) = $group->AddMember( $member->PrincipalObj->Id );
-                if ($ok) {
-                    RT->Logger->info("Added member $name to $group_name");
-                } else {
-                    RT->Logger->error("Unable to AddMember $name to $group_name: $msg");
-                }
-            }
-        } else {
-            RT->Logger->error("Unable to find group '$group_name'!");
-        }
-    }
+In addition to the C<Members> option shown above, which can take both
+users and groups, the C<MemberOf> field 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 added as a member.
 
 =head2 C<@Queues>
 
@@ -315,6 +279,14 @@ granted.  This is B<different> than the user/group/role receiving the right.
     CF      => 'Name',
     Queue   => 'Name',
 
+=item Granted on a custom field applied to some other object
+
+    # This finds the CF named "Name" applied to Articles in the
+    # "Responses" class
+    CF         => 'Name',
+    LookupType => RT::Article->CustomFieldLookupType,
+    ObjectId   => 'Responses',
+
 =item Granted on some other object (article Classes, etc)
 
     ObjectType => 'RT::Class',
@@ -455,8 +427,33 @@ L<RT::Template/Create> for the fields you can use.
 
 An array of L<RT::Attribute>s to create.  You likely don't need to mess with
 this.  If you do, know that the key C<Object> is expected to be an
-L<RT::Record> object on which to call C<AddAttribute>.  If you don't provide
-C<Object> or it's undefined, C<< RT->System >> will be used.
+L<RT::Record> object or a subroutine reference that returns an object on which
+to call C<AddAttribute>.  If you don't provide C<Object> or it's undefined,
+C<< RT->System >> will be used.
+
+Here is an example of using a subroutine reference as a value for Object:
+
+    @Attributes = ({
+        Name        => 'SavedSearch',
+        Description => 'New Tickets in SomeQueue',
+        Object      => sub {
+            my $GroupName = 'SomeQueue Group';
+            my $group     = RT::Group->new( RT->SystemUser );
+    
+            my( $ret, $msg ) = $group->LoadUserDefinedGroup( $GroupName );
+            die $msg unless $ret;
+    
+            return $group;
+        },
+        Content     => {
+            Format =>  <<'        END_OF_FORMAT',
+    ....
+            END_OF_FORMAT
+            Query   => "Status = 'new' AND Queue = 'SomeQueue'",
+            OrderBy => 'id',
+            Order   => 'DESC'
+        },
+    });
 
 =head2 C<@Initial>
 
index c2bc67d..e3af881 100644 (file)
@@ -46,7 +46,7 @@ To schedule the reminders, add a line like the following to your RT crontab:
                    --search-arg 'Type = "reminder" and (Status = "open" or Status = "new")' \
                    --condition RT::Condition::BeforeDue \
                    --condition-arg 2d \
-                   --action RT::Action::SendEmail \
+                   --action RT::Action::Notify \
                    --action-arg Owner,AlwaysNotifyActor \
                    --transaction first \
                    --template 'Reminder'
index d81ceee..d70141a 100644 (file)
@@ -57,8 +57,11 @@ edge [
 "Scrips" [shape = record, fontsize = 18, label = "<col0> \N " ];
 "Scrips" -> "ScripConditions" [label="ScripCondition → id"];
 "Scrips" -> "ScripActions" [label="ScripAction → id"];
-"Scrips" -> "Templates" [label="Template → id"];
-"Scrips" -> "Queues" [label="Queue → id"];
+"Scrips" -> "Templates" [label="Template → Name"];
+"Scrips" -> "ObjectScrips" [label="id → Scrip"]
+
+"ObjectScrips" [shape = record, fontsize = 18, label = "<col0> \N " ];
+"ObjectScrips" -> "Queues" [label="ObjectId → id"];
 
 "Templates" [shape = record, fontsize = 18, label = "<col0> \N " ];
 "Templates" -> "Queues" [label ="Queue → id" ];
index a5684e0..79a9fb4 100644 (file)
@@ -22,12 +22,6 @@ to use L<Starman>, a high performance preforking server:
 
     /opt/rt4/sbin/rt-server --server Starman --port 8080
 
-B<NOTICE>: After you run the standalone server as root, you will need to
-remove your C<var/mason_data> directory, or the non-standalone servers
-(Apache, etc), which run as a non-privileged user, will not be able to
-write to it and will not work.
-
-
 =head2 Apache
 
 B<WARNING>: Both C<mod_speling> and C<mod_cache> are known to break RT.
@@ -35,6 +29,9 @@ C<mod_speling> will cause RT's CSS and JS to not be loaded, making RT
 appear unstyled. C<mod_cache> will cache cookies, making users be
 spontaneously logged in as other users in the system.
 
+See also L<authentication/Apache configuration>, in case you intend to
+use Apache to provide authentication.
+
 =head3 mod_fastcgi
 
     # Tell FastCGI to put its temporary files somewhere sane; this may
@@ -236,4 +233,3 @@ C<Location> directive.
 
 If you're not using Apache, please see L<Plack::Handler::FCGI> or the web
 server's own documentation for configuration examples.
-
diff --git a/docs/writing_extensions.pod b/docs/writing_extensions.pod
new file mode 100644 (file)
index 0000000..10d1466
--- /dev/null
@@ -0,0 +1,376 @@
+=head1 Introduction
+
+RT has a lot of core features, but sometimes you have a problem to solve
+that's beyond the scope of just configuration. The standard way to add
+features to RT is with an extension. You can see the large number of
+freely available extensions on CPAN under the RT::Extension namespace
+to get an idea what's already out there. We also list some of the more
+useful extensions on the Best Practical website at
+L<http://www.bestpractical.com/rt/extensions.html>
+
+After looking through those, you still may not find what you need, so
+you'll want to write your own extension. Through the years there have
+been different ways to safely and effectively add things onto RT.
+This document describes the current best practice which should allow
+you to add what you need and still be able to safely upgrade RT
+in the future.
+
+=head1 Getting Started
+
+There are a few modules that will set up your initial sandbox for you
+to get you started. Install these modules from CPAN:
+
+=over
+
+=item Module::Install::RTx
+
+Sets up your extension to be installed using Module::Install.
+
+=item Dist::Zilla::MintingProfile::RTx
+
+Provides some tools for managing your distribution. Handy even if you're
+not putting your code on CPAN.
+
+=back
+
+If this is your first time using L<Dist::Zilla>, you can set up your
+CPAN details by running:
+
+    dzil setup
+
+You can read about L<Dist::Zilla> and the C<dzil> command at L<http://dzil.org>.
+
+Change to the directory that will be the parent directory for your new
+extension and run the following, replacing Demo with a descriptive name
+for your new extension:
+
+    dzil new -P RTx RT-Extension-Demo
+
+You'll see something like:
+
+    [DZ] making target dir /some-dir/RT-Extension-Demo
+    [DZ] writing files to /some-dir/RT-Extension-Demo
+    [DZ] dist minted in ./RT-Extension-Demo
+
+If you're stuck on a name, take a look at some of the existing RT extensions.
+You can also ask around IRC (#rt on irc.perl.org) to see what people think
+makes sense for what the extension will do.
+
+You'll now have a directory with the basic files for your extension.
+Included is a F<gitignore> file, which is handy if you use git for your version
+control like we do. If you don't use git, feel free to delete it, but we hope
+you're using some sort of version control for your work.
+
+=head1 Extension Directories
+
+There are several places to put code to provide your new features
+and if you follow the guidelines below, you'll make sure things
+get installed in the right places when you're ready to use it. These standards
+apply to RT 4.0 and 4.2 and any differences between the two are noted below.
+
+=head2 Module Code
+
+In your new extension directory you'll already have a
+C<lib/RT/Extension/Demo.pm> file, which is just a standard perl module.
+As you start writing code, you can use all of the standard RT libraries
+because your extension will be running in the context of RT and those
+are already pulled in. You can also create more modules under C<lib>
+as needed.
+
+=head2 Mason Code
+
+RT provides callbacks throughout its Mason templates to give you hooks to
+add features. The easiest way to modify RT is to add Mason template files
+that will use these callbacks. See L</Callbacks> for more information.
+Your Mason templates should go in an C<html> directory with the appropriate
+directory structure to make sure the callbacks are executed.
+
+If you are creating completely new pages for RT, you can put these under the
+C<html> directory also. You can create subdirectories as needed to add the
+page to existing RT paths (like Tools) or to create new directories for your
+extension.
+
+=head2 CSS and Javascript
+
+Where these files live differs between RT 4.2 and above, and RT 4.0 and
+below; if you need your extension to be compatible with both, you may
+need to provide both configurations.  On RT 4.2 and above, create a
+C<static> directory at the top level under your extension, and under
+that a C<css> directory and a C<js> directory. Before RT 4.2, you should
+create C<css> and C<js> directories in C<html/NoAuth/>.
+
+To add files to RT's include paths, you can use the L<RT/AddStyleSheets> and
+L<RT/AddJavascript> methods available in the L<RT> module. You can put the
+lines near the top of your module code (in your "Demo.pm" file). If you set up
+the paths correctly, you should only need to set the file names like this:
+
+    RT->AddStyleSheets('myextension.css');
+    RT->AddJavaScript('myextension.js');
+
+=head2 Creating Objects in RT
+
+If you need to have users create a group, scrip, template, or some other
+object in their RT instance, you can automate this using an F<initialdata>
+file. If you need this, the file should go in the C<etc> directory. This will
+allow users to easily run the F<initialdata> file when installing with:
+
+    make initdb
+
+=head2 Module::Install Files
+
+As mentioned above, the RT extension tools are set up to use L<Module::Install>
+to manage the distribution. When you run
+
+    perl Makefile.PL
+
+for the first time, L<Module::Install> will create an C<inc> directory for all
+of the files it needs. Since you are the author, a C<.author> directory
+(note the . in the directory name) is created for you in the C<inc>
+directory. When L<Module::Install> detects this directory, it does things only
+the author needs, like pulling in modules to put in the C<inc> directory.
+Once you have this set up, L<Module::Install> should mostly do the right thing.
+You can find details in the module documentation.
+
+=head2 Tests
+
+=head3 Test Directory
+
+You can create tests for your new extension just as with other perl code
+you write. However, unlike typical CPAN modules where users run the tests
+as a step in the installation process, RT users installing extensions don't
+usually run tests. This is because running the tests requires your RT to
+be set up in development mode which involves installing some additional
+modules and having a test database. To prevent users from accidentally
+running the tests, which will fail without this testing setup, we put them in
+a C<xt> directory rather than the typical C<t> directory.
+
+=head3 Writing Extension Tests
+
+If you want to write and run tests yourself, you'll need a development RT
+instance set up. Since you are building an extension, you probably already have
+one. To start with testing, set the C<RTHOME> environment variable to the base
+directory of your RT instance so your extension tests run against the right
+instance. This is especially useful if you have your test RT installed in a non-standard location.
+
+Next, you need to subclass from L<RT::Test>
+which gives you access to the test RT and a test database for running
+tests. For this, you'll create a F<Test.pm> file in your C<lib> tree.
+The easiest way to set up the test module to pull in F<RT::Test> is to look at
+an example extension. L<RT::Extension::RepeatTicket>, for example, has a
+testing configuration you can borrow from.
+
+You'll notice that the file included in the extension is
+F<lib/RT/Extension/RepeatTicket/Test.pm.in>. This is because there are paths
+that are set based on your RT location, so the actual F<Test.pm> file is
+written when you run F<Makefile.PL> with appropriate paths substituted
+when F<Makefile.PL> is run. L<Module::Install> provides an interface to make
+this easy with a C<substitute> feature. The substitution code is in the
+F<Makefile.PL> file and you can borrow that as well.
+
+Once you have that set up, add this to the top of your test files:
+
+    use RT::Extension::Demo::Test tests => undef;
+
+and you'll be able to run tests in the context of a fully functioning RT
+instance. The L<RT::Test>
+documentation describes some of the helper methods available and you can
+look at other extensions and the RT source code for examples of how to
+do things like create tickets, queues, and users, how to set rights, and
+how to modify tickets to simulate various RT tasks.
+
+If you have a command-line component in your extension, the easiest way
+to test it is to set up a C<run> method using the Modulino approach.
+You can find an example of this approach in L<RT::Extension::RepeatTicket>
+in the F<bin> directory.
+
+=head2 Patches
+
+If you need to provide patches to RT for any reason, you can put them in
+a C<patches> directory. See L</"Changes to RT"> for more information.
+
+=head1 Callbacks
+
+The RT codebase, mostly the Mason templates, contains hooks called callbacks
+that make it easy to add functionality without changing the RT code itself.
+RT invokes callbacks by looking in the source directories for files that might
+have extra code.
+
+=head2 Directory Structure
+
+RT looks in the F<local/plugins> directory under the RT base directory for
+extensions registered with the C<@Plugins> configuration. RT then uses the
+following structure when looking for callbacks:
+
+    local/plugins/[ext name]/html/Callbacks/[custom name]/[rt mason path]/[callback name]
+
+The extension installation process will handle some of this for you by putting
+your html directory under F<local/plugins/[ext name]> as part of the
+installation process. You need to make sure the path under C<html> is correct
+since that is installed as-is.
+
+The C<Callbacks> directory is required. The next directory can be named
+anything and is provided to allow RT owners to keep local files organized
+in a way that makes sense to them. In the case of
+an extension, you should name the directory the same as your extension.
+So if your extension is C<RT::Extension::Demo>, you should create a
+F<RT-Extension-Demo> directory under F<Callbacks>.
+
+The rest of the path is determined by the RT Mason code and the callback you
+want to use. You can find callbacks by looking for calls to the C<callback>
+method in the RT Mason code. You can use something like this in your base
+RT directory:
+
+    # find share/html/ | xargs grep '\->callback'
+
+As an example, assume you wanted to modify the ticket update page to put
+something after the Time Worked field. You run the above and see there is
+a callback in F<share/html/Ticket/Update.html> that looks like this:
+
+    $m->callback( %ARGS, CallbackName => 'AfterWorked', Ticket => $TicketObj );
+
+You look at the F<Update.html> file and see that the callback is located
+right after the Time Worked field. To add some code that RT will
+run at that point, you would create the directory:
+
+    html/Callbacks/RT-Extension-Demo/Ticket/Update.html/
+
+Note that F<Update.html> is a file in the RT source, but it becomes a directory
+in your extension code. You then create a file with the name of the
+callback, in this case F<AfterWorked>, and that's where you put your code.
+So the full path and file would be:
+
+    html/Callbacks/RT-Extension-Demo/Ticket/Update.html/AfterWorked
+
+If you see a callback that doesn't have a C<CallbackName> parameter, name
+your file F<Default> and it will get invoked since that is the default
+callback name when one isn't provided.
+
+=head2 Callback Parameters
+
+When you look at callbacks using the method above, the other important
+thing to consider is the parameter list. In addition to the C<CallbackName>,
+the other parameters listed in the callback will be passed to you
+to use as you develop your extension.
+
+Getting these parameters is important because you'll likely need them
+in your code, getting data from the current ticket object, for example.
+These values are also often passed by reference, which allows you to modify
+them, potentially changing the behavior of the RT template when it
+continues executing after evaluating your code.
+
+Some examples are adding a C<Limit> call to modify search results on
+a L<DBIx::SearchBuilder> object, or setting a flag like C<$skip_update>
+for a callback like this:
+
+    $m->callback( CallbackName => 'BeforeUpdate', ARGSRef => \%ARGS, skip_update => \$skip_update,
+              checks_failure => $checks_failure, results => \@results, TicketObj => $TicketObj );
+
+There are many different callbacks in RT and these are just a few examples
+to give you idea what you can do in your callback code. You can also look
+at other extensions for examples of how people use callbacks to modify
+and extend RT.
+
+=head1 Adding and Modifying Menus
+
+You can modify all of RT's menus using callbacks as described in L</Callbacks>.
+The file in RT that controls menus is:
+
+    share/html/Elements/Tabs
+
+and you'll find a Privileged and SelfService callback which gives you access
+to those two sets of menus. In those callbacks, you can add to or change
+the main menu, the page menu, or the page widgets.
+
+You can look at the F<Tabs> file itself for examples of adding menu items.
+The menu object is a L<RT::Interface::Web::Menu> and you can find details on
+the available parameters in the documentation.
+
+Here are some simple examples of what you might do in a callback:
+
+    <%init>
+    # Add a brand new root menu item
+    my $bps = Menu()->child(
+        'bps', # any unique identifier
+        title => 'Corporate',
+        path  => 'http://bestpractical.com'
+    );
+
+    #Add a submenu item to this root menu item
+    $bps->child(
+        'wiki',
+        title => 'Wiki',
+        path  => 'http://wiki.bestpractical.com',
+    );
+
+    #Retrieve the 'actions' page menu item
+    if (my $actions = PageMenu->child('actions')) {
+        $actions->child(
+            'newitem',
+            title => loc('New Action'), path => '/new/thing/here',
+        )
+    }
+    </%init>
+
+=head1 Changes to RT
+
+When writing an extension, the goal is to provide all of the new functionality
+in your extension code using standard interfaces into RT. However,
+sometimes when you're working on an extension, you'll find you really need
+a change in RT itself to make your extension work. Often this is something
+like adding a new callback or a method to a core module that would be
+helpful for everyone.
+
+Since any change to RT will only be included in the next version and
+forward, you'll need to provide something for users on current or older
+versions of RT. An easy way to do this is to provide a patch in your
+extension distribution. In general, you should only provide patches
+if you know they will eventually be merged into RT. Otherwise, you
+may have to provide versions of your patches for each release of RT.
+You can read more about getting changes accepted into RT in the
+L<hacking> document. We generally accept patches that add new callbacks.
+
+Create a C<patches> directory in your extension distribution to hold
+your patch files. Name the patch files with the latest version of RT
+that needs the patch. For example, if the patch is needed for RT 4.0.7,
+name your patch C<4.0.7-some-patch.diff>. That tells users that if they
+are using RT 4.0.7 or earlier, they need to apply the patch. If your
+extension can be used for RT 3.8, you'll likely need to provide different
+patches using the same naming convention.
+
+Also remember to update your install documentation to remind users to apply
+the patch.
+
+=head1 Preparing for CPAN
+
+When you have your extension ready and want to release it to the world, you
+can do so with a few simple steps.
+
+Assuming you have run C<perl Makefile.PL> and you created the F<inc/.author>
+directory as described above, a F<README> file will be created for you. You can
+now type:
+
+    make manifest
+
+and a F<MANIFEST> file will be created. It should contain all of the needed
+to install and run your extension. If you followed the steps above, you'll have
+also have a F<inc> directory which contains L<Module::Install> code. Note that
+this code should also be included with your extension when you release it as
+it's part of the install process.
+
+Next, check to see if everything is ready with:
+
+    make distcheck
+
+If anything is missing, it will be reported and you can go fix it.
+When the check is clean, run:
+
+    make dist
+
+and a new distribution will be created in the form of a tarred and gzipped
+file.
+
+Now you can upload to cpan with the F<cpan-upload> utility provided by
+L<CPAN::Uploader> or your favorite method of uploading to CPAN.
+
+=cut
+
index c7c3762..0d5809c 100644 (file)
@@ -115,13 +115,11 @@ Set($Timezone, "US/Eastern");
 
 =item C<@Plugins>
 
-Set C<@Plugins> to a list of external RT plugins that should be
-enabled (those plugins have to be previously downloaded and
-installed).
+Once a plugin has been downloaded and installed, use C<Plugin()> to add
+to the enabled C<@Plugins> list:
 
-Example:
-
-C<Set(@Plugins, (qw(Extension::QuickDelete RT::Extension::CommandByMail)));>
+    Plugin( "RT::Extension::SLA" );
+    Plugin( "RT::Authen::ExternalAuth" );
 
 =cut
 
@@ -159,7 +157,7 @@ Set( @StaticRoots, () );
 =item C<$DatabaseType>
 
 Database driver being used; case matters.  Valid types are "mysql",
-"Oracle" and "Pg".
+"Oracle", and "Pg".  "SQLite" is also available for non-production use.
 
 =cut
 
@@ -223,7 +221,7 @@ database.
 
 Set($DatabaseRequireSSL, undef);
 
-=item <$DatabaseAdmin>
+=item C<$DatabaseAdmin>
 
 The name of the database administrator to connect to the database as
 during upgrades.
@@ -322,8 +320,9 @@ Set(@LogToSyslogConf, ());
 =item C<$EmailSubjectTagRegex>
 
 This regexp controls what subject tags RT recognizes as its own.  If
-you're not dealing with historical C<$rtname> values, you'll likely
-never have to change this configuration.
+you're not dealing with historical C<$rtname> values, or historical
+queue-specific subject tags, you'll likely never have to change this
+configuration.
 
 Be B<very careful> with it. Note that it overrides C<$rtname> for
 subject token matching.
@@ -424,13 +423,13 @@ Set($RTAddressRegexp, undef);
 =item C<$CanonicalizeEmailAddressMatch>, C<$CanonicalizeEmailAddressReplace>
 
 RT provides functionality which allows the system to rewrite incoming
-email addresses.  In its simplest form, you can substitute the value
-in C<CanonicalizeEmailAddressReplace> for the value in
-C<CanonicalizeEmailAddressMatch> (These values are passed to the
-C<CanonicalizeEmailAddress> subroutine in F<RT/User.pm>)
-
-By default, that routine performs a C<s/$Match/$Replace/gi> on any
-address passed to it.
+email addresses, using L<RT::User/CanonicalizeEmailAddress>.  The
+default implementation replaces all occurrences of the regular
+expression in C<CanonicalizeEmailAddressMatch> with
+C<CanonicalizeEmailAddressReplace>, via C<s/$Match/$Replace/gi>.  The
+most common use of this is to replace C<@something.example.com> with
+C<@example.com>.  If more complex noramlization is required,
+L<RT::User/CanonicalizeEmailAddress> can be overridden to provide it.
 
 =cut
 
@@ -566,7 +565,7 @@ address of the queue as it is handed to sendmail -f. This helps force
 the From_ header away from www-data or other email addresses that show
 up in the "Sent by" line in Outlook.
 
-The option is a hash reference of queue name to email address.  If
+The option is a hash reference of queue id/name to email address. If
 there is no ticket involved, then the value of the C<Default> key will
 be used.
 
@@ -1102,24 +1101,6 @@ will be passed through C<loc> for localization.
 
 Set($LogoAltText, "Best Practical Solutions, LLC corporate logo");
 
-=item C<$LogoImageHeight>
-
-C<$LogoImageHeight> is the value of the C<height> attribute of the logo
-C<img> tag.
-
-=cut
-
-Set($LogoImageHeight, 38);
-
-=item C<$LogoImageWidth>
-
-C<$LogoImageWidth> is the value of the C<width> attribute of the logo
-C<img> tag.
-
-=cut
-
-Set($LogoImageWidth, 181);
-
 =item C<$WebNoAuthRegex>
 
 What portion of RT's URL space should not require authentication.  The
@@ -1309,6 +1290,23 @@ Set ($DefaultSearchResultFormat, qq{
    '<small>__LastUpdatedRelative__</small>',
    '<small>__TimeLeft__</small>'});
 
+=item C<$DefaultSearchResultOrderBy>
+
+What Tickets column should we order by for RT Ticket search results.
+
+=cut
+
+Set($DefaultSearchResultOrderBy, 'id');
+
+=item C<$DefaultSearchResultOrder>
+
+When ordering RT Ticket search results by C<$DefaultSearchResultOrderBy>,
+should the sort be ascending (ASC) or descending (DESC).
+
+=cut
+
+Set($DefaultSearchResultOrder, 'ASC');
+
 =item C<$DefaultSelfServiceSearchResultFormat>
 
 C<$DefaultSelfServiceSearchResultFormat> is the default format of
@@ -1676,7 +1674,7 @@ Set($MessageBoxIncludeSignature, 1);
 =item C<$MessageBoxIncludeSignatureOnComment>
 
 Should your users' signatures (from their Preferences page) be
-included in Comments. Setting this to false overrides
+included in Comments. Setting this to 0 overrides
 C<$MessageBoxIncludeSignature>.
 
 =cut
@@ -1764,10 +1762,18 @@ Set($AlwaysDownloadAttachments, undef);
 
 =item C<$PreferRichText>
 
-By default, RT shows rich text (HTML) messages if possible.
+By default, RT shows rich text (HTML) messages if possible.  If
+C<$PreferRichText> is set to 0, RT will show plain text messages in
+preference to any rich text alternatives.
 
-If C<$PreferRichText> is set to 0, RT will show plain text messages
-in preference to any rich text alternatives.
+As a security precaution, RT limits the HTML that is displayed to a
+known-good subset -- as allowing arbitrary HTML to be displayed exposes
+multiple vectors for XSS and phishing attacks.  If
+L</$TrustHTMLAttachments> is enabled, the original HTML is available for
+viewing via the "Download" link.
+
+If the optional L<HTML::Gumbo> dependency is installed, RT will leverage
+this to allow a broader set of HTML through, including tables.
 
 =cut
 
@@ -1857,8 +1863,17 @@ It defaults to on.  Set this to 0 to disable it.
 
 Set($QuoteFolding, 1);
 
-=back
+=item C<$AllowLoginPasswordAutoComplete>
+
+Allow browsers to remember the user's password on login (in case the
+browser can do so, and has the appropriate setting enabled). Default
+is 0.
+
+=cut
+
+Set($AllowLoginPasswordAutoComplete, 0);
 
+=back
 
 
 =head1 Application logic
@@ -1954,7 +1969,7 @@ defaults alone.
 
 =item C<$DisallowExecuteCode>
 
-If set to a true value, the C<ExecuteCode> right will be removed from
+If set to 1, the C<ExecuteCode> right will be removed from
 all users, B<including> the superuser.  This is intended for when RT is
 installed into a shared environment where even the superuser should not
 be allowed to run arbitrary Perl code on the server via scrips.
@@ -1965,7 +1980,7 @@ Set($DisallowExecuteCode, 0);
 
 =item C<$Framebusting>
 
-If set to a false value, framekiller javascript will be disabled and the
+If set to 0, framekiller javascript will be disabled and the
 X-Frame-Options: DENY header will be suppressed from all responses.
 This disables RT's clickjacking protection.
 
@@ -1975,7 +1990,7 @@ Set($Framebusting, 1);
 
 =item C<$RestrictReferrer>
 
-If set to a false value, the HTTP C<Referer> (sic) header will not be
+If set to 0, the HTTP C<Referer> (sic) header will not be
 checked to ensure that requests come from RT's own domain.  As RT allows
 for GET requests to alter state, disabling this opens RT up to
 cross-site request forgery (CSRF) attacks.
@@ -1986,9 +2001,9 @@ Set($RestrictReferrer, 1);
 
 =item C<$RestrictLoginReferrer>
 
-If set to a false value, RT will allow the user to log in from any link
+If set to 0, RT will allow the user to log in from any link
 or request, merely by passing in C<user> and C<pass> parameters; setting
-it to a true value forces all logins to come from the login box, so the
+it to 1 forces all logins to come from the login box, so the
 user is aware that they are being logged in.  The default is off, for
 backwards compatability.
 
@@ -2056,7 +2071,7 @@ Set($WebRemoteUserAuth, undef);
 
 If C<$WebRemoteUserContinuous> is defined, RT will check for the
 REMOTE_USER on each access.  If you would prefer this to only happen
-once (at initial login) set this to a false value.  The default
+once (at initial login) set this to 0.  The default
 setting will help ensure that if your webserver's authentication layer
 deauthenticates a user, RT notices as soon as possible.
 
@@ -2076,8 +2091,7 @@ Set($WebFallbackToRTLogin, undef);
 =item C<$WebRemoteUserGecos>
 
 C<$WebRemoteUserGecos> means to match 'gecos' field as the user
-identity; useful with C<mod_auth_pwcheck> and IIS Integrated Windows
-logon.
+identity; useful with C<mod_auth_external>.
 
 =cut
 
@@ -2108,7 +2122,7 @@ Set($UserAutocreateDefaultsOnLogin, undef);
 C<$WebSessionClass> is the class you wish to use for storing sessions.  On
 MySQL, Pg, and Oracle it defaults to using your database, in other cases
 sessions are stored in files using L<Apache::Session::File>. Other installed
-Apache:Session::* modules can be used to store sessions.
+Apache::Session::* modules can be used to store sessions.
 
     Set($WebSessionClass, "Apache::Session::File");
 
@@ -2330,13 +2344,13 @@ used. Use the C<Outgoing> option to set a security protocol that should
 be used in outgoing emails.  At this moment, only one protocol can be
 used to protect outgoing emails.
 
-Set C<RejectOnUnencrypted> to true if all incoming email must be
+Set C<RejectOnUnencrypted> to 1 if all incoming email must be
 properly encrypted.  All unencrypted emails will be rejected by RT.
 
-Set C<RejectOnMissingPrivateKey> to false if you don't want to reject
+Set C<RejectOnMissingPrivateKey> to 0 if you don't want to reject
 emails encrypted for key RT doesn't have and can not decrypt.
 
-Set C<RejectOnBadData> to false if you don't want to reject letters
+Set C<RejectOnBadData> to 0 if you don't want to reject letters
 with incorrect data.
 
 If you want to allow people to encrypt attachments inside the DB then
@@ -2375,7 +2389,7 @@ L<RT::Crypt::SMIME>.
 
 =item C<%SMIME>
 
-Set C<Enable> to false or true value to disable or enable SMIME for
+Set C<Enable> to 0 or 1 to disable or enable SMIME for
 encrypting and signing messages.
 
 Set C<OpenSSL> to path to F<openssl> executable.
@@ -2430,7 +2444,7 @@ be found by running the command `perldoc L<RT::Crypt::GnuPG>` (or
 
 =item C<%GnuPG>
 
-Set C<Enable> to false or true value to disable or enable GnuPG interfaces
+Set C<Enable> to 0 or 1 to disable or enable GnuPG interfaces
 for encrypting and signing outgoing messages.
 
 Set C<GnuPG> to the name or path of the gpg binary to use.
@@ -2612,6 +2626,14 @@ all possible transitions in each lifecycle using the following format:
         ...
     },
 
+The order of items in the listing for each transition line affects
+the order they appear in the drop-down. If you change the config
+for 'open' state listing to:
+
+    open     => [qw(stalled rejected deleted resolved)],
+
+then the 'resolved' status will appear as the last item in the drop-down.
+
 =head3 Statuses available during ticket creation
 
 By default users can create tickets with a status of new,
@@ -2714,8 +2736,8 @@ For example:
 
 Unless there is an explicit mapping between statuses in two different
 lifecycles, you can not move tickets between queues with these
-lifecycles.  This is true even if the different lifecycles use the exact
-same set of statuses.  Such a mapping is defined as follows:
+lifecycles -- even if both use the exact same set of statuses.
+Such a mapping is defined as follows:
 
     __maps__ => {
         'from lifecycle -> to lifecycle' => {
@@ -2743,64 +2765,32 @@ Set(%Lifecycles,
         },
 
         transitions => {
-            ''       => [qw(new open resolved)],
+            ""       => [qw(new open resolved)],
 
             # from   => [ to list ],
-            new      => [qw(open stalled resolved rejected deleted)],
-            open     => [qw(new stalled resolved rejected deleted)],
-            stalled  => [qw(new open rejected resolved deleted)],
-            resolved => [qw(new open stalled rejected deleted)],
-            rejected => [qw(new open stalled resolved deleted)],
-            deleted  => [qw(new open stalled rejected resolved)],
+            new      => [qw(    open stalled resolved rejected deleted)],
+            open     => [qw(new      stalled resolved rejected deleted)],
+            stalled  => [qw(new open         rejected resolved deleted)],
+            resolved => [qw(new open stalled          rejected deleted)],
+            rejected => [qw(new open stalled resolved          deleted)],
+            deleted  => [qw(new open stalled rejected resolved        )],
         },
         rights => {
             '* -> deleted'  => 'DeleteTicket',
             '* -> *'        => 'ModifyTicket',
         },
         actions => [
-            'new -> open'      => {
-                label  => 'Open It', # loc
-                update => 'Respond',
-            },
-            'new -> resolved'  => {
-                label  => 'Resolve', # loc
-                update => 'Comment',
-            },
-            'new -> rejected'  => {
-                label  => 'Reject', # loc
-                update => 'Respond',
-            },
-            'new -> deleted'   => {
-                label  => 'Delete', # loc
-            },
-
-            'open -> stalled'  => {
-                label  => 'Stall', # loc
-                update => 'Comment',
-            },
-            'open -> resolved' => {
-                label  => 'Resolve', # loc
-                update => 'Comment',
-            },
-            'open -> rejected' => {
-                label  => 'Reject', # loc
-                update => 'Respond',
-            },
-
-            'stalled -> open'  => {
-                label  => 'Open It', # loc
-            },
-            'resolved -> open' => {
-                label  => 'Re-open', # loc
-                update => 'Comment',
-            },
-            'rejected -> open' => {
-                label  => 'Re-open', # loc
-                update => 'Comment',
-            },
-            'deleted -> open'  => {
-                label  => 'Undelete', # loc
-            },
+            'new -> open'      => { label  => 'Open It', update => 'Respond' }, # loc{label}
+            'new -> resolved'  => { label  => 'Resolve', update => 'Comment' }, # loc{label}
+            'new -> rejected'  => { label  => 'Reject',  update => 'Respond' }, # loc{label}
+            'new -> deleted'   => { label  => 'Delete',                      }, # loc{label}
+            'open -> stalled'  => { label  => 'Stall',   update => 'Comment' }, # loc{label}
+            'open -> resolved' => { label  => 'Resolve', update => 'Comment' }, # loc{label}
+            'open -> rejected' => { label  => 'Reject',  update => 'Respond' }, # loc{label}
+            'stalled -> open'  => { label  => 'Open It',                     }, # loc{label}
+            'resolved -> open' => { label  => 'Re-open', update => 'Comment' }, # loc{label}
+            'rejected -> open' => { label  => 'Re-open', update => 'Comment' }, # loc{label}
+            'deleted -> open'  => { label  => 'Undelete',                    }, # loc{label}
         ],
     },
 # don't change lifecyle of the approvals, they are not capable to deal with
@@ -2834,49 +2824,17 @@ Set(%Lifecycles,
             '* -> *'        => 'ModifyTicket',
         },
         actions => [
-            'new -> open'      => {
-                label  => 'Open It', # loc
-                update => 'Respond',
-            },
-            'new -> resolved'  => {
-                label  => 'Resolve', # loc
-                update => 'Comment',
-            },
-            'new -> rejected'  => {
-                label  => 'Reject', # loc
-                update => 'Respond',
-            },
-            'new -> deleted'   => {
-                label  => 'Delete', # loc
-            },
-
-            'open -> stalled'  => {
-                label  => 'Stall', # loc
-                update => 'Comment',
-            },
-            'open -> resolved' => {
-                label  => 'Resolve', # loc
-                update => 'Comment',
-            },
-            'open -> rejected' => {
-                label  => 'Reject', # loc
-                update => 'Respond',
-            },
-
-            'stalled -> open'  => {
-                label  => 'Open It', # loc
-            },
-            'resolved -> open' => {
-                label  => 'Re-open', # loc
-                update => 'Comment',
-            },
-            'rejected -> open' => {
-                label  => 'Re-open', # loc
-                update => 'Comment',
-            },
-            'deleted -> open'  => {
-                label  => 'Undelete', # loc
-            },
+            'new -> open'      => { label  => 'Open It', update => 'Respond' }, # loc{label}
+            'new -> resolved'  => { label  => 'Resolve', update => 'Comment' }, # loc{label}
+            'new -> rejected'  => { label  => 'Reject',  update => 'Respond' }, # loc{label}
+            'new -> deleted'   => { label  => 'Delete',                      }, # loc{label}
+            'open -> stalled'  => { label  => 'Stall',   update => 'Comment' }, # loc{label}
+            'open -> resolved' => { label  => 'Resolve', update => 'Comment' }, # loc{label}
+            'open -> rejected' => { label  => 'Reject',  update => 'Respond' }, # loc{label}
+            'stalled -> open'  => { label  => 'Open It',                     }, # loc{label}
+            'resolved -> open' => { label  => 'Re-open', update => 'Comment' }, # loc{label}
+            'rejected -> open' => { label  => 'Re-open', update => 'Comment' }, # loc{label}
+            'deleted -> open'  => { label  => 'Undelete',                    }, # loc{label}
         ],
     },
 );
@@ -2901,7 +2859,7 @@ which asks the administrator's browser to show an inline page from
 Best Practical's website.
 
 If you'd rather not make this feature available to your
-administrators, set C<$ShowRTPortal> to a false value.
+administrators, set C<$ShowRTPortal> to 0.
 
 =cut
 
@@ -2919,26 +2877,26 @@ Set(%AdminSearchResultFormat,
     Queues =>
         q{'<a href="__WebPath__/Admin/Queues/Modify.html?id=__id__">__id__</a>/TITLE:#'}
         .q{,'<a href="__WebPath__/Admin/Queues/Modify.html?id=__id__">__Name__</a>/TITLE:Name'}
-        .q{,__Description__,__Address__,__Priority__,__DefaultDueIn__,__Disabled__,__Lifecycle__},
+        .q{,__Description__,__Address__,__Priority__,__DefaultDueIn__,__Lifecycle__,__SubjectTag__,__Disabled__},
 
     Groups =>
         q{'<a href="__WebPath__/Admin/Groups/Modify.html?id=__id__">__id__</a>/TITLE:#'}
         .q{,'<a href="__WebPath__/Admin/Groups/Modify.html?id=__id__">__Name__</a>/TITLE:Name'}
-        .q{,'__Description__'},
+        .q{,'__Description__',__Disabled__},
 
     Users =>
         q{'<a href="__WebPath__/Admin/Users/Modify.html?id=__id__">__id__</a>/TITLE:#'}
         .q{,'<a href="__WebPath__/Admin/Users/Modify.html?id=__id__">__Name__</a>/TITLE:Name'}
-        .q{,__RealName__, __EmailAddress__},
+        .q{,__RealName__, __EmailAddress__,__Disabled__},
 
     CustomFields =>
         q{'<a href="__WebPath__/Admin/CustomFields/Modify.html?id=__id__">__id__</a>/TITLE:#'}
         .q{,'<a href="__WebPath__/Admin/CustomFields/Modify.html?id=__id__">__Name__</a>/TITLE:Name'}
-        .q{,__AddedTo__, __FriendlyType__, __FriendlyPattern__},
+        .q{,__AddedTo__, __FriendlyType__, __FriendlyPattern__,__Disabled__},
 
     Scrips =>
-        q{'<a href="__WebPath__/Admin/Scrips/Modify.html?id=__id__">__id__</a>/TITLE:#'}
-        .q{,'<a href="__WebPath__/Admin/Scrips/Modify.html?id=__id__">__Description__</a>/TITLE:Description'}
+        q{'<a href="__WebPath__/Admin/Scrips/Modify.html?id=__id____From__">__id__</a>/TITLE:#'}
+        .q{,'<a href="__WebPath__/Admin/Scrips/Modify.html?id=__id____From__">__Description__</a>/TITLE:Description'}
         .q{,__Condition__, __Action__, __Template__, __Disabled__},
 
     Templates =>
@@ -2948,7 +2906,24 @@ Set(%AdminSearchResultFormat,
     Classes =>
         q{ '<a href="__WebPath__/Admin/Articles/Classes/Modify.html?id=__id__">__id__</a>/TITLE:#'}
         .q{,'<a href="__WebPath__/Admin/Articles/Classes/Modify.html?id=__id__">__Name__</a>/TITLE:Name'}
-        .q{,__Description__},
+        .q{,__Description__,__Disabled__},
+);
+
+=item C<%AdminSearchResultRows>
+
+Use C<%AdminSearchResultRows> to define the search result rows in the admin
+interface on a per-RT-class basis.
+
+=cut
+
+Set(%AdminSearchResultRows,
+    Queues       => 50,
+    Groups       => 50,
+    Users        => 50,
+    CustomFields => 50,
+    Scrips       => 50,
+    Templates    => 50,
+    Classes      => 50,
 );
 
 =back
index 9cd06ec..a659d8e 100644 (file)
@@ -69,14 +69,11 @@ sub acl {
          push @acls, "CREATE USER \"$db_user\" WITH PASSWORD '$db_pass' NOCREATEDB NOCREATEUSER;";
     }
 
-    my $sequence_right
-        = ( $dbh->{pg_server_version} >= 80200 )
-        ? "USAGE, SELECT, UPDATE"
-        : "SELECT, UPDATE";
     foreach my $table (@tables) {
         if ( $table =~ /^[a-z]/ && $table ne 'sessions' ) {
-# table like objectcustomfields_id_s
-            push @acls, "GRANT $sequence_right ON $table TO \"$db_user\";"
+            # Sequences; not all end with _seq because
+            # objectcustomfieldvalues_id_s is too long
+            push @acls, "GRANT USAGE, SELECT, UPDATE ON $table TO \"$db_user\";"
         }
         else {
             push @acls, "GRANT SELECT, INSERT, UPDATE, DELETE ON $table TO \"$db_user\";"
index 3de65f4..dd1daf5 100644 (file)
       Description => 'Sends mail to the Owner and administrative Ccs',    # loc
       ExecModule  => 'Notify',
       Argument    => 'Owner,AdminCc' },
+    { Name        => 'Notify Owner or AdminCcs',                         # loc
+      Description => 'Sends mail to the Owner if set, otherwise administrative Ccs',    # loc
+      ExecModule  => 'NotifyOwnerOrAdminCc',
+    },
     { Name        => 'Notify Requestors and Ccs as Comment',              # loc
       Description => 'Send mail to requestors and Ccs as a comment',      # loc
       ExecModule  => 'NotifyAsComment',
index 610a79c..da14e72 100644 (file)
@@ -46,7 +46,7 @@ CREATE TABLE Links (
   id INTEGER NOT NULL  AUTO_INCREMENT,
   Base varchar(240) NULL,
   Target varchar(240) NULL,
-  Type varchar(20) NOT NULL,
+  Type varchar(20) CHARACTER SET ascii NOT NULL ,
   LocalTarget integer NOT NULL DEFAULT 0  ,
   LocalBase integer NOT NULL DEFAULT 0  ,
   LastUpdatedBy integer NOT NULL DEFAULT 0  ,
@@ -54,7 +54,7 @@ CREATE TABLE Links (
   Creator integer NOT NULL DEFAULT 0  ,
   Created DATETIME NULL  ,
   PRIMARY KEY (id)
-) ENGINE=InnoDB CHARACTER SET ascii;
+) ENGINE=InnoDB CHARACTER SET utf8;
 
 CREATE INDEX Links2 ON Links (Base,  Type) ;
 CREATE INDEX Links3 ON Links (Target,  Type) ;
index cfd41c8..4a6f0bd 100644 (file)
@@ -58,6 +58,7 @@ our @Initial = (
 s!(?<=Your ticket has been (?:approved|rejected) by { eval { )\$Approval->OwnerObj->Name!\$Approver->Name!
               )
             {
+                $template->SetType('Perl');
                 $template->SetContent($content);
             }
         }
index 4ee50c4..6ca1bdf 100644 (file)
@@ -1,2 +1 @@
-ALTER TABLE ACL DROP COLUMN DelegatedBy;
-ALTER TABLE ACL DROP COLUMN DelegatedFrom;
+ALTER TABLE ACL DROP( DelegatedBy, DelegatedFrom );
index 4ee50c4..9b34ac7 100644 (file)
@@ -1,2 +1,3 @@
-ALTER TABLE ACL DROP COLUMN DelegatedBy;
-ALTER TABLE ACL DROP COLUMN DelegatedFrom;
+ALTER TABLE ACL
+    DROP COLUMN DelegatedBy,
+    DROP COLUMN DelegatedFrom;
index 4ee50c4..9b34ac7 100644 (file)
@@ -1,2 +1,3 @@
-ALTER TABLE ACL DROP COLUMN DelegatedBy;
-ALTER TABLE ACL DROP COLUMN DelegatedFrom;
+ALTER TABLE ACL
+    DROP COLUMN DelegatedBy,
+    DROP COLUMN DelegatedFrom;
index 065776d..bcf5b1f 100644 (file)
@@ -6,15 +6,21 @@ 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;
+ALTER TABLE Groups ADD(
+     Creator NUMBER(11,0) DEFAULT 0 NOT NULL,
+     Created DATE,
+     LastUpdatedBy NUMBER(11,0) DEFAULT 0 NOT NULL,
+     LastUpdated DATE
+);
+ALTER TABLE GroupMembers ADD(
+    Creator NUMBER(11,0) DEFAULT 0 NOT NULL,
+    Created DATE,
+    LastUpdatedBy NUMBER(11,0) DEFAULT 0 NOT NULL,
+    LastUpdated DATE
+);
+ALTER TABLE ACL ADD(
+    Creator NUMBER(11,0) DEFAULT 0 NOT NULL,
+    Created DATE,
+    LastUpdatedBy NUMBER(11,0) DEFAULT 0 NOT NULL,
+    LastUpdated DATE
+);
index cea2c44..cd91901 100644 (file)
@@ -6,15 +6,18 @@ 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;
+ALTER TABLE Groups
+    ADD COLUMN Creator integer NOT NULL DEFAULT 0,
+    ADD COLUMN Created TIMESTAMP NULL,
+    ADD COLUMN LastUpdatedBy integer NOT NULL DEFAULT 0,
+    ADD COLUMN LastUpdated TIMESTAMP NULL;
+ALTER TABLE GroupMembers
+    ADD COLUMN Creator integer NOT NULL DEFAULT 0,
+    ADD COLUMN Created TIMESTAMP NULL,
+    ADD COLUMN LastUpdatedBy integer NOT NULL DEFAULT 0,
+    ADD COLUMN LastUpdated TIMESTAMP NULL;
+ALTER TABLE ACL
+    ADD COLUMN Creator integer NOT NULL DEFAULT 0,
+    ADD COLUMN Created TIMESTAMP NULL,
+    ADD COLUMN LastUpdatedBy integer NOT NULL DEFAULT 0,
+    ADD COLUMN LastUpdated TIMESTAMP NULL;
index fe5018c..83f2f40 100644 (file)
@@ -6,15 +6,18 @@ 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 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,
+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,
+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;
index 3c75c91..70b4a12 100644 (file)
@@ -1,6 +1,12 @@
 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;
+
+ALTER TABLE CustomFields ADD(
+    BasedOn NUMBER(11,0) NULL,
+    RenderType VARCHAR2(64) NULL,
+    ValuesClass VARCHAR2(64) NULL
+);
+
+ALTER TABLE Queues ADD(
+    SubjectTag VARCHAR2(120) NULL,
+    Lifecycle VARCHAR2(32) NULL
+);
index 1704fa6..d6fe7cc 100644 (file)
@@ -1,6 +1,10 @@
 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;
+
+ALTER TABLE CustomFields
+    ADD COLUMN BasedOn INTEGER NULL,
+    ADD COLUMN RenderType VARCHAR(64) NULL,
+    ADD COLUMN ValuesClass VARCHAR(64) NULL;
+
+ALTER TABLE Queues
+    ADD COLUMN SubjectTag VARCHAR(120) NULL,
+    ADD COLUMN Lifecycle VARCHAR(32) NULL;
index 4cbed6c..0e61d64 100644 (file)
@@ -1,6 +1,10 @@
 ALTER TABLE Users ADD COLUMN AuthToken VARCHAR(16) CHARACTER SET ascii NULL;
-ALTER TABLE CustomFields ADD COLUMN BasedOn INTEGER 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,
+
+ALTER TABLE Queues
+    ADD COLUMN SubjectTag VARCHAR(120) NULL,
     ADD COLUMN Lifecycle VARCHAR(32) NULL;
index d12e27a..1f56d3b 100644 (file)
@@ -1,3 +1,4 @@
+DROP TABLE IF EXISTS Classes;
 CREATE TABLE Classes (
 id SERIAL,
 Name varchar(255) NOT NULL DEFAULT '',
@@ -12,6 +13,7 @@ HotList smallint NOT NULL DEFAULT 0,
 PRIMARY KEY (id)
 );
 
+DROP TABLE IF EXISTS Articles;
 CREATE TABLE Articles (
 id SERIAL,
 Name varchar(255) NOT NULL DEFAULT '',
@@ -28,6 +30,7 @@ PRIMARY KEY (id)
 );
 
 
+DROP TABLE IF EXISTS Topics;
 CREATE TABLE Topics (
 id SERIAL,
 Parent integer NOT NULL DEFAULT 0,
@@ -39,6 +42,7 @@ PRIMARY KEY (id)
 );
 
 
+DROP TABLE IF EXISTS ObjectTopics;
 CREATE TABLE ObjectTopics (
 id SERIAL,
 Topic integer NOT NULL,
@@ -48,6 +52,7 @@ PRIMARY KEY (id)
 );
 
 
+DROP TABLE IF EXISTS ObjectClasses;
 CREATE TABLE ObjectClasses (
 id SERIAL,
 Class integer NOT NULL,
index 29ed7e8..b5af936 100644 (file)
@@ -1,3 +1,4 @@
+DROP TABLE IF EXISTS Classes;
 CREATE TABLE Classes (
 id INTEGER PRIMARY KEY,
 Name varchar(255) NOT NULL DEFAULT '',
@@ -11,6 +12,7 @@ LastUpdated TIMESTAMP NULL,
 HotList smallint NOT NULL DEFAULT 0
 );
 
+DROP TABLE IF EXISTS Articles;
 CREATE TABLE Articles (
 id INTEGER PRIMARY KEY,
 Name varchar(255) NOT NULL DEFAULT '',
@@ -25,7 +27,7 @@ LastUpdatedBy integer NOT NULL DEFAULT 0,
 LastUpdated TIMESTAMP NULL
 );
 
-
+DROP TABLE IF EXISTS Topics;
 CREATE TABLE Topics (
 id INTEGER PRIMARY KEY,
 Parent integer NOT NULL DEFAULT 0,
@@ -36,6 +38,7 @@ ObjectId integer NOT NULL
 );
 
 
+DROP TABLE IF EXISTS ObjectTopics;
 CREATE TABLE ObjectTopics (
 id INTEGER PRIMARY KEY,
 Topic integer NOT NULL,
@@ -43,6 +46,7 @@ ObjectType varchar(64) NOT NULL DEFAULT '',
 ObjectId integer NOT NULL
 );
 
+DROP TABLE IF EXISTS ObjectClasses;
 CREATE TABLE ObjectClasses (
 id INTEGER PRIMARY KEY,
 Class integer NOT NULL,
index e7ed84d..4eaa3a1 100644 (file)
@@ -1,3 +1,4 @@
+DROP TABLE IF EXISTS Classes;
 CREATE TABLE Classes (
   id int(11) NOT NULL auto_increment,
   Name varchar(255) NOT NULL default '',
@@ -12,6 +13,7 @@ CREATE TABLE Classes (
   PRIMARY KEY  (id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
+DROP TABLE IF EXISTS Articles;
 CREATE TABLE Articles (
   id int(11) NOT NULL auto_increment,
   Name varchar(255) NOT NULL default '',
@@ -27,6 +29,7 @@ CREATE TABLE Articles (
   PRIMARY KEY  (id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
+DROP TABLE IF EXISTS Topics;
 CREATE TABLE Topics (
   id int(11) NOT NULL auto_increment,
   Parent int(11) NOT NULL default '0',
@@ -37,6 +40,7 @@ CREATE TABLE Topics (
   PRIMARY KEY  (id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
+DROP TABLE IF EXISTS ObjectTopics;
 CREATE TABLE ObjectTopics (
   id int(11) NOT NULL auto_increment,
   Topic int(11) NOT NULL default '0',
@@ -45,6 +49,7 @@ CREATE TABLE ObjectTopics (
   PRIMARY KEY  (id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
+DROP TABLE IF EXISTS ObjectClasses;
 CREATE TABLE ObjectClasses (
   id int(11) NOT NULL auto_increment,
   Class int(11) NOT NULL default '0',
diff --git a/etc/upgrade/4.0-customfield-checkbox-extension b/etc/upgrade/4.0-customfield-checkbox-extension
new file mode 100644 (file)
index 0000000..a9f5dc5
--- /dev/null
@@ -0,0 +1,86 @@
+#!/usr/bin/perl
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2014 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::CustomFields;
+my $cfs = RT::CustomFields->new( RT->SystemUser );
+$cfs->{find_disabled_rows} = 1;
+$cfs->Limit(
+    FIELD => 'Type',
+    VALUE => 'SelectCheckbox',
+);
+
+while ( my $cf = $cfs->Next ) {
+    print 'Processing custom field #' . $cf->id . "\n";
+    my ( $ret, $msg ) = $cf->SetType('Select');
+    unless ($ret) {
+        warn "Failed to set custom field #"
+          . $cf->id
+          . " Type to 'Select': $msg\n";
+    }
+
+    ( $ret, $msg ) = $cf->SetRenderType('List');
+    unless ($ret) {
+        warn "Failed to set custom field #"
+          . $cf->id
+          . " RenderType to 'List': $msg\n";
+    }
+}
+
+print "DONE\n";
+
+exit 0;
index 4590585..33ea738 100644 (file)
@@ -26,5 +26,4 @@ UPDATE ObjectScrips SET Stage = 'TransactionCreate' WHERE Stage = 'Disabled';
 
 CREATE UNIQUE INDEX ObjectScrips1 ON ObjectScrips (ObjectId, Scrip);
 
-ALTER TABLE Scrips DROP COLUMN Stage;
-ALTER TABLE Scrips DROP COLUMN Queue;
+ALTER TABLE Scrips DROP( Stage, Queue );
index 25fe3c1..91ba5a6 100644 (file)
@@ -1,5 +1,7 @@
-CREATE SEQUENCE objectscrips_id_seq;
+DROP TABLE IF EXISTS ObjectScrips;
+DROP SEQUENCE IF EXISTS objectscrips_id_seq;
 
+CREATE SEQUENCE objectscrips_id_seq;
 CREATE TABLE ObjectScrips (
   id INTEGER DEFAULT nextval('objectscrips_id_seq'),
   Scrip integer NOT NULL,
@@ -29,5 +31,6 @@ UPDATE ObjectScrips SET Stage = 'TransactionCreate' WHERE Stage = 'Disabled';
 
 CREATE UNIQUE INDEX ObjectScrips1 ON ObjectScrips (ObjectId, Scrip);
 
-ALTER TABLE Scrips DROP COLUMN Stage;
-ALTER TABLE Scrips DROP COLUMN Queue;
+ALTER TABLE Scrips
+    DROP COLUMN Stage,
+    DROP COLUMN Queue;
index a3a34f0..2a6a2c4 100644 (file)
@@ -1,4 +1,4 @@
-
+DROP TABLE IF EXISTS ObjectScrips;
 CREATE TABLE ObjectScrips (
   id INTEGER NOT NULL  ,
   Scrip int NOT NULL  ,
index d285019..82f3f84 100644 (file)
@@ -1,3 +1,4 @@
+DROP TABLE IF EXISTS ObjectScrips;
 CREATE TABLE ObjectScrips (
   id INTEGER NOT NULL  AUTO_INCREMENT,
   Scrip integer NOT NULL  ,
@@ -26,5 +27,6 @@ UPDATE ObjectScrips SET Stage = 'TransactionCreate' WHERE Stage = 'Disabled';
 
 CREATE UNIQUE INDEX ObjectScrips1 ON ObjectScrips (ObjectId, Scrip);
 
-ALTER TABLE Scrips DROP COLUMN Stage;
-ALTER TABLE Scrips DROP COLUMN Queue;
+ALTER TABLE Scrips
+    DROP COLUMN Stage,
+    DROP COLUMN Queue;
index 5c4609c..f626093 100644 (file)
@@ -1,2 +1 @@
-ALTER TABLE Scrips DROP COLUMN ConditionRules;
-ALTER TABLE Scrips DROP COLUMN ActionRules;
+ALTER TABLE Scrips DROP( ConditionRules, ActionRules );
index 5c4609c..0b45d51 100644 (file)
@@ -1,2 +1,3 @@
-ALTER TABLE Scrips DROP COLUMN ConditionRules;
-ALTER TABLE Scrips DROP COLUMN ActionRules;
+ALTER TABLE Scrips
+    DROP COLUMN ConditionRules,
+    DROP COLUMN ActionRules;
index 5c4609c..0b45d51 100644 (file)
@@ -1,2 +1,3 @@
-ALTER TABLE Scrips DROP COLUMN ConditionRules;
-ALTER TABLE Scrips DROP COLUMN ActionRules;
+ALTER TABLE Scrips
+    DROP COLUMN ConditionRules,
+    DROP COLUMN ActionRules;
index 3454f78..2e6a78c 100644 (file)
@@ -15,7 +15,7 @@ our @Initial = (sub {
         # Switch from "CreatedMonthly" to "Created.Monthly"
         $content->{GroupBy} ||= [delete $content->{PrimaryGroupBy}];
         for (@{$content->{GroupBy}}) {
-            next if /\./;
+            next if !defined || /\./;
             s/(?<=[a-z])(?=[A-Z])/./;
         }
 
index 4e938e1..2371a5d 100644 (file)
@@ -1,2 +1 @@
-ALTER TABLE Templates DROP COLUMN Language;
-ALTER TABLE Templates DROP COLUMN TranslationOf;
+ALTER TABLE Templates DROP( Language, TranslationOf );
index 4e938e1..cfaa9a7 100644 (file)
@@ -1,2 +1,3 @@
-ALTER TABLE Templates DROP COLUMN Language;
-ALTER TABLE Templates DROP COLUMN TranslationOf;
+ALTER TABLE Templates
+    DROP COLUMN Language,
+    DROP COLUMN TranslationOf;
index 4e938e1..cfaa9a7 100644 (file)
@@ -1,2 +1,3 @@
-ALTER TABLE Templates DROP COLUMN Language;
-ALTER TABLE Templates DROP COLUMN TranslationOf;
+ALTER TABLE Templates
+    DROP COLUMN Language,
+    DROP COLUMN TranslationOf;
index afed9c3..edde022 100644 (file)
@@ -33,7 +33,7 @@ our @Initial = (
 });
         }
         else {
-            RT->Logger->error('Current "Forward" template is not the default version, please check docs/4.2-UPGRADING');
+            RT->Logger->error('Current "Forward" template is not the default version, please check docs/UPGRADING-4.2');
         }
 
         my $forward_ticket_template = RT::Template->new(RT->SystemUser);
@@ -50,7 +50,7 @@ This is a forward of ticket #{ $Ticket->id }
 
         }
         else {
-            RT->Logger->error('Current "Forward Ticket" template is not the default version, please check docs/4.2-UPGRADING');
+            RT->Logger->error('Current "Forward Ticket" template is not the default version, please check docs/UPGRADING-4.2');
         }
     },
 );
index 4cf1bdb..0ed1dda 100644 (file)
@@ -6,7 +6,6 @@ our @Initial = (
     sub {
         require RT::Scrips;
         my $scrips = RT::Scrips->new( RT->SystemUser );
-        $scrips->{'find_disabled_rows'} = 1;
         $scrips->UnLimit;
         while ( my $scrip = $scrips->Next ) {
             my $id = $scrip->Template;
diff --git a/etc/upgrade/4.2.4/content b/etc/upgrade/4.2.4/content
new file mode 100644 (file)
index 0000000..c56e369
--- /dev/null
@@ -0,0 +1,47 @@
+use strict;
+use warnings;
+
+our @ScripActions = (
+    { Name        => 'Open Inactive Tickets',                             # loc
+      Description => 'Open inactive tickets',                             # loc
+      ExecModule  => 'AutoOpenInactive' },
+);
+
+# Ignore the above if there is already an AutoOpenInactive in the
+# database -- i.e. originally a 4.2 install, or someone added one
+# themselves.
+our @Initial;
+push @Initial, sub {
+    my $exist = RT::ScripAction->new( RT->SystemUser );
+    $exist->LoadByCols( ExecModule => 'AutoOpenInactive' );
+    @ScripActions = () if $exist->Id;
+};
+
+push @Initial, sub {
+    my $queue = RT::Queue->new( RT->SystemUser );
+    my ($ok, $msg) = $queue->Load('___Approvals');
+    unless ($ok) {
+        RT->Logger->warning("Unable to load ___Approvals: $msg");
+        return;
+    }
+    unless ($queue->Disabled == 2) {
+        RT->Logger->warning("Going to force ___Approvals queue to be Disabled = 2");
+        ($ok, $msg) = $queue->SetDisabled( 2 );
+        unless ($ok) {
+            RT->Logger->error("Unable to set ___Approvals.Disabled = 2: $msg");
+            return;
+        }
+    }
+
+    unless ($queue->Lifecycle eq "approvals") {
+        RT->Logger->warning("Going to force ___Approvals queue to the approvals lifecycle");
+        ($ok, $msg) = $queue->SetLifecycle( "approvals" );
+        unless ($ok) {
+            RT->Logger->error("Unable to set ___Approvals lifecycle: $msg");
+            return;
+        }
+    }
+
+    return 1;
+
+};
diff --git a/etc/upgrade/4.2.6/content b/etc/upgrade/4.2.6/content
new file mode 100644 (file)
index 0000000..e17c5ea
--- /dev/null
@@ -0,0 +1,9 @@
+use strict;
+use warnings;
+
+our @ScripActions = (
+    { Name        => 'Notify Owner or AdminCcs',                         # loc
+      Description => 'Sends mail to the Owner if set, otherwise administrative Ccs',    # loc
+      ExecModule  => 'NotifyOwnerOrAdminCc',
+    },
+);
diff --git a/etc/upgrade/4.2.6/schema.mysql b/etc/upgrade/4.2.6/schema.mysql
new file mode 100644 (file)
index 0000000..71f8f64
--- /dev/null
@@ -0,0 +1,4 @@
+ALTER TABLE Links
+  DEFAULT CHARACTER SET utf8,
+  MODIFY Base   varchar(240) CHARACTER SET utf8 NULL,
+  MODIFY Target varchar(240) CHARACTER SET utf8 NULL;
diff --git a/etc/upgrade/4.2.7/content b/etc/upgrade/4.2.7/content
new file mode 100644 (file)
index 0000000..e828cc7
--- /dev/null
@@ -0,0 +1,15 @@
+use strict;
+use warnings;
+
+our @Initial = (
+    sub {
+        # We do the delete in pure SQL because Attribute collections
+        # otherwise attempt to hash everything in memory.  As this may
+        # be a large list, do it directly.
+        RT->DatabaseHandle->dbh->do(<<EOSQL);
+            DELETE FROM Attributes
+             WHERE (Name = 'LinkValueTo' OR Name = 'IncludeContentForValue')
+               AND (LENGTH(Content) = 0 OR Content IS NULL)
+EOSQL
+    },
+);
diff --git a/etc/upgrade/4.2.8/content b/etc/upgrade/4.2.8/content
new file mode 100644 (file)
index 0000000..64b61de
--- /dev/null
@@ -0,0 +1,16 @@
+use strict;
+use warnings;
+
+our @Initial = (
+    sub {
+        # This upgrade step is identical to the 4.2.7 upgrade, but only
+        # runs on Oracle because 4.2.7 was originally released with
+        # flawed SQL which did not run on Oracle.
+        return unless RT->Config->Get('DatabaseType') eq 'Oracle';
+        RT->DatabaseHandle->dbh->do(<<EOSQL);
+            DELETE FROM Attributes
+             WHERE (Name = 'LinkValueTo' OR Name = 'IncludeContentForValue')
+               AND (LENGTH(Content) = 0 OR Content IS NULL)
+EOSQL
+    },
+);
index 3bc1b6d..0b78f0f 100644 (file)
@@ -83,10 +83,11 @@ $txns->Limit(
 
 $txns->Limit(
     ALIAS => $alias,
-    FIELD => 'Type',
+    FIELD => 'Name',
     OPERATOR => '=',
     VALUE => 'UserEquiv',
     QUOTEVALUE => 1,
+    CASESENSITIVE => 0,
     ENTRYAGGREGATOR => 'AND',
 );
 
index 67ee104..46961aa 100644 (file)
@@ -86,7 +86,7 @@ sub fix_time_worked_history {
         } elsif ( $txn->Type eq 'Set' && $txn->Field eq 'TimeWorked' ) {
             $history += $txn->NewValue - $txn->OldValue;
             $candidate = $txn;
-        } elsif ( $candidate && $txn->Field eq 'MergedInto' ) {
+        } elsif ( $candidate && ($txn->Field||'') eq 'MergedInto' ) {
             if ($candidate->Creator eq $txn->Creator ) {
                 push @delete, $candidate;
                 $delete_time += $candidate->NewValue - $candidate->OldValue;
index 17aaffb..2c477b2 100644 (file)
--- a/lib/RT.pm
+++ b/lib/RT.pm
@@ -53,9 +53,11 @@ use 5.010;
 package RT;
 
 
+use Encode ();
 use File::Spec ();
 use Cwd ();
 use Scalar::Util qw(blessed);
+use UNIVERSAL::require;
 
 use vars qw($Config $System $SystemUser $Nobody $Handle $Logger $_Privileged $_Unprivileged $_INSTALL_MODE);
 
@@ -262,6 +264,9 @@ sub InitLogging {
             $frame++ while caller($frame) && caller($frame) =~ /^Log::/;
             my ($package, $filename, $line) = caller($frame);
 
+            # Encode to bytes, so we don't send wide characters
+            $p{message} = Encode::encode("UTF-8", $p{message});
+
             $p{'message'} =~ s/(?:\r*\n)+$//;
             return "[$$] [". gmtime(time) ."] [". $p{'level'} ."]: "
                 . $p{'message'} ." ($filename:$line)\n";
@@ -277,8 +282,8 @@ sub InitLogging {
             $frame++ while caller($frame) && caller($frame) =~ /^Log::/;
             my ($package, $filename, $line) = caller($frame);
 
-            # syswrite() cannot take utf8; turn it off here.
-            Encode::_utf8_off($p{message});
+            # Encode to bytes, so we don't send wide characters
+            $p{message} = Encode::encode("UTF-8", $p{message});
 
             $p{message} =~ s/(?:\r*\n)+$//;
             if ($p{level} eq 'debug') {
@@ -368,19 +373,9 @@ sub InitSignalHandlers {
 ## mechanism (see above).
 
     $SIG{__WARN__} = sub {
-        # The 'wide character' warnings has to be silenced for now, at least
-        # until HTML::Mason offers a sane way to process both raw output and
-        # unicode strings.
         # use 'goto &foo' syntax to hide ANON sub from stack
-        if( index($_[0], 'Wide character in ') != 0 ) {
-            unshift @_, $RT::Logger, qw(level warning message);
-            goto &Log::Dispatch::log;
-        }
-        # Return value is used only by RT::Test to filter warnings from
-        # reaching the Test::NoWarnings catcher.  If Log::Dispatch::log() ever
-        # starts returning 'IGNORE', we'll need to switch to something more
-        # clever.  I don't expect that to happen.
-        return 'IGNORE';
+        unshift @_, $RT::Logger, qw(level warning message);
+        goto &Log::Dispatch::log;
     };
 
 #When we call die, trap it and log->crit with the value of the die.
@@ -495,8 +490,7 @@ sub InitClasses {
         }
 
         foreach my $class ( grep $_, RT->Config->Get('CustomFieldValuesSources') ) {
-            local $@;
-            eval "require $class; 1" or $RT::Logger->error(
+            $class->require or $RT::Logger->error(
                 "Class '$class' is listed in CustomFieldValuesSources option"
                 ." in the config, but we failed to load it:\n$@\n"
             );
@@ -524,6 +518,7 @@ sub _BuildTableAttributes {
         RT::ScripAction
         RT::ScripCondition
         RT::Scrip
+        RT::ObjectScrip
         RT::Group
         RT::GroupMember
         RT::CustomField
@@ -702,7 +697,9 @@ sub InitPluginPaths {
     my @tmp_inc;
     my $added;
     for (@INC) {
-        if ( Cwd::realpath($_) eq $RT::LocalLibPath) {
+        my $realpath = Cwd::realpath($_);
+        next unless defined $realpath;
+        if ( $realpath eq $RT::LocalLibPath) {
             push @tmp_inc, $_, @lib_dirs;
             $added = 1;
         } else {
@@ -914,6 +911,7 @@ sub Deprecated {
         Instead => undef,
         Message => undef,
         Stack   => 1,
+        LogLevel => "warn",
         @_,
     );
 
@@ -950,7 +948,9 @@ sub Deprecated {
         if $args{Object};
 
     $msg .= "  Call stack:\n$stack" if $args{Stack};
-    RT->Logger->warn($msg);
+
+    my $loglevel = $args{LogLevel};
+    RT->Logger->$loglevel($msg);
 }
 
 =head1 BUGS
index a88a6ee..ea640fe 100644 (file)
@@ -535,15 +535,11 @@ sub _ParseMultilineTemplate {
     my %args = (@_);
 
     my $template_id;
-    require Encode;
-    require utf8;
     my ( $queue, $requestor );
         $RT::Logger->debug("Line: ===");
         foreach my $line ( split( /\n/, $args{'Content'} ) ) {
             $line =~ s/\r$//;
-            $RT::Logger->debug( "Line: " . utf8::is_utf8($line)
-                ? Encode::encode_utf8($line)
-                : $line );
+            $RT::Logger->debug( "Line: $line" );
             if ( $line =~ /^===/ ) {
                 if ( $template_id && !$queue && $args{'Queue'} ) {
                     $self->{'templates'}->{$template_id}
@@ -691,7 +687,7 @@ sub ParseLines {
             eval {
                 $dateobj->Set( Format => 'iso', Value => $args{$date} );
             };
-            if ($@ or $dateobj->Unix <= 0) {
+            if ($@ or not $dateobj->IsSet) {
                 $dateobj->Set( Format => 'unknown', Value => $args{$date} );
             }
         }
@@ -740,10 +736,10 @@ sub ParseLines {
     );
 
     if ( $args{content} ) {
-        my $mimeobj = MIME::Entity->new();
-        $mimeobj->build(
-            Type => $args{'contenttype'} || 'text/plain',
-            Data => $args{'content'}
+        my $mimeobj = MIME::Entity->build(
+            Type    => $args{'contenttype'} || 'text/plain',
+            Charset => 'UTF-8',
+            Data    => [ map {Encode::encode( "UTF-8", $_ )} @{$args{'content'}} ],
         );
         $ticketargs{MIMEObj} = $mimeobj;
         $ticketargs{UpdateType} = $args{'updatetype'} || 'correspond';
@@ -756,14 +752,22 @@ sub ParseLines {
             $ticketargs{ "CustomField-" . $1 } = $args{$tag};
         } elsif ( $orig_tag =~ /^(?:customfield|cf)-?(.+)$/i ) {
             my $cf = RT::CustomField->new( $self->CurrentUser );
-            $cf->LoadByName( Name => $1, Queue => $ticketargs{Queue} );
-            $cf->LoadByName( Name => $1, Queue => 0 ) unless $cf->id;
+            $cf->LoadByName(
+                Name          => $1,
+                LookupType    => RT::Ticket->CustomFieldLookupType,
+                ObjectId      => $ticketargs{Queue},
+                IncludeGlobal => 1,
+            );
             next unless $cf->id;
             $ticketargs{ "CustomField-" . $cf->id } = $args{$tag};
         } elsif ($orig_tag) {
             my $cf = RT::CustomField->new( $self->CurrentUser );
-            $cf->LoadByName( Name => $orig_tag, Queue => $ticketargs{Queue} );
-            $cf->LoadByName( Name => $orig_tag, Queue => 0 ) unless $cf->id;
+            $cf->LoadByName(
+                Name          => $orig_tag,
+                LookupType    => RT::Ticket->CustomFieldLookupType,
+                ObjectId      => $ticketargs{Queue},
+                IncludeGlobal => 1,
+            );
             next unless $cf->id;
             $ticketargs{ "CustomField-" . $cf->id } = $args{$tag};
 
index 046d6dd..344604c 100644 (file)
@@ -104,7 +104,7 @@ sub Prepare  {
 
     # If we don't have a due date, adjust the priority by one
     # until we hit the final priority
-    if ($due->Unix() < 1) {
+    if (not $due->IsSet) {
         if ( $self->TicketObj->Priority > $self->TicketObj->FinalPriority ){
             $self->{'prio'} = ($self->TicketObj->Priority - 1);
             return 1;
index e6260aa..fb2f6ea 100644 (file)
@@ -155,8 +155,7 @@ sub Prepare {
 
     my $ticket = $self->TicketObj;
 
-    my $due = $ticket->DueObj->Unix;
-    unless ( $due > 0 ) {
+    unless ( $ticket->DueObj->IsSet ) {
         $RT::Logger->debug('Due is not set. Not escalating.');
         return 1;
     }
@@ -181,9 +180,8 @@ sub Prepare {
     # now we know we have a due date. for every day that passes,
     # increment priority according to the formula
 
-    my $starts         = $ticket->StartsObj->Unix;
-    $starts            = $ticket->CreatedObj->Unix unless $starts > 0;
-    my $now            = time;
+    my $starts = $ticket->StartsObj->IsSet ? $ticket->StartsObj->Unix : $ticket->CreatedObj->Unix;
+    my $now    = time;
 
     # do nothing if we didn't reach starts or created date
     if ( $starts > $now ) {
@@ -191,6 +189,7 @@ sub Prepare {
         return 1;
     }
 
+    my $due = $ticket->DueObj->Unix;
     $due = $starts + 1 if $due <= $starts; # +1 to avoid div by zero
 
     my $percent_complete = ($now-$starts)/($due - $starts);
diff --git a/lib/RT/Action/NotifyOwnerOrAdminCc.pm b/lib/RT/Action/NotifyOwnerOrAdminCc.pm
new file mode 100644 (file)
index 0000000..b99c7d9
--- /dev/null
@@ -0,0 +1,76 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2014 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 }}}
+
+package RT::Action::NotifyOwnerOrAdminCc;
+
+use strict;
+use warnings;
+
+use base qw(RT::Action::Notify);
+
+use Email::Address;
+
+=head1 Notify Owner or AdminCc
+
+If the owner of this ticket is Nobody, notify the AdminCcs.  Otherwise, only notify the Owner.
+
+=cut
+
+sub Argument {
+    my $self = shift;
+    my $ticket = $self->TicketObj;
+    if ($ticket->Owner == RT->Nobody->id) {
+        return 'AdminCc';
+    } else {
+        return 'Owner';
+    }
+}
+
+RT::Base->_ImportOverlays();
+
+1;
index 1266d21..c200b01 100644 (file)
@@ -257,7 +257,7 @@ sub Bcc {
 sub AddressesFromHeader {
     my $self      = shift;
     my $field     = shift;
-    my $header    = $self->TemplateObj->MIMEObj->head->get($field);
+    my $header    = Encode::decode("UTF-8",$self->TemplateObj->MIMEObj->head->get($field));
     my @addresses = Email::Address->parse($header);
 
     return (@addresses);
@@ -276,7 +276,7 @@ sub SendMessage {
     # ability to pass @_ to a 'post' routine.
     my ( $self, $MIMEObj ) = @_;
 
-    my $msgid = $MIMEObj->head->get('Message-ID');
+    my $msgid = Encode::decode( "UTF-8", $MIMEObj->head->get('Message-ID') );
     chomp $msgid;
 
     $self->ScripActionObj->{_Message_ID}++;
@@ -299,7 +299,7 @@ sub SendMessage {
 
     my $success = $msgid . " sent ";
     foreach (@EMAIL_RECIPIENT_HEADERS) {
-        my $recipients = $MIMEObj->head->get($_);
+        my $recipients = Encode::decode( "UTF-8", $MIMEObj->head->get($_) );
         $success .= " $_: " . $recipients if $recipients;
     }
 
@@ -531,7 +531,7 @@ sub RecordOutgoingMailTransaction {
         $type = 'EmailRecord';
     }
 
-    my $msgid = $MIMEObj->head->get('Message-ID');
+    my $msgid = Encode::decode( "UTF-8", $MIMEObj->head->get('Message-ID') );
     chomp $msgid;
 
     my ( $id, $msg ) = $transaction->Create(
@@ -643,7 +643,7 @@ sub DeferDigestRecipients {
 
         # Have to get the list of addresses directly from the MIME header
         # at this point.
-        $RT::Logger->debug( $self->TemplateObj->MIMEObj->head->as_string );
+        $RT::Logger->debug( Encode::decode( "UTF-8", $self->TemplateObj->MIMEObj->head->as_string ) );
         foreach my $rcpt ( map { $_->address } $self->AddressesFromHeader($mailfield) ) {
             next unless $rcpt;
             my $user_obj = RT::User->new(RT->SystemUser);
@@ -752,7 +752,7 @@ sub RemoveInappropriateRecipients {
     # If there are no recipients, don't try to send the message.
     # If the transaction has content and has the header RT-Squelch-Replies-To
 
-    my $msgid = $self->TemplateObj->MIMEObj->head->get('Message-Id');
+    my $msgid = Encode::decode( "UTF-8", $self->TemplateObj->MIMEObj->head->get('Message-Id') );
     chomp $msgid;
 
     if ( my $attachment = $self->TransactionObj->Attachments->First ) {
@@ -967,7 +967,8 @@ sub GetFriendlyName {
 
 =head2 SetHeader FIELD, VALUE
 
-Set the FIELD of the current MIME object into VALUE.
+Set the FIELD of the current MIME object into VALUE, which should be in
+characters, not bytes.  Returns the new header, in bytes.
 
 =cut
 
@@ -980,7 +981,7 @@ sub SetHeader {
     chomp $field;
     my $head = $self->TemplateObj->MIMEObj->head;
     $head->fold_length( $field, 10000 );
-    $head->replace( $field, $val );
+    $head->replace( $field, Encode::encode( "UTF-8", $val ) );
     return $head->get($field);
 }
 
@@ -1021,7 +1022,7 @@ sub SetSubject {
 
     $subject =~ s/(\r\n|\n|\s)/ /g;
 
-    $self->SetHeader( 'Subject', Encode::encode_utf8( $subject ) );
+    $self->SetHeader( 'Subject', $subject );
 
 }
 
@@ -1037,11 +1038,9 @@ sub SetSubjectToken {
     my $head = $self->TemplateObj->MIMEObj->head;
     $self->SetHeader(
         Subject =>
-            Encode::encode_utf8(
-                RT::Interface::Email::AddSubjectTag(
-                    Encode::decode_utf8( $head->get('Subject') ),
-                    $self->TicketObj,
-                ),
+            RT::Interface::Email::AddSubjectTag(
+                Encode::decode( "UTF-8", $head->get('Subject') ),
+                $self->TicketObj,
             ),
     );
 }
@@ -1130,7 +1129,8 @@ sub PseudoReference {
 
 =head2 SetHeaderAsEncoding($field_name, $charset_encoding)
 
-This routine converts the field into specified charset encoding.
+This routine converts the field into specified charset encoding, then
+applies the MIME-Header transfer encoding.
 
 =cut
 
@@ -1140,8 +1140,8 @@ sub SetHeaderAsEncoding {
 
     my $head = $self->TemplateObj->MIMEObj->head;
 
-    my $value = $head->get( $field );
-    $value = $self->MIMEEncodeString( $value, $enc );
+    my $value = Encode::decode("UTF-8", $head->get( $field ));
+    $value = $self->MIMEEncodeString( $value, $enc ); # Returns bytes
     $head->replace( $field, $value );
 
 }
@@ -1151,7 +1151,8 @@ sub SetHeaderAsEncoding {
 Takes a perl string and optional encoding pass it over
 L<RT::Interface::Email/EncodeToMIME>.
 
-Basicly encode a string using B encoding according to RFC2047.
+Basicly encode a string using B encoding according to RFC2047, returning
+bytes.
 
 =cut
 
index 0a7949c..914e748 100644 (file)
@@ -110,12 +110,12 @@ sub Prepare {
     my $txn_attachment = $self->TransactionObj->Attachments->First;
     for my $header (qw/From To Cc Bcc/) {
         if ( $txn_attachment->GetHeader( $header ) ) {
-            $mime->head->set( $header => $txn_attachment->GetHeader($header) );
+            $mime->head->replace( $header => Encode::encode( "UTF-8", $txn_attachment->GetHeader($header) ) );
         }
     }
 
     if ( RT->Config->Get('ForwardFromUser') ) {
-        $mime->head->set( 'X-RT-Sign' => 0 );
+        $mime->head->replace( 'X-RT-Sign' => 0 );
     }
 
     $self->SUPER::Prepare();
index 410578a..97a4708 100644 (file)
@@ -595,16 +595,10 @@ sub Search {
     }
 
 
-    require Time::ParseDate;
     foreach my $date (qw(Created< Created> LastUpdated< LastUpdated>)) {
         next unless ( $args{$date} );
-        my ($seconds, $error) = Time::ParseDate::parsedate( $args{$date}, FUZZY => 1, PREFER_PAST => 1 );
-        unless ( defined $seconds ) {
-            $RT::Logger->warning(
-                "Couldn't parse date '$args{$date}' by Time::ParseDate" );
-        }
         my $date_obj = RT::Date->new( $self->CurrentUser );
-        $date_obj->Set( Format => 'unix', Value => $seconds );
+        $date_obj->Set( Format => 'unknown', Value => $args{$date} );
         $dates->{$date} = $date_obj;
 
         if ( $date =~ /^(.*?)<$/i ) {
index 800f4f5..4f1763c 100644 (file)
@@ -130,13 +130,12 @@ sub Create {
     my $head = $Attachment->head;
 
     # Get the subject
-    my $Subject = $head->get( 'subject', 0 );
+    my $Subject = Encode::decode( 'UTF-8', $head->get( 'subject' ) );
     $Subject = '' unless defined $Subject;
     chomp $Subject;
-    utf8::decode( $Subject ) unless utf8::is_utf8( $Subject );
 
     #Get the Message-ID
-    my $MessageId = $head->get( 'Message-ID', 0 );
+    my $MessageId = Encode::decode( "UTF-8", $head->get( 'Message-ID' ) );
     defined($MessageId) or $MessageId = '';
     chomp ($MessageId);
     $MessageId =~ s/^<(.*?)>$/$1/o;
@@ -150,18 +149,15 @@ sub Create {
     my $content;
     unless ( $head->get('Content-Length') ) {
         my $length = 0;
-        if ( defined $Attachment->bodyhandle ) {
-            $content = $Attachment->bodyhandle->as_string;
-            utf8::encode( $content ) if utf8::is_utf8( $content );
-            $length = length $content;
-        }
-        $head->replace( 'Content-Length' => $length );
+        $length = length $Attachment->bodyhandle->as_string
+            if defined $Attachment->bodyhandle;
+        $head->replace( 'Content-Length' => Encode::encode( "UTF-8", $length ) );
     }
     $head = $head->as_string;
 
     # MIME::Head doesn't support perl strings well and can return
     # octets which later will be double encoded in low-level code
-    utf8::decode( $head ) unless utf8::is_utf8( $head );
+    $head = Encode::decode( 'UTF-8', $head );
 
     # If a message has no bodyhandle, that means that it has subparts (or appears to)
     # and we should act accordingly.  
@@ -198,12 +194,9 @@ sub Create {
     #If it's not multipart
     else {
 
-        my ($encoding, $type);
-        ($encoding, $content, $type, $Filename) = $self->_EncodeLOB(
-            $Attachment->bodyhandle->as_string,
-            $Attachment->mime_type,
-            $Filename
-        );
+        my ( $encoding, $type, $note_args );
+        ( $encoding, $content, $type, $Filename, $note_args ) =
+                $self->_EncodeLOB( $Attachment->bodyhandle->as_string, $Attachment->mime_type, $Filename, );
 
         my $id = $self->SUPER::Create(
             TransactionId   => $args{'TransactionId'},
@@ -217,7 +210,12 @@ sub Create {
             MessageId       => $MessageId,
         );
 
-        unless ($id) {
+        if ($id) {
+            if ($note_args) {
+                $self->TransactionObj->Object->_NewTransaction( %$note_args );
+            }
+        }
+        else {
             $RT::Logger->crit("Attachment insert failed: ". $RT::Handle->dbh->errstr);
         }
         return $id;
@@ -341,7 +339,7 @@ before returning it.
 sub Content {
     my $self = shift;
     return $self->_DecodeLOB(
-        $self->ContentType,
+        $self->GetHeader('Content-Type'),  # Includes charset, unlike ->ContentType
         $self->ContentEncoding,
         $self->_Value('Content', decode_utf8 => 0),
     );
@@ -372,7 +370,6 @@ sub OriginalContent {
     }
 
     return $self->Content unless RT::I18N::IsTextualContentType($self->ContentType);
-    my $enc = $self->OriginalEncoding;
 
     my $content;
     if ( !$self->ContentEncoding || $self->ContentEncoding eq 'none' ) {
@@ -385,18 +382,20 @@ sub OriginalContent {
         return( $self->loc("Unknown ContentEncoding [_1]", $self->ContentEncoding));
     }
 
-    # Turn *off* the SvUTF8 bits here so decode_utf8 and from_to below can work.
-    local $@;
-    Encode::_utf8_off($content);
+    my $entity = MIME::Entity->new();
+    $entity->head->add("Content-Type", $self->GetHeader("Content-Type"));
+    $entity->bodyhandle( MIME::Body::Scalar->new( $content ) );
+    my $from = RT::I18N::_FindOrGuessCharset($entity);
+    $from = 'utf-8' if not $from or not Encode::find_encoding($from);
 
-    if (!$enc || $enc eq '' ||  $enc eq 'utf8' || $enc eq 'utf-8') {
-        # If we somehow fail to do the decode, at least push out the raw bits
-        eval { return( Encode::decode_utf8($content)) } || return ($content);
-    }
+    my $to = RT::I18N::_CanonicalizeCharset(
+        $self->OriginalEncoding || 'utf-8'
+    );
 
-    eval { Encode::from_to($content, 'utf8' => $enc) } if $enc;
+    local $@;
+    eval { Encode::from_to($content, $from => $to) };
     if ($@) {
-        $RT::Logger->error("Could not convert attachment from assumed utf8 to '$enc' :".$@);
+        $RT::Logger->error("Could not convert attachment from $from to $to: ".$@);
     }
     return $content;
 }
@@ -436,7 +435,7 @@ sub ContentLength {
 =head2 FriendlyContentLength
 
 Returns L</ContentLength> in bytes, kilobytes, or megabytes as most
-appropriate.  The size is suffixed with C<M>, C<k>, and C<b> and the returned
+appropriate.  The size is suffixed with C<MiB>, C<KiB>, or C<B> and the returned
 string is localized.
 
 Returns the empty string if the L</ContentLength> is 0 or undefined.
@@ -450,13 +449,13 @@ sub FriendlyContentLength {
 
     my $res = '';
     if ( $size > 1024*1024 ) {
-        $res = $self->loc( "[_1]M", int( $size / 1024 / 102.4 ) / 10 );
+        $res = $self->loc( "[_1]MiB", int( $size / 1024 / 102.4 ) / 10 );
     }
     elsif ( $size > 1024 ) {
-        $res = $self->loc( "[_1]k", int( $size / 102.4 ) / 10 );
+        $res = $self->loc( "[_1]KiB", int( $size / 102.4 ) / 10 );
     }
     else {
-        $res = $self->loc( "[_1]b", $size );
+        $res = $self->loc( "[_1]B", $size );
     }
     return $res;
 }
@@ -530,7 +529,7 @@ sub Addresses {
     my $self = shift;
 
     my %data = ();
-    my $current_user_address = lc $self->CurrentUser->EmailAddress;
+    my $current_user_address = lc($self->CurrentUser->EmailAddress || '');
     foreach my $hdr (@ADDRESS_HEADERS) {
         my @Addresses;
         my $line = $self->GetHeader($hdr);
index 797e22e..40e560f 100644 (file)
@@ -181,9 +181,6 @@ sub Create {
         $args{'ContentType'} = 'storable';
     }
 
-    delete $RT::User::PREFERENCES_CACHE{ $args{'ObjectId'} }{ $args{'Name'} }
-        if $args{'ObjectType'} eq 'RT::User';
-
     $self->SUPER::Create(
                          Name => $args{'Name'},
                          Content => $args{'Content'},
@@ -278,11 +275,6 @@ sub SetContent {
     my $self = shift;
     my $content = shift;
 
-    if ( $self->__Value('ObjectType') eq 'RT::User' ) {
-        delete $RT::User::PREFERENCES_CACHE
-            { $self->__Value('ObjectId') }{ $self->__Value('Name') };
-    }
-
     # Call __Value to avoid ACL check.
     if ( ($self->__Value('ContentType')||'') eq 'storable' ) {
         # We eval the serialization because it will lose on a coderef.
@@ -385,6 +377,7 @@ sub Delete {
     unless ($self->CurrentUserHasRight('delete')) {
         return (0,$self->loc('Permission Denied'));
     }
+
     return($self->SUPER::Delete(@_));
 }
 
index f83ed8e..7a20700 100644 (file)
@@ -122,7 +122,7 @@ to this object's CurrentUser->LanguageHandle for localization.
 
 you call it like this:
 
-    $self->loc("I have [quant,_1,concrete mixer].", 6);
+    $self->loc("I have [quant,_1,concrete mixer,concrete mixers].", 6);
 
 In english, this would return:
     I have 6 concrete mixers.
index 7e5f1f7..fa7f364 100644 (file)
@@ -86,7 +86,7 @@ sub IsApplicable {
     my $cur = RT::Date->new( RT->SystemUser );
     $cur->SetToNow();
     my $due = $self->TicketObj->DueObj;
-    return (undef) if $due->Unix <= 0;
+    return (undef) unless $due->IsSet;
 
     my $diff = $due->Diff($cur);
     if ( $diff >= 0 and $diff <= $elapse ) {
index bf044f8..444e373 100644 (file)
@@ -70,7 +70,7 @@ If the due date is before "now" return true
 
 sub IsApplicable {
     my $self = shift;
-    if ($self->TicketObj->DueObj->Unix > 0 and
+    if ($self->TicketObj->DueObj->IsSet and
         $self->TicketObj->DueObj->Unix < time())  {
         return(1);
     }
index a6b3269..4329fdb 100644 (file)
@@ -51,8 +51,10 @@ package RT::Config;
 use strict;
 use warnings;
 
+use 5.010;
 use File::Spec ();
 use Symbol::Global::Name;
+use List::MoreUtils 'uniq';
 
 =head1 NAME
 
@@ -201,8 +203,26 @@ our %META;
         Widget          => '/Widgets/Form/Select',
         WidgetArguments => {
             Description => 'Theme',                  #loc
-            # XXX: we need support for 'get values callback'
-            Values => [qw(rudder web2 aileron ballard)],
+            Callback    => sub {
+                state @stylesheets;
+                unless (@stylesheets) {
+                    for my $static_path ( RT::Interface::Web->StaticRoots ) {
+                        my $css_path =
+                          File::Spec->catdir( $static_path, 'css' );
+                        next unless -d $css_path;
+                        if ( opendir my $dh, $css_path ) {
+                            push @stylesheets, grep {
+                                -e File::Spec->catfile( $css_path, $_, 'base.css' )
+                            } readdir $dh;
+                        }
+                        else {
+                            RT->Logger->error("Can't read $css_path: $!");
+                        }
+                    }
+                    @stylesheets = sort { lc $a cmp lc $b } uniq @stylesheets;
+                }
+                return { Values => \@stylesheets };
+            },
         },
         PostLoadCheck => sub {
             my $self = shift;
@@ -215,10 +235,10 @@ our %META;
 
             $RT::Logger->warning(
                 "The default stylesheet ($value) does not exist in this instance of RT. "
-              . "Defaulting to aileron."
+              . "Defaulting to rudder."
             );
 
-            $self->Set('WebDefaultStylesheet', 'aileron');
+            $self->Set('WebDefaultStylesheet', 'rudder');
         },
     },
     TimeInICal => {
@@ -584,9 +604,7 @@ our %META;
             my $self  = shift;
             my $value = shift;
             return if $value;
-            return if $INC{'GraphViz.pm'};
-            local $@;
-            return if eval {require GraphViz; 1};
+            return if GraphViz->require;
             $RT::Logger->debug("You've enabled GraphViz, but we couldn't load the module: $@");
             $self->Set( DisableGraphViz => 1 );
         },
@@ -597,9 +615,7 @@ our %META;
             my $self  = shift;
             my $value = shift;
             return if $value;
-            return if $INC{'GD.pm'};
-            local $@;
-            return if eval {require GD; 1};
+            return if GD->require;
             $RT::Logger->debug("You've enabled GD, but we couldn't load the module: $@");
             $self->Set( DisableGD => 1 );
         },
@@ -619,6 +635,10 @@ our %META;
         Type => 'ARRAY',
         PostLoadCheck => sub {
             my $self = shift;
+
+            # Make sure Crypt is post-loaded first
+            $META{Crypt}{'PostLoadCheck'}->( $self, $self->Get( 'Crypt' ) );
+
             my @plugins = $self->Get('MailPlugins');
             if ( grep $_ eq 'Auth::GnuPG' || $_ eq 'Auth::SMIME', @plugins ) {
                 $RT::Logger->warning(
@@ -636,6 +656,10 @@ our %META;
                     } @plugins;
                 $self->Set( MailPlugins => @plugins );
             }
+
+            if ( not @{$self->Get('Crypt')->{Incoming}} and grep $_ eq 'Auth::Crypt', @plugins ) {
+                $RT::Logger->warning("Auth::Crypt enabled in MailPlugins, but no available incoming encryption formats");
+            }
         },
     },
     Crypt        => {
@@ -665,11 +689,18 @@ our %META;
             $opt->{'Incoming'} = [ $opt->{'Incoming'} ]
                 if $opt->{'Incoming'} and not ref $opt->{'Incoming'};
             if ( $opt->{'Incoming'} && @{ $opt->{'Incoming'} } ) {
+                $RT::Logger->warning("$_ explicitly set as incoming Crypt plugin, but not marked Enabled; removing")
+                    for grep {not $enabled{$_}} @{$opt->{'Incoming'}};
                 $opt->{'Incoming'} = [ grep {$enabled{$_}} @{$opt->{'Incoming'}} ];
             } else {
                 $opt->{'Incoming'} = \@enabled;
             }
             if ( $opt->{'Outgoing'} ) {
+                if (not $enabled{$opt->{'Outgoing'}}) {
+                    $RT::Logger->warning($opt->{'Outgoing'}.
+                                             " explicitly set as outgoing Crypt plugin, but not marked Enabled; "
+                                             . (@enabled ? "using $enabled[0]" : "removing"));
+                }
                 $opt->{'Outgoing'} = $enabled[0] unless $enabled{$opt->{'Outgoing'}};
             } else {
                 $opt->{'Outgoing'} = $enabled[0];
@@ -939,6 +970,18 @@ our %META;
     WebExternalGecos          => { Deprecated => { Instead => 'WebRemoteUserGecos',            Remove => '4.4' }},
     WebExternalAuto           => { Deprecated => { Instead => 'WebRemoteUserAutocreate',       Remove => '4.4' }},
     AutoCreate                => { Deprecated => { Instead => 'UserAutocreateDefaultsOnLogin', Remove => '4.4' }},
+    LogoImageHeight => {
+        Deprecated => {
+            LogLevel => "info",
+            Message => "The LogoImageHeight configuration option did not affect display, and has been removed; please remove it from your RT_SiteConfig.pm",
+        },
+    },
+    LogoImageWidth => {
+        Deprecated => {
+            LogLevel => "info",
+            Message => "The LogoImageWidth configuration option did not affect display, and has been removed; please remove it from your RT_SiteConfig.pm",
+        },
+    },
 );
 my %OPTIONS = ();
 my @LOADED_CONFIGS = ();
@@ -1219,7 +1262,6 @@ sub Get {
 
     my $res;
     if ( $user && $user->id && $META{$name}->{'Overridable'} ) {
-        $user = $user->UserObj if $user->isa('RT::CurrentUser');
         my $prefs = $user->Preferences($RT::System);
         $res = $prefs->{$name} if $prefs;
     }
index e275295..f9b83f5 100644 (file)
@@ -84,7 +84,9 @@ additional options to fine-tune behaviour.
 
 However, note that you B<must> add the
 L<Auth::Crypt|RT::Interface::Email::Auth::Crypt> email filter to enable
-the handling of incoming encrypted/signed messages.
+the handling of incoming encrypted/signed messages.  It should be added
+in addition to the standard
+L<Auth::MailFrom|RT::Interface::Email::Auth::Crypt> plugin.
 
 =head2 %Crypt
 
@@ -275,7 +277,7 @@ sub LoadImplementation {
     my $class = 'RT::Crypt::'. $proto;
     return $cache{ $class } if exists $cache{ $class };
 
-    if (eval "require $class; 1") {
+    if ($class->require) {
         return $cache{ $class } = $class;
     } else {
         RT->Logger->warn( "Could not load $class: $@" );
@@ -435,15 +437,17 @@ sub SignEncrypt {
         $args{'Signer'} =
             $self->UseKeyForSigning
             || do {
-                my $addr = (Email::Address->parse( $entity->head->get( 'From' ) ))[0];
-                $addr? $addr->address : undef
+                my ($addr) = map {Email::Address->parse( Encode::decode( "UTF-8", $_ ) )}
+                    $entity->head->get( 'From' );
+                $addr ? $addr->address : undef
             };
     }
     if ( $args{'Encrypt'} && !$args{'Recipients'} ) {
         my %seen;
         $args{'Recipients'} = [
             grep $_ && !$seen{ $_ }++, map $_->address,
-            map Email::Address->parse( $entity->head->get( $_ ) ),
+            map Email::Address->parse( Encode::decode("UTF-8", $_ ) ),
+            map $entity->head->get( $_ ),
             qw(To Cc Bcc)
         ];
     }
index 316d2fa..c949bec 100644 (file)
@@ -494,7 +494,8 @@ sub SignEncryptRFC3156 {
     }
     if ( $args{'Encrypt'} ) {
         my @recipients = map $_->address,
-            map Email::Address->parse( $entity->head->get( $_ ) ),
+            map Email::Address->parse( Encode::decode( "UTF-8", $_ ) ),
+            map $entity->head->get( $_ ),
             qw(To Cc Bcc);
 
         my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 );
index 5351dba..47fdeb7 100644 (file)
@@ -220,7 +220,7 @@ sub SignEncrypt {
     if ( $args{'Encrypt'} ) {
         my %seen;
         $args{'Recipients'} = [
-            grep !$seen{$_}++, map $_->address, map Email::Address->parse($_),
+            grep !$seen{$_}++, map $_->address, map Email::Address->parse(Encode::decode("UTF-8",$_)),
             grep defined && length, map $entity->head->get($_), qw(To Cc Bcc)
         ];
     }
@@ -327,7 +327,7 @@ sub _SignEncrypt {
 
     my $opts = RT->Config->Get('SMIME');
 
-    my @command;
+    my @commands;
     if ( $args{'Sign'} ) {
         my $file = $self->CheckKeyring( Key => $args{'Signer'} );
         unless ($file) {
@@ -343,14 +343,14 @@ sub _SignEncrypt {
         $args{'Passphrase'} = $self->GetPassphrase( Address => $args{'Signer'} )
             unless defined $args{'Passphrase'};
 
-        push @command, join ' ', shell_quote(
+        push @commands, [
             $self->OpenSSLPath, qw(smime -sign),
             -signer => $file,
             -inkey  => $file,
             (defined $args{'Passphrase'} && length $args{'Passphrase'})
                 ? (qw(-passin env:SMIME_PASS))
                 : (),
-        );
+        ];
     }
     if ( $args{'Encrypt'} ) {
         foreach my $key ( @keys ) {
@@ -359,23 +359,32 @@ sub _SignEncrypt {
             close $key_file;
             $key = $key_file;
         }
-        push @command, join ' ', shell_quote(
+        push @commands, [
             $self->OpenSSLPath, qw(smime -encrypt -des3),
             map { $_->filename } @keys
-        );
+        ];
     }
 
-    my ($buf, $err) = ('', '');
-    {
-        local $ENV{'SMIME_PASS'} = $args{'Passphrase'};
-        local $SIG{'CHLD'} = 'DEFAULT';
-        safe_run_child { run3(
-            join( ' | ', @command ),
-            $args{'Content'},
-            \$buf, \$err
-        ) };
+    my $buf = ${ $args{'Content'} };
+    for my $command (@commands) {
+        my ($out, $err) = ('', '');
+        {
+            local $ENV{'SMIME_PASS'} = $args{'Passphrase'};
+            local $SIG{'CHLD'} = 'DEFAULT';
+            safe_run_child { run3(
+                $command,
+                \$buf,
+                \$out, \$err
+            ) };
+        }
+
+        $RT::Logger->debug( "openssl stderr: " . $err ) if length $err;
+
+        # copy output from the first command to the second command
+        # similar to the pipe we used to use to pipe signing -> encryption
+        # Using the pipe forced us to invoke the shell, this avoids any use of shell.
+        $buf = $out;
     }
-    $RT::Logger->debug( "openssl stderr: " . $err ) if length $err;
 
     if ($buf) {
         $res{'status'} .= $self->FormatStatus({
@@ -742,7 +751,8 @@ sub CheckIfProtected {
 
         if ( $security_type eq 'encrypted' ) {
             my $top = $args{'TopEntity'}->head;
-            $res{'Recipients'} = [grep defined && length, map $top->get($_), 'To', 'Cc'];
+            $res{'Recipients'} = [map {Encode::decode("UTF-8", $_)}
+                                      grep defined && length, map $top->get($_), 'To', 'Cc'];
         }
 
         return %res;
index 0ec3170..491c958 100644 (file)
@@ -54,7 +54,7 @@
 
     use RT::CurrentUser;
 
-    # laod
+    # load
     my $current_user = RT::CurrentUser->new;
     $current_user->Load(...);
     # or
@@ -254,9 +254,6 @@ sub loc_fuzzy {
     my $self = shift;
     return '' if !defined $_[0] || $_[0] eq '';
 
-    # XXX: work around perl's deficiency when matching utf8 data
-    return $_[0] if Encode::is_utf8($_[0]);
-
     return $self->LanguageHandle->maketext_fuzzy( @_ );
 }
 
index fe4e5f6..91ffdc3 100644 (file)
@@ -71,9 +71,9 @@ our %FieldTypes = (
         sort_order => 10,
         selection_type => 1,
 
-        labels => [ 'Select multiple values',      # loc
-                    'Select one value',            # loc
-                    'Select up to [_1] values',    # loc
+        labels => [ 'Select multiple values',               # loc
+                    'Select one value',                     # loc
+                    'Select up to [quant,_1,value,values]', # loc
                   ],
 
         render_types => {
@@ -94,27 +94,27 @@ our %FieldTypes = (
         sort_order => 20,
         selection_type => 0,
 
-        labels => [ 'Enter multiple values',       # loc
-                    'Enter one value',             # loc
-                    'Enter up to [_1] values',     # loc
+        labels => [ 'Enter multiple values',               # loc
+                    'Enter one value',                     # loc
+                    'Enter up to [quant,_1,value,values]', # loc
                   ]
                 },
     Text => {
         sort_order => 30,
         selection_type => 0,
         labels         => [
-                    'Fill in multiple text areas',      # loc
-                    'Fill in one text area',            # loc
-                    'Fill in up to [_1] text areas',    # loc
+                    'Fill in multiple text areas',                   # loc
+                    'Fill in one text area',                         # loc
+                    'Fill in up to [quant,_1,text area,text areas]', # loc
                   ]
             },
     Wikitext => {
         sort_order => 40,
         selection_type => 0,
         labels         => [
-                    'Fill in multiple wikitext areas',      # loc
-                    'Fill in one wikitext area',            # loc
-                    'Fill in up to [_1] wikitext areas',    # loc
+                    'Fill in multiple wikitext areas',                       # loc
+                    'Fill in one wikitext area',                             # loc
+                    'Fill in up to [quant,_1,wikitext area,wikitext areas]', # loc
                   ]
                 },
 
@@ -124,16 +124,16 @@ our %FieldTypes = (
         labels         => [
                     'Upload multiple images',               # loc
                     'Upload one image',                     # loc
-                    'Upload up to [_1] images',             # loc
+                    'Upload up to [quant,_1,image,images]', # loc
                   ]
              },
     Binary => {
         sort_order => 60,
         selection_type => 0,
         labels         => [
-                    'Upload multiple files',                # loc
-                    'Upload one file',                      # loc
-                    'Upload up to [_1] files',              # loc
+                    'Upload multiple files',              # loc
+                    'Upload one file',                    # loc
+                    'Upload up to [quant,_1,file,files]', # loc
                   ]
               },
 
@@ -141,18 +141,18 @@ our %FieldTypes = (
         sort_order => 70,
         selection_type => 1,
         labels         => [
-                    'Combobox: Select or enter multiple values',      # loc
-                    'Combobox: Select or enter one value',            # loc
-                    'Combobox: Select or enter up to [_1] values',    # loc
+                    'Combobox: Select or enter multiple values',               # loc
+                    'Combobox: Select or enter one value',                     # loc
+                    'Combobox: Select or enter up to [quant,_1,value,values]', # loc
                   ]
                 },
     Autocomplete => {
         sort_order => 80,
         selection_type => 1,
         labels         => [
-                    'Enter multiple values with autocompletion',      # loc
-                    'Enter one value with autocompletion',            # loc
-                    'Enter up to [_1] values with autocompletion',    # loc
+                    'Enter multiple values with autocompletion',               # loc
+                    'Enter one value with autocompletion',                     # loc
+                    'Enter up to [quant,_1,value,values] with autocompletion', # loc
                   ]
     },
 
@@ -160,18 +160,18 @@ our %FieldTypes = (
         sort_order => 90,
         selection_type => 0,
         labels         => [
-                    'Select multiple dates',                          # loc
-                    'Select date',                                    # loc
-                    'Select up to [_1] dates',                        # loc
+                    'Select multiple dates',              # loc
+                    'Select date',                        # loc
+                    'Select up to [quant,_1,date,dates]', # loc
                   ]
             },
     DateTime => {
         sort_order => 100,
         selection_type => 0,
         labels         => [
-                    'Select multiple datetimes',                      # loc
-                    'Select datetime',                                # loc
-                    'Select up to [_1] datetimes',                    # loc
+                    'Select multiple datetimes',                  # loc
+                    'Select datetime',                            # loc
+                    'Select up to [quant,_1,datetime,datetimes]', # loc
                   ]
                 },
 
@@ -179,18 +179,18 @@ our %FieldTypes = (
         sort_order => 110,
         selection_type => 0,
 
-        labels => [ 'Enter multiple IP addresses',       # loc
-                    'Enter one IP address',             # loc
-                    'Enter up to [_1] IP addresses',     # loc
+        labels => [ 'Enter multiple IP addresses',                    # loc
+                    'Enter one IP address',                           # loc
+                    'Enter up to [quant,_1,IP address,IP addresses]', # loc
                   ]
                 },
     IPAddressRange => {
         sort_order => 120,
         selection_type => 0,
 
-        labels => [ 'Enter multiple IP address ranges',       # loc
-                    'Enter one IP address range',             # loc
-                    'Enter up to [_1] IP address ranges',     # loc
+        labels => [ 'Enter multiple IP address ranges',                          # loc
+                    'Enter one IP address range',                                # loc
+                    'Enter up to [quant,_1,IP address range,IP address ranges]', # loc
                   ]
                 },
 );
@@ -378,20 +378,58 @@ sub Load {
 
 
 
-=head2 LoadByName (Queue => QUEUEID, Name => NAME)
+=head2 LoadByName Name => C<NAME>, [...]
 
-Loads the Custom field named NAME.
+Loads the Custom field named NAME.  As other optional parameters, takes:
 
-Will load a Disabled Custom Field even if there is a non-disabled Custom Field
-with the same Name.
+=over
 
-If a Queue parameter is specified, only look for ticket custom fields tied to that Queue.
+=item LookupType => C<LOOKUPTYPE>
 
-If the Queue parameter is '0', look for global ticket custom fields.
+The type of Custom Field to look for; while this parameter is not
+required, it is highly suggested, or you may not find the Custom Field
+you are expecting.  It should be passed a C<LookupType> such as
+L<RT::Ticket/CustomFieldLookupType> or
+L<RT::User/CustomFieldLookupType>.
 
-If no queue parameter is specified, look for any and all custom fields with this name.
+=item ObjectType => C<CLASS>
 
-BUG/TODO, this won't let you specify that you only want user or group CFs.
+The class of object that the custom field is applied to.  This can be
+intuited from the provided C<LookupType>.
+
+=item ObjectId => C<ID>
+
+limits the custom field search to one applied to the relevant id.  For
+example, if a C<LookupType> of C<< RT::Ticket->CustomFieldLookupType >>
+is used, this is which Queue the CF must be applied to.  Pass 0 to only
+search custom fields that are applied globally.
+
+=item IncludeDisabled => C<BOOLEAN>
+
+Whether it should return Disabled custom fields if they match; defaults
+to on, though non-Disabled custom fields are returned preferentially.
+
+=item IncludeGlobal => C<BOOLEAN>
+
+Whether to also search global custom fields, even if a value is provided
+for C<ObjectId>; defaults to off.  Non-global custom fields are returned
+preferentially.
+
+=back
+
+For backwards compatibility, a value passed for C<Queue> is equivalent
+to specifying a C<LookupType> of L<RT::Ticket/CustomFieldLookupType>,
+and a C<ObjectId> of the value passed as C<Queue>.
+
+If multiple custom fields match the above constraints, the first
+according to C<SortOrder> will be returned; ties are broken by C<id>,
+lowest-first.
+
+=head2 LoadNameAndQueue
+
+=head2 LoadByNameAndQueue
+
+Deprecated alternate names for L</LoadByName>.
 
 =cut
 
@@ -403,9 +441,17 @@ BUG/TODO, this won't let you specify that you only want user or group CFs.
 sub LoadByName {
     my $self = shift;
     my %args = (
-        Queue => undef,
-        Name  => undef,
+        Name       => undef,
         LookupType => undef,
+        ObjectType => undef,
+        ObjectId   => undef,
+
+        IncludeDisabled => 1,
+        IncludeGlobal   => 0,
+
+        # Back-compat
+        Queue => undef,
+
         @_,
     );
 
@@ -414,16 +460,53 @@ sub LoadByName {
         return wantarray ? (0, $self->loc("No name provided")) : 0;
     }
 
-    # if we're looking for a queue by name, make it a number
-    if ( defined $args{'Queue'} && ($args{'Queue'} =~ /\D/ || !$self->ContextObject) ) {
-        my $QueueObj = RT::Queue->new( $self->CurrentUser );
-        $QueueObj->Load( $args{'Queue'} );
-        $args{'Queue'} = $QueueObj->Id;
-        $self->SetContextObject( $QueueObj )
-            unless $self->ContextObject;
-    }
+    if ( defined $args{'Queue'} ) {
+        # Set a LookupType for backcompat, otherwise we'll calculate
+        # one of RT::Queue from your ContextObj.  Older code was relying
+        # on us defaulting to RT::Queue-RT::Ticket in old LimitToQueue call.
+        $args{LookupType} ||= 'RT::Queue-RT::Ticket';
+        $args{ObjectId}   //= delete $args{Queue};
+    }
+
+    # Default the ObjectType to the top category of the LookupType; it's
+    # what the CFs are assigned on.
+    $args{ObjectType} ||= $1 if $args{LookupType} and $args{LookupType} =~ /^([^-]+)/;
+
+    # Resolve the ObjectId/ObjectType; this is necessary to properly
+    # limit ObjectId, and also possibly useful to set a ContextObj if we
+    # are currently lacking one.  It is not strictly necessary if we
+    # have a context object and were passed a numeric ObjectId, but it
+    # cannot hurt to verify its sanity.  Skip if we have a false
+    # ObjectId, which means "global", or if we lack an ObjectType
+    if ($args{ObjectId} and $args{ObjectType}) {
+        my ($obj, $ok, $msg);
+        eval {
+            $obj = $args{ObjectType}->new( $self->CurrentUser );
+            ($ok, $msg) = $obj->Load( $args{ObjectId} );
+        };
 
-    # XXX - really naive implementation.  Slow. - not really. still just one query
+        if ($ok) {
+            $args{ObjectId} = $obj->id;
+            $self->SetContextObject( $obj )
+                unless $self->ContextObject;
+        } else {
+            $RT::Logger->warning("Failed to load $args{ObjectType} '$args{ObjectId}'");
+            if ($args{IncludeGlobal}) {
+                # Fall back to acting like we were only asked about the
+                # global case
+                $args{ObjectId} = 0;
+            } else {
+                # If they didn't also want global results, there's no
+                # point in searching; abort
+                return wantarray ? (0, $self->loc("Not found")) : 0;
+            }
+        }
+    } elsif (not $args{ObjectType} and $args{ObjectId}) {
+        # If we skipped out on the above due to lack of ObjectType, make
+        # sure we clear out ObjectId of anything lingering
+        $RT::Logger->warning("No LookupType or ObjectType passed; ignoring ObjectId");
+        delete $args{ObjectId};
+    }
 
     my $CFs = RT::CustomFields->new( $self->CurrentUser );
     $CFs->SetContextObject( $self->ContextObject );
@@ -438,23 +521,55 @@ sub LoadByName {
             ($self->ContextObject, $self->ContextObject->ACLEquivalenceObjects) ]
         if $self->ContextObject;
 
+    # Apply LookupType limits
     $args{LookupType} = [ $args{LookupType} ]
         if $args{LookupType} and not ref($args{LookupType});
     $CFs->Limit( FIELD => "LookupType", OPERATOR => "IN", VALUE => $args{LookupType} )
         if $args{LookupType};
 
-    # Don't limit to queue if queue is 0.  Trying to do so breaks
-    # RT::Group type CFs.
-    if ( defined $args{'Queue'} ) {
-        $CFs->LimitToQueue( $args{'Queue'} );
+    # Default to by SortOrder and id; this mirrors the standard ordering
+    # of RT::CustomFields (minus the Name, which is guaranteed to be
+    # fixed)
+    my @order = (
+        { FIELD => 'SortOrder',
+          ORDER => 'ASC' },
+        { FIELD => 'id',
+          ORDER => 'ASC' },
+    );
+
+    if (defined $args{ObjectId}) {
+        # The join to OCFs is distinct -- either we have a global
+        # application or an objectid match, but never both.  Even if
+        # this were not the case, we care only for the first row.
+        my $ocfs = $CFs->_OCFAlias( Distinct => 1);
+        if ($args{IncludeGlobal}) {
+            $CFs->Limit(
+                ALIAS    => $ocfs,
+                FIELD    => 'ObjectId',
+                OPERATOR => 'IN',
+                VALUE    => [ $args{ObjectId}, 0 ],
+            );
+            # Find the queue-specific first
+            unshift @order, { ALIAS => $ocfs, FIELD => "ObjectId", ORDER => "DESC" };
+        } else {
+            $CFs->Limit(
+                ALIAS => $ocfs,
+                FIELD => 'ObjectId',
+                VALUE => $args{ObjectId},
+            );
+        }
     }
 
-    # When loading by name, we _can_ load disabled fields, but prefer
-    # non-disabled fields.
-    $CFs->FindAllRows;
-    $CFs->OrderByCols(
-        { FIELD => "Disabled", ORDER => 'ASC' },
-    );
+    if ($args{IncludeDisabled}) {
+        # Load disabled fields, but return them only as a last resort.
+        # This goes at the front of @order, as we prefer the
+        # non-disabled global CF to the disabled Queue-specific CF.
+        $CFs->FindAllRows;
+        unshift @order, { FIELD => "Disabled", ORDER => 'ASC' };
+    }
+
+    # Apply the above orderings
+    $CFs->OrderByCols( @order );
 
     # We only want one entry.
     $CFs->RowsPerPage(1);
@@ -486,7 +601,7 @@ sub Values {
 
     my $class = $self->ValuesClass;
     if ( $class ne 'RT::CustomFieldValues') {
-        eval "require $class" or die "$@";
+        $class->require or die "Can't load $class: $@";
     }
     my $cf_values = $class->new( $self->CurrentUser );
     # if the user has no rights, return an empty object
@@ -1560,12 +1675,6 @@ sub AddValueForObject {
         }
     }
 
-    if (my $canonicalizer = $self->can('_CanonicalizeValue'.$self->Type)) {
-         $canonicalizer->($self, \%args);
-    }
-
-
-
     my $newval = RT::ObjectCustomFieldValue->new( $self->CurrentUser );
     my ($val, $msg) = $newval->Create(
         ObjectType   => ref($obj),
@@ -1587,6 +1696,17 @@ sub AddValueForObject {
 }
 
 
+sub _CanonicalizeValue {
+    my $self = shift;
+    my $args = shift;
+
+    my $type = $self->_Value('Type');
+    return 1 unless $type;
+
+    my $method = '_CanonicalizeValue'. $type;
+    return 1 unless $self->can($method);
+    $self->$method($args);
+}
 
 sub _CanonicalizeValueDateTime {
     my $self    = shift;
@@ -1595,6 +1715,7 @@ sub _CanonicalizeValueDateTime {
     $DateObj->Set( Format => 'unknown',
                    Value  => $args->{'Content'} );
     $args->{'Content'} = $DateObj->ISO;
+    return 1;
 }
 
 # For date, we need to store Content as ISO date
@@ -1609,6 +1730,33 @@ sub _CanonicalizeValueDate {
                    Value    => $args->{'Content'},
                  );
     $args->{'Content'} = $DateObj->Date( Timezone => 'user' );
+    return 1;
+}
+
+sub _CanonicalizeValueIPAddress {
+    my $self = shift;
+    my $args = shift;
+
+    $args->{Content} = RT::ObjectCustomFieldValue->ParseIP( $args->{Content} );
+    return (0, $self->loc("Content is not a valid IP address"))
+        unless $args->{Content};
+    return 1;
+}
+
+sub _CanonicalizeValueIPAddressRange {
+    my $self = shift;
+    my $args = shift;
+
+    my $content = $args->{Content};
+    $content .= "-".$args->{LargeContent} if $args->{LargeContent};
+
+    ($args->{Content}, $args->{LargeContent})
+        = RT::ObjectCustomFieldValue->ParseIPRange( $content );
+
+    $args->{ContentType} = 'text/plain';
+    return (0, $self->loc("Content is not a valid IP address range"))
+        unless $args->{Content};
+    return 1;
 }
 
 =head2 MatchPattern STRING
@@ -1820,18 +1968,20 @@ sub _URLTemplate {
         unless ( $self->CurrentUserHasRight('AdminCustomField') ) {
             return ( 0, $self->loc('Permission Denied') );
         }
-        $self->SetAttribute( Name => $template_name, Content => $value );
+        if (length $value and defined $value) {
+            $self->SetAttribute( Name => $template_name, Content => $value );
+        } else {
+            $self->DeleteAttribute( $template_name );
+        }
         return ( 1, $self->loc('Updated') );
     } else {
         unless ( $self->id && $self->CurrentUserHasRight('SeeCustomField') ) {
             return (undef);
         }
 
-        my @attr = $self->Attributes->Named($template_name);
-        my $attr = shift @attr;
-
-        if ($attr) { return $attr->Content }
-
+        my ($attr) = $self->Attributes->Named($template_name);
+        return undef unless $attr;
+        return $attr->Content;
     }
 }
 
index 96ffc00..500a967 100644 (file)
@@ -77,8 +77,10 @@ the identifier by which the user will see the dropdown.
 =head2 ExternalValues
 
 This method should return an array reference of hash references.  The
-hash references should contain keys for C<name>, C<description>, and
-C<sortorder>.
+hash references must contain a key for C<name> and can optionally contain
+keys for C<description>, C<sortorder>, and C<category>. If supplying a
+category, you must also set the category the custom field is based on in
+the custom field configuration page.
 
 =head1 SEE ALSO
 
@@ -179,6 +181,7 @@ sub _DoSearch {
             customfield => $self->{'__external_cf'},
             sortorder => 0,
             description => '',
+            category => undef,
             creator => RT->SystemUser->id,
             created => undef,
             lastupdatedby => RT->SystemUser->id,
index eab9a10..14b0e8f 100644 (file)
@@ -118,9 +118,11 @@ sub LimitToGrouping {
     my $obj = shift;
     my $grouping = shift;
 
+    my $grouping_class = $self->NewItem->_GroupingClass($obj);
+
     my $config = RT->Config->Get('CustomFieldGroupings');
        $config = {} unless ref($config) eq 'HASH';
-       $config = $config->{ref($obj) || $obj} || [];
+       $config = $config->{$grouping_class} || [];
     my %h = ref $config eq "ARRAY" ? @{$config} : %{$config};
 
     if ( $grouping ) {
index 5126560..762e2c8 100644 (file)
@@ -386,9 +386,14 @@ sub BuildEmail {
 
             $cid_of{$uri} = time() . $$ . int(rand(1e6));
 
-            # downgrade non-text strings, because all strings are utf8 by
-            # default, which is wrong for non-text strings.
-            if ( $mimetype !~ m{text/} ) {
+            # Encode textual data in UTF-8, and downgrade (treat
+            # codepoints as codepoints, and ensure the UTF-8 flag is
+            # off) everything else.
+            my @extra;
+            if ( $mimetype =~ m{text/} ) {
+                $data = Encode::encode( "UTF-8", $data );
+                @extra = ( Charset => "UTF-8" );
+            } else {
                 utf8::downgrade( $data, 1 ) or $RT::Logger->warning("downgrade $data failed");
             }
 
@@ -400,6 +405,7 @@ sub BuildEmail {
                 Disposition  => 'inline',
                 Name         => RT::Interface::Email::EncodeToMIME( String => $filename ),
                 'Content-Id' => $cid_of{$uri},
+                @extra,
             );
 
             return "cid:$cid_of{$uri}";
@@ -413,16 +419,16 @@ sub BuildEmail {
     );
 
     my $entity = MIME::Entity->build(
-        From    => Encode::encode_utf8($args{From}),
-        To      => Encode::encode_utf8($args{To}),
+        From    => Encode::encode("UTF-8", $args{From}),
+        To      => Encode::encode("UTF-8", $args{To}),
         Subject => RT::Interface::Email::EncodeToMIME( String => $args{Subject} ),
         Type    => "multipart/mixed",
     );
 
     $entity->attach(
-        Data        => Encode::encode_utf8($content),
         Type        => 'text/html',
         Charset     => 'UTF-8',
+        Data        => Encode::encode("UTF-8", $content),
         Disposition => 'inline',
         Encoding    => "base64",
     );
@@ -558,7 +564,8 @@ sub GetResource {
         $HTML::Mason::Commands::r->path_info($path);
 
         # grab the query arguments
-        my %args = map { $_ => [ $uri->query_param($_) ] } $uri->query_param;
+        my %args = map { $_ => [ map {Encode::decode("UTF-8",$_)}
+                                     $uri->query_param($_) ] } $uri->query_param;
         # Convert empty and single element arrayrefs to a non-ref scalar
         @$_ < 2 and $_ = $_->[0]
             for values %args;
index 509e11c..dca3434 100644 (file)
@@ -56,7 +56,7 @@
 
 =head1 DESCRIPTION
 
-RT Date is a simple Date Object designed to be speedy and easy for RT to use
+RT Date is a simple Date Object designed to be speedy and easy for RT to use.
 
 The fact that it assumes that a time of 0 means "never" is probably a bug.
 
@@ -359,21 +359,6 @@ Turn on short notation with one character units, for example
 
 =cut
 
-# loc("[_1]s")
-# loc("[_1]m")
-# loc("[_1]h")
-# loc("[_1]d")
-# loc("[_1]W")
-# loc("[_1]M")
-# loc("[_1]Y")
-# loc("[quant,_1,second]")
-# loc("[quant,_1,minute]")
-# loc("[quant,_1,hour]")
-# loc("[quant,_1,day]")
-# loc("[quant,_1,week]")
-# loc("[quant,_1,month]")
-# loc("[quant,_1,year]")
-
 sub DurationAsString {
     my $self     = shift;
     my $duration = int shift;
@@ -387,61 +372,59 @@ sub DurationAsString {
     $negative = 1 if $duration < 0;
     $duration = abs $duration;
 
-    my %units = (
-        s => 1,
-        m => $MINUTE,
-        h => $HOUR,
-        d => $DAY,
-        W => $WEEK,
-        M => $MONTH,
-        Y => $YEAR,
-    );
-    my %long_units = (
-        s => 'second',
-        m => 'minute',
-        h => 'hour',
-        d => 'day',
-        W => 'week',
-        M => 'month',
-        Y => 'year',
-    );
-
     my @res;
 
     my $coef = 2;
     my $i = 0;
     while ( $duration > 0 && ++$i <= $args{'Show'} ) {
 
-        my $unit;
+        my ($locstr, $unit);
         if ( $duration < $MINUTE ) {
-            $unit = 's';
+            $locstr = $args{Short}
+                    ? '[_1]s'                      # loc
+                    : '[quant,_1,second,seconds]'; # loc
+            $unit = 1;
         }
         elsif ( $duration < ( $coef * $HOUR ) ) {
-            $unit = 'm';
+            $locstr = $args{Short}
+                    ? '[_1]m'                      # loc
+                    : '[quant,_1,minute,minutes]'; # loc
+            $unit = $MINUTE;
         }
         elsif ( $duration < ( $coef * $DAY ) ) {
-            $unit = 'h';
+            $locstr = $args{Short}
+                    ? '[_1]h'                      # loc
+                    : '[quant,_1,hour,hours]';     # loc
+            $unit = $HOUR;
         }
         elsif ( $duration < ( $coef * $WEEK ) ) {
-            $unit = 'd';
+            $locstr = $args{Short}
+                    ? '[_1]d'                      # loc
+                    : '[quant,_1,day,days]';       # loc
+            $unit = $DAY;
         }
         elsif ( $duration < ( $coef * $MONTH ) ) {
-            $unit = 'W';
+            $locstr = $args{Short}
+                    ? '[_1]W'                      # loc
+                    : '[quant,_1,week,weeks]';     # loc
+            $unit = $WEEK;
         }
         elsif ( $duration < $YEAR ) {
-            $unit = 'M';
+            $locstr = $args{Short}
+                    ? '[_1]M'                      # loc
+                    : '[quant,_1,month,months]';   # loc
+            $unit = $MONTH;
         }
         else {
-            $unit = 'Y';
+            $locstr = $args{Short}
+                    ? '[_1]Y'                      # loc
+                    : '[quant,_1,year,years]';     # loc
+            $unit = $YEAR;
         }
-        my $value = int( $duration / $units{$unit}  + ($i < $args{'Show'}? 0 : 0.5) );
-        $duration -= int( $value * $units{$unit} );
+        my $value = int( $duration / $unit  + ($i < $args{'Show'}? 0 : 0.5) );
+        $duration -= int( $value * $unit );
 
-        if ( $args{'Short'} ) {
-            push @res, $self->loc("[_1]$unit", $value);
-        } else {
-            push @res, $self->loc("[quant,_1,$long_units{$unit}]", $value);
-        }
+        push @res, $self->loc($locstr, $value);
 
         $coef = 1;
     }
@@ -456,7 +439,7 @@ sub DurationAsString {
 
 =head2 AgeAsString
 
-Takes nothing. Returns a string that's the differnce between the
+Takes nothing. Returns a string that's the difference between the
 time in the object and now.
 
 =cut
@@ -467,10 +450,10 @@ sub AgeAsString { return $_[0]->DiffAsString }
 
 =head2 AsString
 
-Returns the object's time as a localized string with curent user's prefered
+Returns the object's time as a localized string with curent user's preferred
 format and timezone.
 
-If the current user didn't choose prefered format then system wide setting is
+If the current user didn't choose preferred format then system wide setting is
 used or L</DefaultFormat> if the latter is not specified. See config option
 C<DateTimeFormat>.
 
@@ -480,7 +463,7 @@ sub AsString {
     my $self = shift;
     my %args = (@_);
 
-    return $self->loc("Not set") unless $self->Unix > 0;
+    return $self->loc("Not set") unless $self->IsSet;
 
     my $format = RT->Config->Get( 'DateTimeFormat', $self->CurrentUser ) || 'DefaultFormat';
     $format = { Format => $format } unless ref $format;
@@ -551,7 +534,8 @@ Returns new unix time.
 
 sub AddDays {
     my $self = shift;
-    my $days = shift || 1;
+    my $days = shift;
+    $days = 1 unless defined $days;
     return $self->AddSeconds( $days * $DAY );
 }
 
@@ -572,13 +556,21 @@ Returns the number of seconds since the epoch
 
 sub Unix {
     my $self = shift; 
-    $self->{'time'} = int(shift || 0) if @_;
+
+    if (@_) {
+        my $time = int(shift || 0);
+        if ($time < 0) {
+            RT->Logger->notice("Passed a unix time less than 0, forcing to 0: [$time]");
+            $time = 0;
+        }
+        $self->{'time'} = int $time;
+    }
     return $self->{'time'};
 }
 
 =head2 DateTime
 
-Alias for L</Get> method. Arguments C<Date> and <Time>
+Alias for L</Get> method. Arguments C<Date> and C<Time>
 are fixed to true values, other arguments could be used
 as described in L</Get>.
 
@@ -618,7 +610,7 @@ sub Time {
 
 =head2 Get
 
-Returnsa a formatted and localized string that represets time of
+Returns a formatted and localized string that represents the time of
 the current object.
 
 
@@ -656,7 +648,7 @@ Each method takes several arguments:
 
 Formatters may also add own arguments to the list, for example
 in RFC2822 format day of time in output is optional so it
-understand boolean argument C<DayOfTime>.
+understands boolean argument C<DayOfTime>.
 
 =head3 Formatters
 
@@ -813,11 +805,11 @@ sub LocalizedDateTime
 =head3 ISO
 
 Returns the object's date in ISO format C<YYYY-MM-DD mm:hh:ss>.
-ISO format is locale independant, but adding timezone offset info
+ISO format is locale-independent, but adding timezone offset info
 is not implemented yet.
 
 Supports arguments: C<Timezone>, C<Date>, C<Time> and C<Seconds>.
-See </Output formatters> for description of arguments.
+See L</Output formatters> for description of arguments.
 
 =cut
 
@@ -839,7 +831,7 @@ sub ISO {
     my $res = '';
     $res .= sprintf("%04d-%02d-%02d", $year, $mon, $mday) if $args{'Date'};
     $res .= sprintf(' %02d:%02d', $hour, $min) if $args{'Time'};
-    $res .= sprintf(':%02d', $sec, $min) if $args{'Time'} && $args{'Seconds'};
+    $res .= sprintf(':%02d', $sec) if $args{'Time'} && $args{'Seconds'};
     $res =~ s/^\s+//;
 
     return $res;
@@ -850,12 +842,12 @@ sub ISO {
 Returns the object's date and time in W3C date time format
 (L<http://www.w3.org/TR/NOTE-datetime>).
 
-Format is locale independand and is close enought to ISO, but
+Format is locale-independent and is close enough to ISO, but
 note that date part is B<not optional> and output string
 has timezone offset mark in C<[+-]hh:mm> format.
 
 Supports arguments: C<Timezone>, C<Time> and C<Seconds>.
-See </Output formatters> for description of arguments.
+See L</Output formatters> for description of arguments.
 
 =cut
 
@@ -879,7 +871,7 @@ sub W3CDTF {
     $res .= sprintf("%04d-%02d-%02d", $year, $mon, $mday);
     if ( $args{'Time'} ) {
         $res .= sprintf('T%02d:%02d', $hour, $min);
-        $res .= sprintf(':%02d', $sec, $min) if $args{'Seconds'};
+        $res .= sprintf(':%02d', $sec) if $args{'Seconds'};
         if ( $offset ) {
             $res .= sprintf "%s%02d:%02d", $self->_SplitOffset( $offset );
         } else {
@@ -895,11 +887,11 @@ sub W3CDTF {
 
 Returns the object's date and time in RFC2822 format,
 for example C<Sun, 06 Nov 1994 08:49:37 +0000>.
-Format is locale independand as required by RFC. Time
+Format is locale-independent as required by RFC. Time
 part always has timezone offset in digits with sign prefix.
 
 Supports arguments: C<Timezone>, C<Date>, C<Time>, C<DayOfWeek>
-and C<Seconds>. See </Output formatters> for description of
+and C<Seconds>. See L</Output formatters> for description of
 arguments.
 
 =cut
@@ -937,8 +929,8 @@ Returns the object's date and time in RFC2616 (HTTP/1.1) format,
 for example C<Sun, 06 Nov 1994 08:49:37 GMT>. While the RFC describes
 version 1.1 of HTTP, but the same form date can be used in version 1.0.
 
-Format is fixed length, locale independand and always represented in GMT
-what makes it quite useless for users, but any date in HTTP transfers
+Format is fixed-length, locale-independent and always represented in GMT
+which makes it quite useless for users, but any date in HTTP transfers
 must be presented using this format.
 
     HTTP-date = rfc1123 | ...
@@ -953,7 +945,7 @@ must be presented using this format.
 
 Supports arguments: C<Date> and C<Time>, but you should use them only for
 some personal reasons, RFC2616 doesn't define any optional parts.
-See </Output formatters> for description of arguments.
+See L</Output formatters> for description of arguments.
 
 =cut
 
@@ -973,11 +965,11 @@ sub RFC2616 {
 =head4 iCal
 
 Returns the object's date and time in iCalendar format.
-If only date requested then users timezone is used, otherwise
+If only date requested then user's timezone is used, otherwise
 it's UTC.
 
 Supports arguments: C<Date> and C<Time>.
-See </Output formatters> for description of arguments.
+See L</Output formatters> for description of arguments.
 
 =cut
 
@@ -1028,11 +1020,19 @@ argument unix C<$time>, default value is the current unix time.
 Returns object's date and time in the format provided by perl's
 builtin functions C<localtime> and C<gmtime> with two exceptions:
 
-1) "Year" is a four-digit year, rather than "years since 1900"
+=over
+
+=item 1)
 
-2) The last element of the array returned is C<offset>, which
+"Year" is a four-digit year, rather than "years since 1900"
+
+=item 2)
+
+The last element of the array returned is C<offset>, which
 represents timezone offset against C<UTC> in seconds.
 
+=back
+
 =cut
 
 sub Localtime
@@ -1054,7 +1054,7 @@ sub Localtime
             POSIX::tzset();
             @local = localtime($unix);
         }
-        POSIX::tzset(); # return back previouse value
+        POSIX::tzset(); # return back previous value
     }
     $local[5] += 1900; # change year to 4+ digits format
     my $offset = Time::Local::timegm_nocheck(@local) - $unix;
@@ -1066,16 +1066,16 @@ sub Localtime
 Takes argument C<$context>, which determines whether we should
 treat C<@time> as "user local", "system" or "UTC" time.
 
-C<@time> is array returned by L<Localtime> functions. Only first
+C<@time> is array returned by L</Localtime> functions. Only first
 six elements are mandatory - $sec, $min, $hour, $mday, $mon and $year.
 You may pass $wday, $yday and $isdst, these are ignored.
 
 If you pass C<$offset> as ninth argument, it's used instead of
 C<$context>. It's done such way as code 
-C<$self->Timelocal('utc', $self->Localtime('server'))> doesn't
-makes much sense and most probably would produce unexpected
-result, so the method ignore 'utc' context and uses offset
-returned by L<Localtime> method.
+C<< $self->Timelocal('utc', $self->Localtime('server')) >> doesn't
+make much sense and most probably would produce unexpected
+results, so the method ignores 'utc' context and uses the offset
+returned by the L</Localtime> method.
 
 =cut
 
@@ -1106,33 +1106,31 @@ sub Timelocal {
 
 =head3 Timezone $context
 
-Returns the timezone name.
-
-Takes one argument, C<$context> argument which could be C<user>, C<server> or C<utc>.
+Returns the timezone name for the specified context.  C<$context>
+should be one of these values:
 
 =over
 
-=item user
-
-Default value is C<user> that mean it returns current user's Timezone value.
-
-=item server
+=item C<user>
 
-If context is C<server> it returns value of the C<Timezone> RT config option.
+The current user's Timezone value will be returned.
 
-=item  utc
+=item C<server>
 
-If both server's and user's timezone names are undefined returns 'UTC'.
+The value of the C<Timezone> RT config option will be returned.
 
 =back
 
+For any other value of C<$context>, or if the specified context has no
+defined timezone, C<UTC> is returned.
+
 =cut
 
 sub Timezone {
     my $self = shift;
 
     if (@_ == 0) {
-        Carp::carp "RT::Date->Timezone is a setter only";
+        Carp::carp 'RT::Date->Timezone requires a context argument';
         return undef;
     }
 
@@ -1151,6 +1149,20 @@ sub Timezone {
     return $tz;
 }
 
+=head3 IsSet
+
+Returns true if this Date is set in the database, otherwise returns a false value.
+
+This avoids needing to compare to 1970-01-01 in any of your code.
+
+=cut
+
+sub IsSet {
+    my $self = shift;
+    return $self->Unix ? 1 : 0;
+
+}
+
 
 RT::Base->_ImportOverlays();
 
index 283cc45..55b319f 100644 (file)
@@ -299,8 +299,8 @@ sub ParseCcAddressesFromHead {
 
     my (@Addresses);
 
-    my @ToObjs = Email::Address->parse( $self->Head->get('To') );
-    my @CcObjs = Email::Address->parse( $self->Head->get('Cc') );
+    my @ToObjs = Email::Address->parse( Encode::decode( "UTF-8", $self->Head->get('To') ) );
+    my @CcObjs = Email::Address->parse( Encode::decode( "UTF-8", $self->Head->get('Cc') ) );
 
     foreach my $AddrObj ( @ToObjs, @CcObjs ) {
         my $Address = $AddrObj->address;
@@ -634,7 +634,7 @@ sub RescueOutlook {
     # Add base64 since we've seen examples of double newlines with
     # this type too. Need an example of a multi-part base64 to
     # handle that permutation if it exists.
-    elsif ( $mime->head->get('Content-Transfer-Encoding') =~ m{base64} ) {
+    elsif ( ($mime->head->get('Content-Transfer-Encoding')||'') =~ m{base64} ) {
         $text_part = $mime;    # Assuming single part, already decoded.
     }
 
index c970d63..be85faf 100644 (file)
@@ -50,7 +50,7 @@ package RT;
 use warnings;
 use strict;
 
-our $VERSION = '4.2.2';
+our $VERSION = '4.2.8';
 our ($MAJOR_VERSION, $MINOR_VERSION, $REVISION) = $VERSION =~ /^(\d)\.(\d)\.(\d+)/;
 
 
index 477a5d0..573d820 100644 (file)
@@ -299,9 +299,12 @@ sub TicketLinks {
     }
 
     $args{'Seen'} ||= {};
-    return $args{'Graph'} if $args{'Seen'}{ $args{'Ticket'}->id }++;
-
-    $self->AddTicket( %args );
+    if ( $args{'Seen'}{ $args{'Ticket'}->id } && $args{'Seen'}{ $args{'Ticket'}->id } <= $args{'CurrentDepth'} ) {
+      return $args{'Graph'};
+    } elsif ( ! defined $args{'Seen'}{ $args{'Ticket'}->id } ) {
+      $self->AddTicket( %args );
+    }
+    $args{'Seen'}{ $args{'Ticket'}->id } = $args{'CurrentDepth'};
 
     return $args{'Graph'} if $args{'MaxDepth'} && $args{'CurrentDepth'} >= $args{'MaxDepth'};
 
index a8652e4..29802ad 100644 (file)
@@ -86,11 +86,10 @@ sub FinalizeDatabaseType {
     my $db_type = RT->Config->Get('DatabaseType');
     my $package = "DBIx::SearchBuilder::Handle::$db_type";
 
-    unless (eval "require $package; 1;") {
+    $package->require or
         die "Unable to load DBIx::SearchBuilder database handle for '$db_type'.\n".
             "Perhaps you've picked an invalid database type or spelled it incorrectly.\n".
             $@;
-    }
 
     @RT::Handle::ISA = ($package);
 
@@ -250,7 +249,7 @@ sub CheckIntegrity {
         return (0, 'no nobody user', "Couldn't find Nobody user in the DB '". $RT::Handle->DSN ."'");
     }
 
-    return $RT::Handle->dbh;
+    return 1;
 }
 
 sub CheckCompatibility {
@@ -805,9 +804,9 @@ sub InsertData {
     );
 
     # Slurp in stuff to insert from the datafile. Possible things to go in here:-
-    our (@Groups, @Users, @ACL, @Queues, @ScripActions, @ScripConditions,
+    our (@Groups, @Users, @Members, @ACL, @Queues, @ScripActions, @ScripConditions,
            @Templates, @CustomFields, @Scrips, @Attributes, @Initial, @Final);
-    local (@Groups, @Users, @ACL, @Queues, @ScripActions, @ScripConditions,
+    local (@Groups, @Users, @Members, @ACL, @Queues, @ScripActions, @ScripConditions,
            @Templates, @CustomFields, @Scrips, @Attributes, @Initial, @Final);
 
     local $@;
@@ -829,6 +828,7 @@ sub InsertData {
             my $new_entry = RT::Group->new( RT->SystemUser );
             $item->{'Domain'} ||= 'UserDefined';
             my $member_of = delete $item->{'MemberOf'};
+            my $members = delete $item->{'Members'};
             my ( $return, $msg ) = $new_entry->_Create(%$item);
             unless ( $return ) {
                 $RT::Logger->error( $msg );
@@ -867,6 +867,12 @@ sub InsertData {
                     }
                 }
             }
+            push @Members, map { +{Group => $new_entry->id,
+                                   Class => "RT::User", Name => $_} }
+                @{ $members->{Users} || [] };
+            push @Members, map { +{Group => $new_entry->id,
+                                   Class => "RT::Group", Name => $_} }
+                @{ $members->{Groups} || [] };
         }
         $RT::Logger->debug("done.");
     }
@@ -918,6 +924,33 @@ sub InsertData {
         }
         $RT::Logger->debug("done.");
     }
+    if ( @Members ) {
+        $RT::Logger->debug("Adding users and groups to groups...");
+        for my $item (@Members) {
+            my $group = RT::Group->new(RT->SystemUser);
+            $group->LoadUserDefinedGroup( delete $item->{Group} );
+            unless ($group->Id) {
+                RT->Logger->error("Unable to find group '$group' to add members to");
+                next;
+            }
+
+            my $class = delete $item->{Class} || 'RT::User';
+            my $member = $class->new( RT->SystemUser );
+            $item->{Domain} = 'UserDefined' if $member->isa("RT::Group");
+            $member->LoadByCols( %$item );
+            unless ($member->Id) {
+                RT->Logger->error("Unable to find $class '".($item->{id} || $item->{Name})."' to add to ".$group->Name);
+                next;
+            }
+
+            my ( $return, $msg) = $group->AddMember( $member->PrincipalObj->Id );
+            unless ( $return ) {
+                $RT::Logger->error( $msg );
+            } else {
+                $RT::Logger->debug( $return ."." );
+            }
+        }
+    }
     if ( @Queues ) {
         $RT::Logger->debug("Creating queues...");
         for my $item (@Queues) {
@@ -1024,6 +1057,7 @@ sub InsertData {
                 $object = RT::CustomField->new( RT->SystemUser );
                 my @columns = ( Name => $item->{'CF'} );
                 push @columns, LookupType => $item->{'LookupType'} if $item->{'LookupType'};
+                push @columns, ObjectId => $item->{'ObjectId'} if $item->{'ObjectId'};
                 push @columns, Queue => $item->{'Queue'} if $item->{'Queue'} and not ref $item->{'Queue'};
                 my ($ok, $msg) = $object->LoadByName( @columns );
                 unless ( $ok ) {
@@ -1174,7 +1208,12 @@ sub InsertData {
         my $sys = RT::System->new(RT->SystemUser);
 
         for my $item (@Attributes) {
-            my $obj = delete $item->{Object}; # XXX: make this something loadable
+            my $obj = delete $item->{Object};
+
+            if ( ref $obj eq 'CODE' ) {
+                $obj = $obj->();
+            }
+
             $obj ||= $sys;
             my ( $return, $msg ) = $obj->AddAttribute (%$item);
             unless ( $return ) {
index 5ac3f6c..8b8c453 100644 (file)
@@ -62,7 +62,6 @@ use Locale::Maketext 1.04;
 use Locale::Maketext::Lexicon 0.25;
 use base 'Locale::Maketext::Fuzzy';
 
-use Encode;
 use MIME::Entity;
 use MIME::Head;
 use File::Glob;
@@ -282,7 +281,7 @@ sub SetMIMEEntityToEncoding {
     );
 
     # If this is a textual entity, we'd need to preserve its original encoding
-    $head->replace( "X-RT-Original-Encoding" => $charset )
+    $head->replace( "X-RT-Original-Encoding" => Encode::encode( "UTF-8", $charset ) )
         if $head->mime_attr('content-type.charset') or IsTextualContentType($head->mime_type);
 
     return unless IsTextualContentType($head->mime_type);
@@ -291,13 +290,12 @@ sub SetMIMEEntityToEncoding {
 
     if ( $body && ($enc ne $charset || $enc =~ /^utf-?8(?:-strict)?$/i) ) {
         my $string = $body->as_string or return;
+        RT::Util::assert_bytes($string);
 
         $RT::Logger->debug( "Converting '$charset' to '$enc' for "
               . $head->mime_type . " - "
-              . ( $head->get('subject') || 'Subjectless message' ) );
+              . ( Encode::decode("UTF-8",$head->get('subject')) || 'Subjectless message' ) );
 
-        # NOTE:: see the comments at the end of the sub.
-        Encode::_utf8_off($string);
         my $orig_string = $string;
         ( my $success, $string ) = EncodeFromToWithCroak( $orig_string, $charset => $enc );
         if ( !$success ) {
@@ -328,30 +326,11 @@ sub SetMIMEEntityToEncoding {
     }
 }
 
-# NOTES:  Why Encode::_utf8_off before Encode::from_to
-#
-# All the strings in RT are utf-8 now.  Quotes from Encode POD:
-#
-# [$length =] from_to($octets, FROM_ENC, TO_ENC [, CHECK])
-# ... The data in $octets must be encoded as octets and not as
-# characters in Perl's internal format. ...
-#
-# Not turning off the UTF-8 flag in the string will prevent the string
-# from conversion.
-
-
-
 =head2 DecodeMIMEWordsToUTF8 $raw
 
 An utility method which mimics MIME::Words::decode_mimewords, but only
-limited functionality.  This function returns an utf-8 string.
-
-It returns the decoded string, or the original string if it's not
-encoded.  Since the subroutine converts specified string into utf-8
-charset, it should not alter a subject written in English.
-
-Why not use MIME::Words directly?  Because it fails in RT when I
-tried.  Maybe it's ok now.
+limited functionality.  Despite its name, this function returns the
+bytes of the string, in UTF-8.
 
 =cut
 
@@ -537,8 +516,8 @@ use Encode::Guess to try to figure it out the string's encoding.
 
 =cut
 
-use constant HAS_ENCODE_GUESS => do { local $@; eval { require Encode::Guess; 1 } };
-use constant HAS_ENCODE_DETECT => do { local $@; eval { require Encode::Detect::Detector; 1 } };
+use constant HAS_ENCODE_GUESS => Encode::Guess->require;
+use constant HAS_ENCODE_DETECT => Encode::Detect::Detector->require;
 
 sub _GuessCharset {
     my $fallback = _CanonicalizeCharset('iso-8859-1');
@@ -634,8 +613,12 @@ sub _CanonicalizeCharset {
     elsif ( $charset eq 'euc-cn' ) {
         # gbk is superset of gb2312/euc-cn so it's safe
         return 'gbk';
-        # XXX TODO: gb18030 is an even larger, more permissive superset of gbk,
-        # but needs Encode::HanExtra installed
+    }
+    elsif ( $charset =~ /^(?:(?:big5(-1984|-2003|ext|plus))|cccii|unisys|euc-tw|gb18030|(?:cns11643-\d+))$/ ) {
+        unless ( Encode::HanExtra->require ) {
+            RT->Logger->error("Please install Encode::HanExtra to handle $charset");
+        }
+        return $charset;
     }
     else {
         return $charset;
@@ -686,13 +669,13 @@ sub SetMIMEHeadToEncoding {
 
     return if $charset eq $enc and $preserve_words;
 
+    RT::Util::assert_bytes( $head->as_string );
     foreach my $tag ( $head->tags ) {
         next unless $tag; # seen in wild: headers with no name
         my @values = $head->get_all($tag);
         $head->delete($tag);
         foreach my $value (@values) {
             if ( $charset ne $enc || $enc =~ /^utf-?8(?:-strict)?$/i ) {
-                Encode::_utf8_off($value);
                 my $orig_value = $value;
                 ( my $success, $value ) = EncodeFromToWithCroak( $orig_value, $charset => $enc );
                 if ( !$success ) {
index 3c151fe..c6b49bd 100644 (file)
@@ -81,44 +81,21 @@ sub quant {
 
   # Normal case:
   # Note that the formatting of $num is preserved.
-  #return( $handle->numf($num) . ' ' . $handle->numerate($num, @forms) );
-  return( $handle->numerate($num, @forms) );
-   # Most human languages put the number phrase before the qualified phrase.
+  return( $handle->numf($num) . ' ' . $handle->numerate($num, @forms) );
 }
 
 
 sub numerate {
- # return this lexical item in a form appropriate to this number
-  my($handle, $num, @forms) = @_;
-  my $s = ($num == 1);
-
-  return '' unless @forms;
-  return (
-   $s ? $forms[0] :
-   ( $num > 1 && $num < 5 ) ? $forms[1] :
-   $forms[2]
-  ) || (grep defined, @forms)[0];
-}
-
-#--------------------------------------------------------------------------
+    # return this lexical item in a form appropriate to this number
+    my($handle, $num, @forms) = @_;
 
-sub numf {
-  my($handle, $num) = @_[0,1];
-  if($num < 10_000_000_000 and $num > -10_000_000_000 and $num == int($num)) {
-    $num += 0;  # Just use normal integer stringification.
-         # Specifically, don't let %G turn ten million into 1E+007
-  } else {
-    $num = CORE::sprintf("%G", $num);
-     # "CORE::" is there to avoid confusion with the above sub sprintf.
-  }
-  while( $num =~ s/^([-+]?\d+)(\d{3})/$1,$2/s ) {1}  # right from perlfaq5
-   # The initial \d+ gobbles as many digits as it can, and then we
-   #  backtrack so it un-eats the rightmost three, and then we
-   #  insert the comma there.
+    return '' unless @forms;
 
-  $num =~ tr<.,><,.> if ref($handle) and $handle->{'numf_comma'};
-   # This is just a lame hack instead of using Number::Format
-  return $num;
+    my $fallback = (grep defined, @forms)[0];
+    return $forms[0] // $fallback if $num == 1;
+    return $forms[1] // $fallback
+        if $num > 1 and $num < 5;
+    return $forms[2] // $fallback;
 }
 
 RT::Base->_ImportOverlays();
index 33ac68e..c0b932d 100644 (file)
@@ -48,7 +48,6 @@
 
 use strict;
 use warnings;
-use utf8;
 
 package RT::I18N::fr;
 use base 'RT::I18N';
@@ -59,8 +58,8 @@ use warnings;
 sub numf {
         my ($handle, $num) = @_[0,1];
         my $fr_num = $handle->SUPER::numf($num);
-        # French prefer to print 1000 as 1 000 rather than 1,000
-        $fr_num =~ tr<.,><, >;
+        # French prefer to print 1000 as 1(nbsp)000 rather than 1,000
+        $fr_num =~ tr<.,><,\x{A0}>;
         return $fr_num;
 }
 
index 0bce4af..51f7ec0 100644 (file)
@@ -61,7 +61,7 @@ sub quant {
     return $num unless @forms;
     return $forms[3] if !$num && $forms[3];
 
-    return $num .' '. $handle->numerate($num, @forms);
+    return $handle->numf($num) .' '. $handle->numerate($num, @forms);
 }
 
 sub numerate {
index 8b6e540..251b11c 100644 (file)
@@ -121,21 +121,21 @@ loaded with that user.  if the current user isn't found, returns a copy of RT::N
 =cut
 
 sub GetCurrentUser  {
-    
+
     require RT::CurrentUser;
-    
+
     #Instantiate a user object
-    
-    my $Gecos= ($^O eq 'MSWin32') ? Win32::LoginName() : (getpwuid($<))[0];
+
+    my $Gecos= (getpwuid($<))[0];
 
     #If the current user is 0, then RT will assume that the User object
     #is that of the currentuser.
 
     $CurrentUser = RT::CurrentUser->new();
     $CurrentUser->LoadByGecos($Gecos);
-    
+
     unless ($CurrentUser->Id) {
-        $RT::Logger->debug("No user with a unix login of '$Gecos' was found. ");
+        $RT::Logger->error("No user with a GECOS (unix login) of '$Gecos' was found.");
     }
 
     return($CurrentUser);
index 7c1e126..ef939cf 100644 (file)
@@ -55,7 +55,6 @@ use Email::Address;
 use MIME::Entity;
 use RT::EmailParser;
 use File::Temp;
-use UNIVERSAL::require;
 use Mail::Mailer ();
 use Text::ParseWords qw/shellwords/;
 
@@ -111,7 +110,7 @@ sub CheckForLoops {
     my $head = shift;
 
     # If this instance of RT sent it our, we don't want to take it in
-    my $RTLoop = $head->get("X-RT-Loop-Prevention") || "";
+    my $RTLoop = Encode::decode( "UTF-8", $head->get("X-RT-Loop-Prevention") || "" );
     chomp ($RTLoop); # remove that newline
     if ( $RTLoop eq RT->Config->Get('rtname') ) {
         return 1;
@@ -162,17 +161,16 @@ sub CheckForSuspiciousSender {
 
 =head2 CheckForAutoGenerated HEAD
 
-Takes a HEAD object of L<MIME::Head> class and returns true if message
-is autogenerated. Checks 'Precedence' and 'X-FC-Machinegenerated'
-fields of the head in tests.
+Takes a HEAD object of L<MIME::Head> class and returns true if message is
+autogenerated. Checks C<Precedence>, C<Auto-Submitted>, and
+C<X-FC-Machinegenerated> fields of the head in tests.
 
 =cut
 
 sub CheckForAutoGenerated {
     my $head = shift;
 
-    my $Precedence = $head->get("Precedence") || "";
-    if ( $Precedence =~ /^(bulk|junk)/i ) {
+    if (grep { /^(bulk|junk)/i } $head->get_all("Precedence")) {
         return (1);
     }
 
@@ -250,22 +248,27 @@ sub MailError {
     # the colons are necessary to make ->build include non-standard headers
     my %entity_args = (
         Type                    => "multipart/mixed",
-        From                    => $args{'From'},
-        Bcc                     => $args{'Bcc'},
-        To                      => $args{'To'},
-        Subject                 => $args{'Subject'},
-        'X-RT-Loop-Prevention:' => RT->Config->Get('rtname'),
+        From                    => Encode::encode( "UTF-8", $args{'From'} ),
+        Bcc                     => Encode::encode( "UTF-8", $args{'Bcc'} ),
+        To                      => Encode::encode( "UTF-8", $args{'To'} ),
+        Subject                 => EncodeToMIME( String => $args{'Subject'} ),
+        'X-RT-Loop-Prevention:' => Encode::encode( "UTF-8", RT->Config->Get('rtname') ),
     );
 
     # only set precedence if the sysadmin wants us to
     if (defined(RT->Config->Get('DefaultErrorMailPrecedence'))) {
-        $entity_args{'Precedence:'} = RT->Config->Get('DefaultErrorMailPrecedence');
+        $entity_args{'Precedence:'} =
+            Encode::encode( "UTF-8", RT->Config->Get('DefaultErrorMailPrecedence') );
     }
 
     my $entity = MIME::Entity->build(%entity_args);
     SetInReplyTo( Message => $entity, InReplyTo => $args{'MIMEObj'} );
 
-    $entity->attach( Data => $args{'Explanation'} . "\n" );
+    $entity->attach(
+        Type    => "text/plain",
+        Charset => "UTF-8",
+        Data    => Encode::encode( "UTF-8", $args{'Explanation'} . "\n" ),
+    );
 
     if ( $args{'MIMEObj'} ) {
         $args{'MIMEObj'}->sync_headers;
@@ -273,7 +276,7 @@ sub MailError {
     }
 
     if ( $args{'Attach'} ) {
-        $entity->attach( Data => $args{'Attach'}, Type => 'message/rfc822' );
+        $entity->attach( Data => Encode::encode( "UTF-8", $args{'Attach'} ), Type => 'message/rfc822' );
 
     }
 
@@ -364,7 +367,7 @@ sub SendEmail {
         return 0;
     }
 
-    my $msgid = $args{'Entity'}->head->get('Message-ID') || '';
+    my $msgid = Encode::decode( "UTF-8", $args{'Entity'}->head->get('Message-ID') || '' );
     chomp $msgid;
     
     # If we don't have any recipients to send to, don't send a message;
@@ -384,7 +387,7 @@ sub SendEmail {
     if (my $precedence = RT->Config->Get('DefaultMailPrecedence')
         and !$args{'Entity'}->head->get("Precedence")
     ) {
-        $args{'Entity'}->head->set( 'Precedence', $precedence );
+        $args{'Entity'}->head->replace( 'Precedence', Encode::encode("UTF-8",$precedence) );
     }
 
     if ( $TransactionObj && !$TicketObj
@@ -398,15 +401,15 @@ sub SendEmail {
         require RT::Date;
         my $date = RT::Date->new( RT->SystemUser );
         $date->SetToNow;
-        $head->set( 'Date', $date->RFC2822( Timezone => 'server' ) );
+        $head->replace( 'Date', Encode::encode("UTF-8",$date->RFC2822( Timezone => 'server' ) ) );
     }
     unless ( $head->get('MIME-Version') ) {
         # We should never have to set the MIME-Version header
-        $head->set( 'MIME-Version', '1.0' );
+        $head->replace( 'MIME-Version', '1.0' );
     }
     unless ( $head->get('Content-Transfer-Encoding') ) {
         # fsck.com #5959: Since RT sends 8bit mail, we should say so.
-        $head->set( 'Content-Transfer-Encoding', '8bit' );
+        $head->replace( 'Content-Transfer-Encoding', '8bit' );
     }
 
     if ( RT->Config->Get('Crypt')->{'Enable'} ) {
@@ -437,14 +440,15 @@ sub SendEmail {
             my $Overrides = RT->Config->Get('OverrideOutgoingMailFrom') || {};
 
             if ($TicketObj) {
-                my $QueueName = $TicketObj->QueueObj->Name;
-                my $QueueAddressOverride = $Overrides->{$QueueName};
+                my $Queue = $TicketObj->QueueObj;
+                my $QueueAddressOverride = $Overrides->{$Queue->id}
+                    || $Overrides->{$Queue->Name};
 
                 if ($QueueAddressOverride) {
                     $OutgoingMailAddress = $QueueAddressOverride;
                 } else {
-                    $OutgoingMailAddress ||= $TicketObj->QueueObj->CorrespondAddress
-                                             || RT->Config->Get('CorrespondAddress');
+                    $OutgoingMailAddress ||= $Queue->CorrespondAddress
+                        || RT->Config->Get('CorrespondAddress');
                 }
             }
             elsif ($Overrides->{'Default'}) {
@@ -585,10 +589,10 @@ sub SendEmailUsingTemplate {
         return -1;
     }
 
-    $mail->head->set( $_ => Encode::encode_utf8( $args{ $_ } ) )
+    $mail->head->replace( $_ => Encode::encode( "UTF-8", $args{ $_ } ) )
         foreach grep defined $args{$_}, qw(To Cc Bcc From);
 
-    $mail->head->set( $_ => $args{ExtraHeaders}{$_} )
+    $mail->head->replace( $_ => Encode::encode( "UTF-8", $args{ExtraHeaders}{$_} ) )
         foreach keys %{ $args{ExtraHeaders} };
 
     SetInReplyTo( Message => $mail, InReplyTo => $args{'InReplyTo'} );
@@ -671,7 +675,7 @@ sub SignEncrypt {
     );
     return 1 unless $args{'Sign'} || $args{'Encrypt'};
 
-    my $msgid = $args{'Entity'}->head->get('Message-ID') || '';
+    my $msgid = Encode::decode( "UTF-8", $args{'Entity'}->head->get('Message-ID') || '' );
     chomp $msgid;
 
     $RT::Logger->debug("$msgid Signing message") if $args{'Sign'};
@@ -808,9 +812,6 @@ sub EncodeToMIME {
 
     $value =~ s/\s+$//;
 
-    # we need perl string to split thing char by char
-    Encode::_utf8_on($value) unless Encode::is_utf8($value);
-
     my ( $tmp, @chunks ) = ( '', () );
     while ( length $value ) {
         my $char = substr( $value, 0, 1, '' );
@@ -912,7 +913,8 @@ sub ParseCcAddressesFromHead {
     return
         grep $_ ne $current_address && !RT::EmailParser->IsRTAddress( $_ ),
         map lc $user->CanonicalizeEmailAddress( $_->address ),
-        map RT::EmailParser->CleanupAddresses( Email::Address->parse( $args{'Head'}->get( $_ ) ) ),
+        map RT::EmailParser->CleanupAddresses( Email::Address->parse(
+              Encode::decode( "UTF-8", $args{'Head'}->get( $_ ) ) ) ),
         qw(To Cc);
 }
 
@@ -938,7 +940,7 @@ sub ParseSenderAddressFromHead {
 
     #Figure out who's sending this message.
     foreach my $header ( @sender_headers ) {
-        my $addr_line = $head->get($header) || next;
+        my $addr_line = Encode::decode( "UTF-8", $head->get($header) ) || next;
         my ($addr, $name) = ParseAddressFromHeader( $addr_line );
         # only return if the address is not empty
         return ($addr, $name, @errors) if $addr;
@@ -966,7 +968,7 @@ sub ParseErrorsToAddressFromHead {
     foreach my $header ( 'Errors-To', 'Reply-To', 'From', 'Sender' ) {
 
         # If there's a header of that name
-        my $headerobj = $head->get($header);
+        my $headerobj = Encode::decode( "UTF-8", $head->get($header) );
         if ($headerobj) {
             my ( $addr, $name ) = ParseAddressFromHeader($headerobj);
 
@@ -1011,9 +1013,9 @@ sub DeleteRecipientsFromHead {
     my %skip = map { lc $_ => 1 } @_;
 
     foreach my $field ( qw(To Cc Bcc) ) {
-        $head->set( $field =>
+        $head->replace( $field => Encode::encode( "UTF-8",
             join ', ', map $_->format, grep !$skip{ lc $_->address },
-                Email::Address->parse( $head->get( $field ) )
+                Email::Address->parse( Encode::decode( "UTF-8", $head->get( $field ) ) ) )
         );
     }
 }
@@ -1046,7 +1048,7 @@ sub SetInReplyTo {
     my $get_header = sub {
         my @res;
         if ( $args{'InReplyTo'}->isa('MIME::Entity') ) {
-            @res = $args{'InReplyTo'}->head->get( shift );
+            @res = map {Encode::decode("UTF-8", $_)} $args{'InReplyTo'}->head->get( shift );
         } else {
             @res = $args{'InReplyTo'}->GetHeader( shift ) || '';
         }
@@ -1069,8 +1071,8 @@ sub SetInReplyTo {
         if @references > 10;
 
     my $mail = $args{'Message'};
-    $mail->head->set( 'In-Reply-To' => Encode::encode_utf8(join ' ', @rtid? (@rtid) : (@id)) ) if @id || @rtid;
-    $mail->head->set( 'References' => Encode::encode_utf8(join ' ', @references) );
+    $mail->head->replace( 'In-Reply-To' => Encode::encode( "UTF-8", join ' ', @rtid? (@rtid) : (@id)) ) if @id || @rtid;
+    $mail->head->replace( 'References' => Encode::encode( "UTF-8", join ' ', @references) );
 }
 
 sub PseudoReference {
@@ -1078,14 +1080,35 @@ sub PseudoReference {
     return '<RT-Ticket-'. $ticket->id .'@'. RT->Config->Get('Organization') .'>';
 }
 
+=head2 ExtractTicketId
+
+Passed a MIME::Entity.  Returns a ticket id or undef to signal 'new ticket'.
+
+This is a great entry point if you need to customize how ticket ids are
+handled for your site. RT-Extension-RepliesToResolved demonstrates one
+possible use for this extension.
+
+If the Subject of this ticket is modified, it will be reloaded by the
+mail gateway code before Ticket creation.
+
+=cut
+
 sub ExtractTicketId {
     my $entity = shift;
 
-    my $subject = $entity->head->get('Subject') || '';
+    my $subject = Encode::decode( "UTF-8", $entity->head->get('Subject') || '' );
     chomp $subject;
     return ParseTicketId( $subject );
 }
 
+=head2 ParseTicketId
+
+Takes a string and searches for [subjecttag #id]
+
+Returns the id if a match is found.  Otherwise returns undef.
+
+=cut
+
 sub ParseTicketId {
     my $Subject = shift;
 
@@ -1296,14 +1319,14 @@ sub Gateway {
     my $head = $Message->head;
     my $ErrorsTo = ParseErrorsToAddressFromHead( $head );
     my $Sender = (ParseSenderAddressFromHead( $head ))[0];
-    my $From = $head->get("From");
+    my $From = Encode::decode( "UTF-8", $head->get("From") );
     chomp $From if defined $From;
 
-    my $MessageId = $head->get('Message-ID')
+    my $MessageId = Encode::decode( "UTF-8", $head->get('Message-ID') )
         || "<no-message-id-". time . rand(2000) .'@'. RT->Config->Get('Organization') .'>';
 
     #Pull apart the subject line
-    my $Subject = $head->get('Subject') || '';
+    my $Subject = Encode::decode( "UTF-8", $head->get('Subject') || '');
     chomp $Subject;
     
     # Lets check for mail loops of various sorts.
@@ -1326,7 +1349,7 @@ sub Gateway {
     $args{'ticket'} ||= ExtractTicketId( $Message );
 
     # ExtractTicketId may have been overridden, and edited the Subject
-    my $NewSubject = $Message->head->get('Subject');
+    my $NewSubject = Encode::decode( "UTF-8", $Message->head->get('Subject') );
     chomp $NewSubject;
 
     $SystemTicket = RT::Ticket->new( RT->SystemUser );
@@ -1570,7 +1593,7 @@ sub _RunUnsafeAction {
         @_
     );
 
-    my $From = $args{Message}->head->get("From");
+    my $From = Encode::decode( "UTF-8", $args{Message}->head->get("From") );
 
     if ( $args{'Action'} =~ /^take$/i ) {
         my ( $status, $msg ) = $args{'Ticket'}->SetOwner( $args{'CurrentUser'}->id );
@@ -1726,7 +1749,7 @@ sub _HandleMachineGeneratedMail {
         # to the scrip. We might want to notify nobody. Or just
         # the RT Owner. Or maybe all Privileged watchers.
         my ( $Sender, $junk ) = ParseSenderAddressFromHead($head);
-        $head->replace( 'RT-Squelch-Replies-To',    $Sender );
+        $head->replace( 'RT-Squelch-Replies-To',    Encode::encode("UTF-8", $Sender ) );
         $head->replace( 'RT-DetectedAutoGenerated', 'true' );
     }
     return ( 1, $ErrorsTo, "Handled machine detection", $IsALoop );
@@ -1751,9 +1774,10 @@ sub IsCorrectAction {
 sub _RecordSendEmailFailure {
     my $ticket = shift;
     if ($ticket) {
-        $ticket->_RecordNote(
-            NoteType => 'SystemError',
-            Content => "Sending the previous mail has failed.  Please contact your admin, they can find more details in the logs.",
+        $ticket->_NewTransaction(
+            Type => "SystemError",
+            Data => "Sending the previous mail has failed.  Please contact your admin, they can find more details in the logs.", #loc
+            ActivateScrips => 0,
         );
         return 1;
     }
index 2703af9..17df0d7 100644 (file)
@@ -66,6 +66,9 @@ it put the module in the mail plugins list:
 
     Set(@MailPlugins, 'Auth::MailFrom', 'Auth::Crypt', ...other filters...);
 
+C<Auth::Crypt> will not function without C<Auth::MailFrom> listed before
+it.
+
 =head3 GnuPG
 
 To use the gnupg-secured mail gateway, you need to do the following:
@@ -172,7 +175,7 @@ sub GetCurrentUser {
 
         foreach my $protocol ( @check_protocols ) {
             my @status = grep defined && length,
-                $part->head->get( "X-RT-$protocol-Status" );
+                map Encode::decode( "UTF-8", $_), $part->head->get( "X-RT-$protocol-Status" );
             next unless @status;
 
             push @found, $protocol;
@@ -183,20 +186,20 @@ sub GetCurrentUser {
                 }
                 if ( $_->{Operation} eq 'Verify' && $_->{Status} eq 'DONE' ) {
                     $part->head->replace(
-                        'X-RT-Incoming-Signature' => $_->{UserString}
+                        'X-RT-Incoming-Signature' => Encode::encode( "UTF-8", $_->{UserString} )
                     );
                 }
             }
         }
 
         $part->head->replace(
-            'X-RT-Incoming-Encryption' => 
+            'X-RT-Incoming-Encryption' =>
                 $decrypted ? 'Success' : 'Not encrypted'
         );
     }
 
     my %seen;
-    $args{'Message'}->head->replace( 'X-RT-Privacy' => $_ )
+    $args{'Message'}->head->replace( 'X-RT-Privacy' => Encode::encode( "UTF-8", $_ ) )
         foreach grep !$seen{$_}++, @found;
 
     return 1;
index 17fe446..06d7f83 100644 (file)
@@ -328,7 +328,7 @@ sub process_attachments {
             Path => $tmp_fn,
             Type => $info->{'Content-Type'} || guess_media_type($tmp_fn),
             Filename => $file,
-            Disposition => "attachment",
+            Disposition => $info->{'Content-Disposition'} || "attachment",
         );
         $new_entity->bodyhandle->{'_dirty_hack_to_save_a_ref_tmp_fh'} = $tmp_fh;
         $i++;
index 0aebeed..a2fa00f 100644 (file)
@@ -68,9 +68,9 @@ use URI qw();
 use RT::Interface::Web::Menu;
 use RT::Interface::Web::Session;
 use Digest::MD5 ();
-use Encode qw();
 use List::MoreUtils qw();
 use JSON qw();
+use Plack::Util;
 
 =head2 SquishedCSS $style
 
@@ -105,7 +105,7 @@ sub SquishedJS {
 =cut
 
 sub JSFiles {
-    return qw/
+    return qw{
       jquery-1.9.1.min.js
       jquery_noconflict.js
       jquery-ui-1.10.0.custom.min.js
@@ -127,7 +127,8 @@ sub JSFiles {
       forms.js
       event-registration.js
       late.js
-      /, RT->Config->Get('JSFiles');
+      /static/RichText/ckeditor.js
+      }, RT->Config->Get('JSFiles');
 }
 
 =head2 ClearSquished
@@ -704,11 +705,6 @@ sub AttemptExternalAuth {
         $user = RT::Interface::Web::WebCanonicalizeInfo();
         my $load_method = RT->Config->Get('WebRemoteUserGecos') ? 'LoadByGecos' : 'Load';
 
-        if ( $^O eq 'MSWin32' and RT->Config->Get('WebRemoteUserGecos') ) {
-            my $NodeName = Win32::NodeName();
-            $user =~ s/^\Q$NodeName\E\\//i;
-        }
-
         my $next = RemoveNextPage($ARGS->{'next'});
            $next = $next->{'url'} if ref $next;
         InstantiateNewSession() unless _UserLoggedIn;
@@ -826,7 +822,7 @@ sub AttemptPasswordAuthentication {
         InstantiateNewSession();
         $HTML::Mason::Commands::session{'CurrentUser'} = $user_obj;
 
-        $m->callback( %$ARGS, CallbackName => 'SuccessfulLogin', CallbackPage => '/autohandler' );
+        $m->callback( %$ARGS, CallbackName => 'SuccessfulLogin', CallbackPage => '/autohandler', RedirectTo => \$next );
 
         # Really the only time we don't want to redirect here is if we were
        &